Restoration notes
Bringing back the colour of a 35mm print
The negative had faded to magenta. We rebuilt each reel's colour from the one frame that kept its blues.
walnut.css is a CSS framework under 18 KB gzipped, with components, charts and dark mode built in. Set a hue and chroma for the ground and three colours, and walnut derives the rest.
Plate 14
Walnut sideboard
Tambour doors, brass pulls and tapered legs.
Shop overview
Example dataRestoration notes
Bringing back the colour of a 35mm print
The negative had faded to magenta. We rebuilt each reel's colour from the one frame that kept its blues.
Tonight, Screen 2, 21:15
The Long Field
A restored 35mm print, followed by a Q&A with the restorer.
Everything is CSS. Optional scripts add tabs, dragging and scroll effects.
Set a hue and chroma for the ground and three colours. walnut derives the rest.
Each token defines both modes. Any section can switch to the other.
Pass in any colour and walnut picks readable text for it.
Buttons, fields, cards, tabs, dialogs, popovers, menus, select and carousel.
A column chart written as an HTML list, and a ring chart set by two numbers. No charting library.
Tint any photo with a palette colour using CSS.
Accordions, side panels and full-page views built on details and dialog.
Printed pages show link addresses and sheet numbers. Nothing to add.
You set a hue and chroma for the ground, accent, gold and olive. Bloom and cool take their hue from the accent, and their chroma can be set too.
Neutrals are the ground hue at nine lightness steps. Paste this block after walnut.css to use the palette. The number under each colour is its lowest text contrast in light and dark, and it updates as you edit.
Five palettes ship with walnut. Add the theme file and put its class on the page or on a section. Prairie is this site's own. Open a palette below to see its numbers and contrast, then use it on this page.
<link rel="stylesheet" href="…/themes/forest.css"> <html class="wal-forest">
| Colour | Hue | Chroma | Contrast |
|---|
Choose a photo or use your own. Its colours become walnut's numbers, ready to paste, with lightness adjusted until the text pairs pass AA where they can. The photo is read in this tab and never uploaded.
Or drop one anywhere on this tab.
Stock walnut components using the numbers read from the photo, as they would look on your site.
Choose a photo to read its colours.
| Colour | Hue | Chroma | Light | Dark | As text, light / dark | Text on it, light / dark |
|---|
Pick a vibe and walnut sets a palette, fonts, corners, a shadow and a texture that suit it. Use it on this page, edit its colours, then copy the CSS or download it as a file to load after walnut.css. Every vibe passes AA in light and dark before it is offered.
Vibes are applied with JavaScript. Each is the same tokens shown in Your palette and Finishes.
walnut-vibe.css, or copy the CSS below into a file of your own.Plate 14
Tambour doors, brass pulls.
A finish sets corner radius, border weight, shadow depth and label font. It works with any palette, in either mode. Catalogue and Soft ship with walnut. Cabinet and Poster are this site's own, written with the same tokens.
<html data-finish="catalogue">
/* corners: round, squircle or bevel */
:root { --wal-corner-shape: squircle; }
Squircle and bevel corners show in Chrome and Edge. Other browsers keep round corners.
Same markup as its neighbour.
Same markup as its neighbour.
--wal-bg: light-dark( oklch(93% …), /* light */ oklch(16% …) /* dark */ ); /* any section can take the other mode */ <section class="wal-dark">…</section>
Both cards use the same markup and tokens. One class switches a section's mode, and text on buttons and badges switches with it. The mode button in the header follows your system until you choose.
Each component is one class on standard HTML. None of them depends on which palette, finish or mode is in use.
.wal-btn.wal-badge .wal-chip.wal-card .wal-metricA card chooses light or dark text to suit its own surface, so it stays readable on any background.
.wal-field .wal-input<button class="wal-btn wal-btn-primary">Primary</button>
<span class="wal-badge wal-badge-olive">In stock</span>
<div class="wal-card">
<div class="wal-metric">
<span class="wal-metric-value">640</span>
<span class="wal-metric-label">Orders shipped</span>
</div>
</div>
<div class="wal-field">
<label class="wal-label" for="name">Name</label>
<input class="wal-input" id="name">
<span class="wal-hint">Shown on your public profile.</span>
</div>.wal-dialogA native dialog. Focus, Escape and the backdrop are the browser's.
popoverUses the popover attribute. No script.
.wal-tipShows on hover and focus, and Escape hides it. Chrome and Edge only, so keep what the reader needs in the button itself.
.wal-menuA popover placed next to its button, on whichever side has room.
<button commandfor="reserve" command="show-modal">Open</button> <dialog id="reserve" class="wal-dialog" closedby="any" aria-labelledby="reserve-h"> <h2 id="reserve-h">Reserve Plate 14?</h2>… </dialog> <button popovertarget="more">More</button> <div id="more" popover class="wal-menu">…</div> <button interestfor="save-tip">Save</button> <span id="save-tip" popover="hint" class="wal-tip">Saves a draft</span>
walnut picks dark or light text from the colour's lightness, so labels stay readable in any colour and in both modes.
<span class="wal-chip wal-tint" style="--wal-tint: #f5d90a">Design</span>
<span class="wal-badge wal-tint wal-fill-tint" style="--wal-tint: #f5d90a">Design</span>
/* in your own CSS, with a fallback first */
.price { color: #0a7f6f; color: --wal-ink(#0a7f6f); }Charts are plain HTML styled by walnut. Photos are tinted with CSS. Both update when the palette or mode changes.
Example data, in hundreds, May to October
<ol class="wal-chart" style="--max: 80">
<li><b>May</b>
<span style="--v: 42">42</span>
<span style="--v: 18">18</span>
</li>
…
</ol>
<ul class="wal-key"><li>Search</li><li>Links</li></ul>Share of visits by device
A new value moves the columns and turns the ring. Try the year switch.
<div class="wal-ring" style="--a: 58; --b: 34"> 58%<small>on a phone</small> </div>
Tints a photo with one palette colour using CSS blend modes. In forced colours mode the photo shows in greyscale.
Ink for every print<figure class="wal-duotone" data-ink="cool"> <img src="photo.jpg" alt="…"> </figure>
Photos from Unsplash, via picsum.photos.
Accordions and panels are built on details and dialog, so keyboard and screen reader support comes from the browser. One attribute sets how they move.
Each sideboard is made to order in six to eight weeks, then delivered by two people who carry it inside.
Yes. Each tambour door is lacquered to order, in any of the six colourways.
Wipe it with a dry cloth. Oil it again once a year, or when the wood looks dry.
.wal-chestRows share a name, so opening one closes the others.
<div class="wal-chest" data-wal-material="walnut">
<details name="faq">
<summary>How long does delivery take?</summary>
<div>…</div>
</details>
</div>Close it by dragging the handle, pressing Escape or clicking outside.
.wal-doorSlides in from the edge, or up from the bottom on phones. With walnut-motion.js you can drag it closed by its handle.
<button commandfor="basket" command="show-modal">Basket</button>
<dialog id="basket" class="wal-door" closedby="any" aria-labelledby="basket-h">
<button class="wal-door-pull" commandfor="basket"
command="close" aria-label="Close"></button>
<div class="wal-door-body"><h2 id="basket-h">Your basket</h2>…</div>
</dialog>Close it with the button, Escape or clicking outside.
data-wal-open="gatefold"Opens from the card into a two-page spread, a picture and its details. On phones the pages stack. The notes fold out from behind the cover.
<button commandfor="forest" command="show-modal">
<span data-wal-art>…</span> Forest
</button>
<dialog id="forest" class="wal-window" data-wal-open="gatefold" closedby="any" aria-labelledby="forest-h">
<div class="wal-gatefold-spread">
<figure class="wal-gatefold-cover">…</figure>
<div class="wal-gatefold-notes"><h2 id="forest-h">Forest</h2>…</div>
</div>
</dialog>Your browser's Back button closes it.
Opens from the card into a full-screen page in its own palette. Each section inside can set a palette with data-act, and the page changes palette as you scroll.
<dialog class="wal-window wal-palette wal-cafe" data-wal-acts aria-label="Plate 14">
<section data-act="wal-cafe">…</section>
<section data-act="wal-dusk">…</section>
<section data-act="wal-forest">…</section>
</dialog>
walnut.recolour(page, () => room.showModal(), { from: card });Starts slowly and ends with a small bounce.
Opens fast and bounces three times.
Opens slowly and stops with no bounce.
<html data-motion="materials">
/* or one part, and everything inside it */
<div class="wal-chest" data-wal-material="stone">…</div>
Set it on the page, and drawers, doors and windows use the walnut timing while menus, pickers and charts use the brass timing. Or set one material on one part. With reduced motion turned on, everything opens at once.
.wal-shuffleFilter or sort a grid and each card moves to its new place. Cards that leave fade out, and cards that arrive fade in.
<ul class="wal-shuffle" id="materials">…</ul>
walnut.shuffle(materials, () => showOnly("Wood"));Four states that used to need a script are now handled by the browser. The old classes still work as a fallback.
.is-scrolled is now a scroll-driven animation, as on this page's header. Chrome, Edge and Safari 26.
.is-active is now scroll-target-group in walnut-scroll.css. This page sets the class itself in other browsers.
.is-dismissed is now interestfor with popover="hint".
.is-open is now a dialog opened with commandfor. This page opens it from a script in browsers without commandfor.
Green, with a copper bloom. The theme file is in the walnut repository (https://github.com/SKMinWoo/walnut.css).
On paper, walnut drops the nav and side panels, prints in light mode, writes the address after each outside link and numbers each sheet. Every tab and accordion prints open, and charts and duotones keep their colour.
walnut styles HTML. It doesn't provide app logic or complex widgets.
Requires Chrome or Edge 123, Safari 17.5 or Firefox 120. Fallbacks for newer features are listed in the reference.
One stylesheet, under 18 KB gzipped. No build step.
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/SKMinWoo/walnut.css@v0.4.0/dist/walnut.min.css">
# copy dist/ from the release into your project
<link rel="stylesheet" href="/dist/walnut.min.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/SKMinWoo/walnut.css@v0.4.0/dist/walnut.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/SKMinWoo/walnut.css@v0.4.0/dist/themes/forest.css"> <html class="wal-forest" data-finish="catalogue">
Put a palette class on the html element, or paste your eight numbers after walnut.css. This is the palette on this page right now.
Your own CSS overrides walnut without !important, as long as it isn't in a layer.
<div class="wal-card"> <h3>Walnut sideboard</h3> <span class="wal-badge wal-badge-olive">In stock</span> <button class="wal-btn wal-btn-primary">Reserve a sample</button> </div>
| Add | For |
|---|---|
themes/forest.css | A shipped palette: Forest, Bone, Café, Dusk or Press. One class on a page or a section. |
walnut-tabs.js | Tabs from plain sections with a data-tab label, with arrow keys and a link to each tab. |
walnut-recolour.js | Palette buttons that recolour the page, opening out from the button pressed. |
walnut-motion.js | Side panels you drag closed by the handle, windows that open as a gatefold, and lists that shuffle. |
walnut-acts.js | A page, or a full-page view, that changes palette section by section as you scroll. |
walnut-scroll.css | Carousel dots and a nav that highlights the section in view, with no script. Chrome and Edge. |
| Token | Controls |
|---|---|
--wal-hue, --wal-chroma | The ground: every neutral |
--wal-<cue>-hue, -chroma | Each of the five colours |
--wal-<cue>-ink | The colour as text, not as a fill |
--wal-<cue>-soft, -fg | A pale tint of the colour, and text on the colour |
--wal-cue-1 to --wal-cue-5 | The five colours by position, for looping over them |
--wal-l-*-light, -dark | The lightness of each surface, in each mode |
--wal-cx-* | Each surface's chroma, as a fraction of the ground's |
--wal-elevation, --wal-wash | Shadow depth, and the tinted light on the page |
--wal-line-boost | How strong hairlines are |
--wal-radius and its variants | Corner radius, per kind of component |
--wal-corner-shape | Round, squircle or bevel corners |
--wal-open-duration, --wal-open-ease | How openings move, with matching close tokens |
A colour that works as a fill is often too light to read as text. Each colour comes in four forms.
| Token | Use | Example |
|---|---|---|
--wal-gold | The colour as a fill | |
--wal-gold-ink | The colour as text | gold text |
--wal-gold-soft | A pale tint, for behind text | soft |
--wal-gold-fg | Text on the fill | on gold |
Each hue has a limit on how vivid a screen can show it, called the gamut, and past it the browser shows a duller, shifted colour without warning. At 62% lightness a chroma of 0.17 fits reds and violets but not yellows or blues, so when you move a hue far from its default, lower its chroma too. Random palette and the photo tool do this for you. The sliders don't.
walnut puts everything in cascade layers, and CSS outside a layer beats every layer whatever its specificity, so your own rules override walnut without !important. A rule replaces only the properties it sets, and walnut's reduced-motion rules use !important so they still win.
@layer reset, tokens, base, layout, components, finish, utilities, cinema;
/* not in a layer: beats all of the above */
.wal-card { border-radius: 3px; }
wal-text-* sets a font size. wal-color-* sets a colour. There is no .wal-text-muted class: that is a token name, and used as a class it does nothing.
<p class="wal-text-sm wal-color-muted">small and muted</p>
commandfor and closedby open and close them with no script, and focus, Escape and find-in-page come from the browser.data-wal-open picks how a window opens. data-motion on the page, or data-wal-material on a part, picks how things move.walnut.open.styles.mine = { open(dialog, from) {…}, close(dialog, from) {…} }, then data-wal-open="mine".walnut.open(dialog, button) and walnut.open.close(dialog).light-dark() sets the floor: Chrome and Edge 123, Safari 17.5, Firefox 120. Newer features are added where the browser has them.
| Feature | Needs | Without it |
|---|---|---|
Text on each colour (-fg tokens) | Relative colour syntax | Firefox 120 to 127 gets a fixed value |
| Palette changes that animate | @property, Firefox 128 | The palette changes at once |
| Dialog and popover entrances | @starting-style: Chrome 117, Safari 17.5, Firefox 129 | They appear at once |
| Page-to-page transitions | Cross-document view transitions: Chrome 126, Safari 18.2 | A link just navigates |
| Recolour from the button | View transition types: Chrome 125, Safari 18.2 | The colours fade across instead |
| Scroll reveals, sticky header shadow | Scroll-driven animations: Chrome 115, Safari 26 | The content is simply there |
| Menus that open by their button | Anchor positioning from a popover's button: Chrome 133, Safari 26 | The menu opens in the middle of the screen |
Chrome and Edge have the newest parts today. Each falls back to something that still works:
::details-content: accordion rows open at once.commandfor: open doors and windows with your own showModal().interestfor: tips stay hidden, so keep what matters in the button.appearance: base-select: the system's own select.::scroll-marker: a carousel is a row you swipe.corner-shape: corners are round.@function: --wal-ink() uses the fallback declaration you put before it.:target-current: the nav needs its .is-active class.Native popover
No JavaScript. Escape or clicking outside closes it.