Release notes
Version 3.1 styles the elements that had no look of their own.
Every element and component of LUZ, one under another, to look at by hand. This page is not indexed.
Every tag with no class, as its card in the docs shows it.
Read the full reference before you start.
LUZ uses plain CSS.
Version 3.1 styles the elements that had no look of their own.
The ET Book typeface is used for headings.
<base href="https://example.com/app/" target="_blank">
User إيان scored 90 points.
This text runs right to left.
The best interface is the one the browser already ships.
<body class="o-shell">
<a class="luz-skip-link" href="#main-content">Skip to main content</a>
<main id="main-content">…</main>
</body>
Line one
Line two
Line three
| Package | Layer |
|---|---|
| tokens | 1 |
| core | 2 |
The Design of Everyday Things is a book by Don Norman.
Use var(--luz-brand-primary) for links.
| Package | Layer |
|---|---|
| tokens | 1 |
| core | 2 |
| Package | Layer |
|---|---|
| tokens | 1 |
| core | 2 |
LUZ has three layers.
Price: $20 $15
A vanilla design system in plain CSS and ES modules.
A design token is a named design decision, stored as a variable.
Load the tokens before the core.
<geolocation>
<button id="fallback">Use location</button>
</geolocation>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My page</title>
</head>
Three layers, adopted in order.
Variables for every design decision.
Above the break.
Below the break.
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>…</head>
<body>…</body>
</html>
The result is ad hoc, not a rule.
Price: $20 $15
Press Ctrl + K to open the command palette.
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@luz-ds/tokens@0.3.0/dist/luz-tokens.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@luz-ds/core@0.3.0/dist/luz.min.css">
<main id="main-content" tabindex="-1">
<h1>Page title</h1>
…
</main>
Search results for tokens.
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="A vanilla design system in plain CSS.">
<noscript>
<p>This page needs JavaScript for the live demos.</p>
</noscript>
LUZ is a vanilla design system: tokens, components and app patterns in plain CSS and ES modules, with no framework and no build step.
$ pnpm add @luz-ds/tokens
$ pnpm add @luz-ds/core
As they say, less is more
.
$20 $15 this week.
The console prints Layer "tokens" is missing.
<script type="module" src="https://cdn.jsdelivr.net/npm/@luz-ds/core@0.3.0/dist/luz.min.js"></script>
Add the tokens first, then the core.
<select>
<button>
<selectedcontent></selectedcontent>
</button>
<option value="design">Design</option>
<option value="build">Build</option>
</select>
Total: $15 Prices include VAT.
LUZ is stable.
Warning: do not edit the dist folder.
<style>
.banner { padding: var(--luz-space-md); }
</style>
Water is H2O.
A vanilla design system in plain CSS and ES modules.
E = mc2
| Package | Layer |
|---|---|
| tokens | 1 |
| core | 2 |
| Package | Layer |
|---|---|
| tokens | 1 |
| core | 2 |
| Package | Layer |
|---|---|
| tokens | 1 |
| core | 2 |
| Package | Layer |
|---|---|
| tokens | 1 |
| core | 2 |
| Total | 2 |
| Package | Layer |
|---|---|
| tokens | 1 |
| core | 2 |
| Package | Layer |
|---|---|
| tokens | 1 |
| core | 2 |
Released on .
<title>HTML elements: LUZ Design System</title>
| Package | Layer |
|---|---|
| tokens | 1 |
| core | 2 |
This word is mispelled.
Solve x + y = 10.
https://
The o-* objects, as the layout page shows them.
The example of each core component.
A named value, such as a color or a space, that components read instead of a literal.
The same name on each details connects them: opening one closes the other.
No. The browser opens, closes and groups them.
The hall holds 400 people, and the Two days ticket sold out last year.
The tint runs to the edges of this frame; in a page, to the edges of the window. The content keeps the column.
The trail at the top of this page is the same component.
1,204 requests per minute.
48 active connections.
Rows of records that share the same attributes.
Looks after billing and invoices.
A two-hour walk from the car park.
48 active connections.
Bodega Albar · 75 cl
12,50 €
Bodega Albar · 75 cl
Now 9,90 € Was 12,50 €
79 €
The talks and the workshops of one day.
Choose One dayNow 149 € Was 180 €
Both days, lunch and the recordings. Most chosen
Choose Two days590 €
Two days for five people, with a shared table.
Choose Team
Tides for any harbour on the coast, read from a public data set and kept up to date every night, with a chart of the week.
A small parser.
.panel {
padding: var(--luz-space-md);
color: var(--luz-text-primary);
background: var(--luz-surface);
}
<button
class="luz-btn luz-btn--primary"
type="button">
Save changes
</button>
import { MidiPlayer }
from "@luz-ds/core/midi";
const player = await MidiPlayer.load(url);
player.play();
Inline code looks like var(--luz-text-primary) inside a sentence.
LUZ's own scheme: keywords stay ink and bold, colour goes to what the author wrote (names, properties, values), comments step back in italic. No red, because red means error.
Each colour keeps 4.5:1 or more on the block, 7:1 or more in high contrast.
| Token | Class | What it marks |
|---|---|---|
--luz-code-comment | .tok-comment | Comments in every language. Muted and italic. |
--luz-code-keyword | .tok-keyword | JavaScript and shell keywords, CSS at-rules and !important, the HTML doctype. Ink and bold, no colour. |
--luz-code-name | .tok-name | HTML tags, CSS selectors and functions, JavaScript function and class names, shell commands. |
--luz-code-property | .tok-property | HTML attributes, CSS properties and custom properties, JSON keys, shell flags and variables. |
--luz-code-string | .tok-string | Quoted strings in every language, HTML attribute values. |
--luz-code-literal | .tok-literal | Numbers and units, hex colours, true, false, null, HTML entities. |
/* Wide cards */
@media (min-width: 48rem) {
.card::after {
content: "new";
inset: 0 auto auto 50%;
color: var(--ink, #036);
}
}
<!doctype html>
<!-- Primary action -->
<button type="submit" disabled>
Save & close
</button>
// Play a track
const player = await
MidiPlayer.load("a.mid");
if (player) player.play(0, true);
{
"name": "@luz-ds/core",
"private": false,
"size": 7.02
}
# Build and test
pnpm core:build
npx playwright test \
--project=chromium $CI
/* Wide cards */
@media (min-width: 48rem) {
.card::after {
content: "new";
inset: 0 auto auto 50%;
color: var(--ink, #036);
}
}
<!doctype html>
<!-- Primary action -->
<button type="submit" disabled>
Save & close
</button>
// Play a track
const player = await
MidiPlayer.load("a.mid");
if (player) player.play(0, true);
{
"name": "@luz-ds/core",
"private": false,
"size": 7.02
}
# Build and test
pnpm core:build
npx playwright test \
--project=chromium $CI
/* Wide cards */
@media (min-width: 48rem) {
.card::after {
content: "new";
inset: 0 auto auto 50%;
color: var(--ink, #036);
}
}
<!doctype html>
<!-- Primary action -->
<button type="submit" disabled>
Save & close
</button>
// Play a track
const player = await
MidiPlayer.load("a.mid");
if (player) player.play(0, true);
{
"name": "@luz-ds/core",
"private": false,
"size": 7.02
}
# Build and test
pnpm core:build
npx playwright test \
--project=chromium $CI
var(--luz-brand-primary)
The button copies the text of its data-copy attribute.
Press Escape or click outside to close it.
The logo is not an icon of the set: it is filled, in four shades of the text colour, on its own 32 × 32 grid. Copy it as it is; do not draw it with a stroke.
Each icon has a button that copies its SVG. These are LUZ's own icons, drawn for it: copy the SVG or the sprite.
They are not a package, so nothing is loaded for them.
Ratios: 16:9, --4x3, --3x2, --1x1, --21x9, --9x16.
| Route | Parcels |
|---|---|
| Sevilla to Madrid | 1,204 |
| Cádiz to Málaga | 312 |
| Huelva to Faro | 58 |
A native <dialog>.
Escape closes it and focus goes back to the button that opened it. Nothing is deleted here.
Video, MP4, 15 seconds, with English captions shown by default and Spanish subtitles. It has no sound: both tracks describe the picture.
Video: NASA, Earth Views from the ISS, in the public domain.
Audio, MP3, 25 seconds.
Nothing is downloaded until you press play. Recording: NASA, Apollo 11 launch countdown and lift-off, in the public domain.
MIDI playback needs JavaScript.
MIDI, parsed and played with Web Audio by midi.js, which loads only when a MIDI player is on the page.
Since March 2016, a group of volunteers has read the tide gauge in the port at six in the morning and six in the evening. Their sheets are the longest record of the sea in the bay that anyone has kept by hand, and the council will publish them as a table of open data.
Each reading is the height of the water against the gauge, to the nearest centimetre. The sheet calls it level_cm.
We did not set out to measure the climate. We wanted to know when to take the boats out.
The average height at high tide was 4 cm higher in 2025 than in 2016, close to what the national gauges record along the coast.
This page has one: press Tab at the top of the page to see it.
| Service | Status | CPU | Region |
|---|---|---|---|
| api-gateway | Running | 12% | eu-west-1 |
| data-pipeline | Degraded | 78% | us-east-1 |
| mailer | Stopped | 0% | eu-west-1 |
Make the window narrow: below 480 px of container width each row becomes a card.
| Route | Parcels | Weight (kg) |
|---|---|---|
| Sevilla to Madrid | 1,204 | 8,430.5 |
| Cádiz to Málaga | 312 | 1,912.0 |
| Huelva to Faro | 58 | 406.3 |
| Route | Parcels | Weight (kg) | Notes |
|---|---|---|---|
| Cádiz to Málaga | 312 | 1,912.0 | Daily |
| Huelva to Faro | 58 | 406.3 | Monday and Thursday |
| Sevilla to Madrid | 1,204 | 8,430.5 | Daily |
Three services are running.
Deploys run on every push.
No errors in the last hour.
Focus a tab, then use the arrow keys, Home and End. The Code and Design tabs of this page are the same component.
Radio group
Button, fixed at the end corner
Button, inline
Without JavaScript the radios show in the flow, as in the first stage, and there is no button.
A static specimen: LUZ draws the toast, and you place it, show it and hide it.
Point at the button, or focus it with Tab.
The example of each catalog component and pattern.
Open day. Visit the winery on Saturday 25 October, from 11:00 to 18:00. Book a visit
Planned maintenance. The shop will be closed on Monday 3 November from 07:00 to 09:00. See what still works
On a site the announcement goes above the header, as wide as the window, and its text starts where the header’s row starts. Here it is as wide as the example.
The bar at a window of full width: the sidebar is a column beside it, so the product name stays there. At 768 px and below the sidebar is a drawer, the menu button opens it and the product name shows beside it.
The example is not sticky: in a page, .o-sticky keeps the bar at the top. The account list opens under the button: press it.
What ten years of tide records say about the bay
Volunteers have read the gauge in the port twice a day since 2016. The sea is four centimetres higher, and winter storms now reach the old town's walls.
| Month | Sevilla | Cádiz | Jerez |
|---|---|---|---|
| Jan | 1,204 | 312 | 640 |
| Feb | 1,310 | 340 | 620 |
| Mar | 1,180 | 365 | 700 |
| Apr | 1,420 | 410 | 690 |
| May | 1,390 | 398 | 760 |
| Jun | 1,480 | 452 | 810 |
One column is one series, up to six. Without JavaScript it is the table.
| Month | Sevilla |
|---|---|
| Jan | 1,204 |
| Feb | 1,310 |
| Mar | 1,180 |
| Apr | 1,420 |
| May | 1,390 |
| Jun | 1,480 |
With variant="area".
type="spark": a small line, and its last figure as text. The other six stay in the list for a screen reader.
| Day | Desktop | Mobile |
|---|---|---|
| Apr 1 | 252 | 148 |
| Apr 2 | 271 | 276 |
| Apr 3 | 182 | 128 |
| Apr 4 | 380 | 247 |
| Apr 5 | 194 | 203 |
| Apr 6 | 191 | 74 |
| Apr 7 | 179 | 98 |
| Apr 8 | 179 | 132 |
| Apr 9 | 281 | 217 |
| Apr 10 | 187 | 171 |
| Apr 11 | 193 | 251 |
| Apr 12 | 278 | 125 |
| Apr 13 | 228 | 152 |
| Apr 14 | 120 | 100 |
| Apr 15 | 331 | 270 |
| Apr 16 | 319 | 125 |
| Apr 17 | 317 | 259 |
| Apr 18 | 271 | 122 |
| Apr 19 | 226 | 121 |
| Apr 20 | 187 | 86 |
| Apr 21 | 146 | 130 |
| Apr 22 | 206 | 248 |
| Apr 23 | 200 | 256 |
| Apr 24 | 248 | 253 |
| Apr 25 | 378 | 284 |
| Apr 26 | 216 | 136 |
| Apr 27 | 190 | 153 |
| Apr 28 | 199 | 94 |
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
| 1,204 | 830 | 901 | 1,420 | 1,500 | 377 | 0 |
| Month | Desktop | Mobile |
|---|---|---|
| Jan | 186 | 80 |
| Feb | 305 | 200 |
| Mar | 237 | 120 |
| Day | Desktop | Mobile |
|---|---|---|
| Apr 1 | 252 | 148 |
| Apr 2 | 271 | 276 |
| Apr 3 | 182 | 128 |
| Apr 4 | 380 | 247 |
| Apr 5 | 194 | 203 |
| Apr 6 | 191 | 74 |
| Apr 7 | 179 | 98 |
| Apr 8 | 179 | 132 |
| Apr 9 | 281 | 217 |
| Apr 10 | 187 | 171 |
| Apr 11 | 193 | 251 |
| Apr 12 | 278 | 125 |
| Apr 13 | 228 | 152 |
| Apr 14 | 120 | 100 |
| Apr 15 | 331 | 270 |
| Apr 16 | 319 | 125 |
| Apr 17 | 317 | 259 |
| Apr 18 | 271 | 122 |
| Apr 19 | 226 | 121 |
| Apr 20 | 187 | 86 |
| Apr 21 | 146 | 130 |
| Apr 22 | 206 | 248 |
| Apr 23 | 200 | 256 |
| Apr 24 | 248 | 253 |
| Apr 25 | 378 | 284 |
| Apr 26 | 216 | 136 |
| Apr 27 | 190 | 153 |
| Apr 28 | 199 | 94 |
The example is live: until you answer it, it shows as a band at the bottom of this window, as it would on your site. Its answer is stored under its own key, luz-consent-demo, so it does not count as an answer for this site; the button above shows it again. This site lists that key on Privacy and storage.
Without JavaScript the banner and its button stay hidden.
Overview
The most important number gets the room of two columns, when the view has it.
Open the sales reportThree products are below their minimum.
Review stockTwelve orders wait for a label.
See ordersThe dashboard at the width of this column. In a page it is the <main>, and the title is the page's <h1>: here this page already has its own, so the title is a paragraph and the panels' titles are <h3>.
The grid answers to the room of the dashboard, not to the window's.
Overview (sample data)
1,204
12 % more than last month
| Month | Orders |
|---|---|
| Jan | 820 |
| Feb | 905 |
| Mar | 870 |
| Apr | 1,010 |
| May | 1,075 |
| Jun | 1,204 |
The wide panel is the line chart: it takes two columns when the view has the room.
A Do and a Don't.
The word, the mark before it and the frame (solid or dashed) say which is which, so the colour is never the only cue. The two stack on a narrow screen.
A picture of the head: it holds a title of its own, so it is hidden from assistive technology on this page. Its text is in the markup below.
The chips wrap on a narrow screen.
Play video:
Without JavaScript the poster is a link to the video.
This is the one page of this site that may load another site, and only after you press the button and agree: the security policy of the site lets this page, and no other, frame www.youtube-nocookie.com.
<button class="luz-btn luz-btn--primary" type="button">Save changes</button>
The preview, and the markup that makes it under it. The width of the frame follows --luz-example-w.
What to know before you pick up your bike at Marea Bikes in Cádiz.
Book on the Marea Bikes website or at our shop. Choose the day, the size and the type of bike, and pay online or when you pick it up. We send a confirmation by e-mail with your pickup time.
Bring three things to the shop:
Yes, free of charge until 24 hours before pickup. After that, Marea Bikes keeps the price of the first day.
Yes, anywhere in the province of Cádiz. For a trip outside it, ask us before you book: the insurance of a Marea Bikes bike covers the province only.
Call the number on the tag of your key. In the city centre we fix or swap the bike within two hours, at no cost, unless the damage is not from normal use.
Not here? Write to the shop
On a site the list takes the width of its container, and each answer stops at a comfortable line length.
| Mon | Tue | Wed | Thu | Fri | |
|---|---|---|---|---|---|
| Morning | 120 | 240 | 360 | 480 | 600 |
| Afternoon | 0 | 60 | 300 | 180 | 540 |
| Evening | 30 | 90 | 150 | 270 | 420 |
12 and 13 May 2027 · Seville
Acme Summit 2027
Two days of talks and workshops for retail teams.
This column is narrower than 51rem, so the picture goes under the text; on a page of full width it sits beside it. The frame is empty, so it shows its flat surface, and the title is a paragraph: this page already has its <h1>.
New in version 2
Stock counts that keep up with the shop
Acme Stock updates every shelf as it sells, so you reorder before it runs out.
The same hero with luz-hero--centered: one column at any width, the text centred. A picture that bleeds cannot be shown in this column, as it runs to the edges of the window: see Variants and options.
The links go to this site’s home page in each language. The list opens under the header, at the end of the row, as it does on the home page; this documentation is in English only.
At 768 px of window width and below the sections collapse into a menu, the catalog's navbar.
The masthead is not sticky: three rows stuck to the top would take a third of a phone's screen. For one row that stays at the top, use the site header with --publication.
About the Review
An independent paper for Vejer and the coast, paid for by its readers.
Install
Add the tool to your project with your package manager.
Configure
The tool reads one file from the root of the project.
The config file
Name it acme.config.json.
The rail beside the text it lists, shown at any width: in a page it is a third column from 80rem and sits above the content below it. With the catalog's script, the section in view is marked as you scroll.
Open the announcement page or the search page, then come back with the Back button.
Nothing happens if your reader asked for reduced motion, in Firefox, or on the first load of a page.
Two blocks side by side, Next at the end; stacked at 640px and below. The whole block is the link.
Coast
What ten years of tide records say about the bay
Volunteers have read the gauge in the port twice a day since 2016.
Culture
The last tuna nets of the strait
Three families still set the almadraba each spring.
Food
Where the best tortillitas are fried
A short route through four kitchens.
The base: as many columns as fit, none under 16rem.
Coast
What ten years of tide records say about the bay
Volunteers have read the gauge in the port twice a day since 2016.
Culture
Food
luz-post-list--lead: the first post spans two columns with its picture beside it, when the list is 40rem wide or more. Narrower, it is a card like the rest.
luz-post-list--compact: one row for each post, the title and the facts, with no picture.
3 results for "tide"
Search results: the search box, then a heading that says how many, then the compact list. The posts, the word searched and the counts are placeholders: your search fills them in.
1,260 of 2,000 visitors
1,830 visitors
A second button for the search in this site’s header: it opens the same dialog. Without JavaScript it is a link to the A–Z index.
Sections are 64 px (--luz-space-3xl) apart in the main; their blocks, 16 px (--luz-space-md).
Why teams switch
One count for every shelf
Acme Stock reads each sale as it happens, in every shop.
The head of a section: an eyebrow, a title and a lead. The title is a paragraph here, as this page already has its headings.
Upcoming talks
See all talksA title with a link to the whole list, at the end of the head. The title and the link are placeholders: write your own.
At 768 px of window width and below the links collapse into a menu, the catalog's navbar. Its panel opens under this example where the browser supports CSS anchor positioning, and under the top of the window, where a header sits, elsewhere.
The second header has .luz-site-header--publication: the name in the heading font, larger. The examples are not sticky: in a page, .o-sticky keeps the header at the top.
LUZ ships no brand logos: the name says which place. The icons are those of the Icons page, or yours.
4,000+
teams on the platform
99.95%
uptime in the last year
12 min
to set up, on average
The first two stand on the page; the third sits in a luz-card. In a column narrower than 768 px the three stack.
npm install @luz-ds/core
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@luz-ds/core@0.3.0/dist/luz.min.css">
Open the CDN tab, then press Copy: the button copies the command of the open tab. Only the npm command shows a $ prompt.
The slider alone
show="time place saved moments"
Without JavaScript there is no bar and the moments are not shown. Core's radios still change the mode.
Horizontal, the default. Pick a date, or use Previous and Next, the arrow keys on the dates, or swipe.
First beta
Ten teams try the app.
Public launch
Sign-up opens to everyone.
Offline mode
Notes sync when the connection returns.
Version 2
A new editor and shared folders.
With autoplay: it moves on by itself while it is on screen. Pause stops it; it also waits while you point at it or use the keyboard in it.
The idea
Two people sketch the first screens.
The team
Five people work on it full time.
The first customers
Fifty schools use it every week.
With layout="vertical": scroll, and the item in the middle of the screen becomes active.
Version 0.1
Buttons, forms and tables.
Dark mode and a print stylesheet.
Right-to-left layouts.
The API is stable.
Since March 2016, a group of volunteers has read the tide gauge in the port at six in the morning and six in the evening, every day of the year.
Each reading is the height of the water against the gauge, to the nearest centimetre, with the time, the wind and the state of the sea. Two people read it, and a third checks the sheet the same week.
The average height at high tide was 4 cm higher in 2025 than in 2016. That is close to the rise that the national tide gauges record along the whole coast.
In the first five winters, waves reached the sea wall of the old town twice. In the last five, they reached it eleven times.
The council will publish the full records as open data in January and has asked the volunteers to add a third daily reading.
Beside the text, at the end, when the article is 46rem wide or more; above it below that. With the catalog's script, the section in view is marked as you scroll.