Get started
Make it yours
Change the colours, fonts, sizes, corners and shadows of LUZ with tokens, and adjust one component with CSS of your own.
LUZ is made to be changed from the outside. You do not edit its files: you write CSS of your own that loads after it, and the rule that matters is that your CSS wins.
Change a token, not a component
LUZ's components read tokens: custom properties that start with --luz-. To change how LUZ looks, write the token again in your own CSS.
Every component that reads it follows.
:root {
--luz-radius-md: 2px;
}
- Write the token on
:root, in CSS that is not inside an@layer. - CSS outside any layer wins over every LUZ layer, wherever it sits in the file. You do not need
!important, or a longer selector. - If you use layers of your own, declare their order first. Cascade layers has the order and the three ways to override LUZ.
- Token names are on the Tokens pages. A token you write that LUZ does not read does nothing.
Colours and modes
LUZ ships one look in three modes: light, dark and high contrast. Colours are semantic tokens, such as --luz-brand-fill and --luz-surface-page, and each one has a value for each mode.
To change them, make your own theme: it gives you a template, the steps and where to load it. Then run the contrast check on your colours, which measures the pairs LUZ promises to keep readable.
To see every component in the three modes, open all components on one page.
Fonts
Two tokens say which font is used for what. Three more hold the font stacks.
--luz-font-body: running text. The serif, ET Book, by default.--luz-font-heading: headings. The serif by default.--luz-font-sans,--luz-font-serifand--luz-font-mono: the three stacks. Controls, labels and tables use the sans, and code uses the mono.
@font-face {
font-family: "My Serif";
src: url("/fonts/my-serif.woff2") format("woff2");
font-display: swap;
}
:root {
--luz-font-serif: "My Serif", georgia, serif;
}
- Your own
@font-facegoes in your CSS, as above. Setting--luz-font-serifchanges the body and the headings, because both use it by default. - To drop ET Book, load
tokens.cssinstead ofluz-tokens.css:@luz-ds/tokens/tokens.cssfrom a bundler,dist/tokens.min.cssfrom a CDN. It has the same tokens and no@font-face. - On an element marked
lang="ar", body and headings already take the sans: ET Book has no Arabic letters. A rule of your own, outside LUZ's layers, still wins. - A font you bring has its own licence. Read it before you publish the file.
Typography lists the font roles and the type scale.
Text and space sizes
Three tokens, called knobs, set the size of the whole type scale and the whole space scale. Every type step, --luz-text-xs to --luz-text-4xl, and every space step, --luz-space-3xs to --luz-space-4xl, is worked out from them, so one line changes the page.
| Knob | Default | What it changes |
|---|---|---|
--luz-type-base | 1rem | The size of --luz-text-base. Every other step is computed from it. |
--luz-type-ratio | 1.25 | The multiple between two steps above the base, and the divisor below it. |
--luz-space-unit | 0.25rem | The grid of the spacing scale: every space step is a multiple of it. |
:root {
--luz-type-base: 1.125rem;
--luz-type-ratio: 1.2;
--luz-space-unit: 0.1875rem;
}
Rules for the knobs
- Write lengths in rem. A rem follows the font size the reader chose in their browser settings. A value in px pins the size and ignores that setting. Spacing is in rem too, so it grows with the text.
- Set them on
:root. The steps are worked out once, on:root, and passed down as finished values. A knob onbodyor on a section changes nothing. Sizes are a choice for the whole page.
/* This changes nothing: the steps are already worked out on :root */
body {
--luz-type-base: 1.5rem;
}
The ranges LUZ is tested at
--luz-type-base: 0.875rem to 1.25rem.--luz-type-ratio: 1.125 to 1.5.--luz-space-unit: 0.1875rem to 0.3125rem, which is 3px to 5px at a 16px root.
Outside these ranges it still works, but nobody has looked at the result. The colour contrast check measures colours only: it does not look at sizes.
What does not move
- Small text keeps a floor.
--luz-text-xsis never under 0.75rem and--luz-text-smnever under 0.875rem, whatever the base. - On a touch screen, and at 768px and below, form text is never under 1rem.
- Control heights,
--luz-control-smto--luz-control-xl(28, 36, 44 and 56px), are not knobs. A smaller unit never makes a target smaller. - Running text is
--luz-text-body, 1.0625 times the base, so the base moves it too.
By width, or on paper
The knobs are on :root, so a media query can change them.
@media (min-width: 80em) {
:root {
--luz-type-base: 1.125rem;
}
}
@media print {
:root {
--luz-type-base: 0.9375rem;
}
}
Try them
These are the nine type sizes of five combinations of the knobs. The steps are --luz-text-xs to --luz-text-4xl, in px.
| Preset | xs | sm | md | base | lg | xl | 2xl | 3xl | 4xl |
|---|---|---|---|---|---|---|---|---|---|
Default | 12 | 14 | 15 | 16 | 20 | 25 | 31 | 39 | 49 |
Larger text --luz-type-base: 1.125rem | 12 | 14 | 16 | 18 | 23 | 29 | 36 | 45 | 56 |
Tighter steps --luz-type-ratio: 1.2 | 12 | 14 | 15 | 16 | 19 | 23 | 28 | 34 | 41 |
Small base --luz-type-base: 0.875rem | 12 | 14 | 14 | 14 | 18 | 23 | 29 | 36 | 45 |
Large and wide --luz-type-base: 1.25rem; --luz-type-ratio: 1.333 | 12 | 15 | 18 | 20 | 27 | 36 | 48 | 64 | 85 |
- The table is for a root font size of 16px, the browser default. If the reader's browser is set to 20px, every size is 1.25 times larger. That is why the knobs are in rem.
- With a base of 0.875rem the floors hold:
--luz-text-sm,--luz-text-mdand--luz-text-baseare all 14px. - Each step is rounded to 1/16 rem, which is a whole pixel at a 16px root. A browser without
round()uses the plain chain: at the defaults the three largest steps are at most 0.25px away.
Typography and Spacing and sizing list each step with its formula.
Corners
--luz-radius-md is the ceiling: no rectangle is rounder. It is the corner of buttons, cards, alerts, menus and dialogs.
Three more steps cover the other cases.
--luz-radius-none(0px): a square corner, for a box that joins a neighbour.--luz-radius-sm(2px): small marks, such as inline code and a checkbox.--luz-radius-full(9999px): a pill or a circle, for shapes that are round by nature. It is not a corner for a box.
:root {
--luz-radius-sm: 0;
--luz-radius-md: 0;
}
Radius says which component uses which step.
Shadows
Three tokens set how far a surface seems to float: --luz-shadow-sm for a card, --luz-shadow-md for tooltips and toasts, and --luz-shadow-lg for dialogs and the drawer.
LUZ redraws them in dark mode, and in high contrast it draws outlines instead. A rule on :root outside any layer wins in every mode, so one value serves them all.
If you want the dark ones heavier, set them for that mode as well.
:root {
--luz-shadow-md: 0 2px 6px rgba(0, 0, 0, 0.12);
}
[data-theme="dark"] {
--luz-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.6);
}
Never let a shadow be the only edge of a surface: it disappears in forced colors. Shadows has the values, and Color modes how the three modes are chosen.
Adjust one component
Change a token first: every component that reads it follows, in every mode. When one component needs a rule of its own, write it in CSS outside any layer.
It wins over every LUZ layer, so the selector can stay short.
.luz-btn {
min-height: 3rem;
}
If your own CSS is in layers, declare the order first and put the rule in your layer. app comes after luz, so it wins.
@layer reset, luz, app;
@layer app {
.luz-btn {
min-height: 3rem;
}
}
Three things to avoid.
!important. It is never needed: unlayered CSS and the order of layers decide first.- A status colour for something else. Success, warning, danger and info keep their meaning, so do not use them to decorate.
- Removing the focus ring.
outline: noneleaves a keyboard user with no way to see where they are. Change its colour with--luz-focus-ringand check the result with the contrast check.
Cascade layers has the full order of LUZ's layers.
Icons
LUZ draws icons of its own, on the Icons page, which also has the markup. For hundreds, bring a set.
Whatever set you use, the pattern is the same:
- Use an inline
<svg>that takescurrentColor. It follows the text colour and the mode, and it works in forced colors. - Mark it decorative with
aria-hidden="true"andfocusable="false". Its name comes from the visible text beside it, or from a.u-sr-onlyspan in the button. - Size it in em, such as
1.25em, so it follows the text and the size knobs. There is no icon-size token on purpose. Inside.luz-btnan icon is 16px already. - Draw one icon many times from one
<symbol>sprite in the same document, never<use href="https://…">, which browsers block across origins. - An icon never carries meaning alone: a status icon comes with words. Do not use a set's brand logos as a status or an action. They are trademarks, even where the set is MIT.
<button class="luz-btn luz-btn--secondary" type="button">
<svg viewBox="0 0 24 24" width="1.25em" height="1.25em" aria-hidden="true" focusable="false">
<use href="#icon-download"></use>
</svg>
Download
</button>
Sets with an MIT licence
These five are under the MIT licence only, the licence LUZ itself uses. Each licence file was read on 2026-10-15.
| Set | Licence | Licence file | Good to know |
|---|---|---|---|
MIT | A 24px grid and a 2px stroke, like LUZ's own icons. Check how recently the repository was updated before you choose it. | ||
MIT | Outline icons at 24px, solid icons at 24, 20 and 16px. | ||
MIT | A very large set. The stroke width can be changed; it is 2px by default. | ||
MIT | Six weights. Take one, so the icons match. | ||
MIT | A 16px grid. |
Iconoir, Ionicons, Radix Icons and Fluent UI System Icons are also under MIT, as of 2026-10-15. They are not on this list to keep it short.
LUZ does not bundle these and does not test them. MIT asks you to keep the copyright notice: copy the notice of the set you take next to its icons, and check the licence file of the version you take.
Sets LUZ does not recommend
These are popular, and each has a licence that is not MIT only. The reason is listed so nobody adds them by mistake.
| Set | Licence | Why it is out |
|---|---|---|
Lucide | ISC | ISC is a different licence from MIT. |
Remix Icon | Its own licence, Remix Icon License v1.0 | It is not MIT. The repository and the npm package name different licences, so the set is unclear. |
Material Symbols and Material Design Icons | Apache-2.0 | Apache-2.0 is not MIT. |
Font Awesome Free | Icons CC BY 4.0, fonts OFL 1.1, code MIT | The icons are under a Creative Commons licence and the fonts under OFL. |
Simple Icons | CC0-1.0 | CC0 is not MIT. |
Octicons | MIT, with an exception | The licence file is MIT, but the README says the GitHub logos follow GitHub's logo guidelines. The set holds files that are not under MIT. |
What not to change
Everything above is safe to change. These are not, and each has a reason.
- The colour scales. Components read semantic tokens. Change those, in a theme of your own, not the palette they are built on.
- The status colours.
--luz-color-success,--luz-color-warning,--luz-color-dangerand--luz-color-infomean what their names say. Do not use them for anything else. - The focus ring. You can set its colour. You cannot remove it.
- The 44px targets. The control heights,
--luz-control-smto--luz-control-xl, are not knobs. - The files in
dist/. They are generated and replaced on every release. Your changes belong in your own CSS.