Skip to content

Theme a whole site from eight numbers.

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.

Get started
  • Under 18 KB gzipped
  • No build step
  • Light and dark
  • AA contrast
  • MIT licence

Plate 14

Walnut sideboard

Tambour doors, brass pulls and tapered legs.

In stockModel 211
Width
180 cm
Depth
45 cm
Height
72 cm

Shop overview

Example data
7,140Visitors this monthUp 12%
312OrdersUp 4%
4.4%ConversionDown 0.3 points
  1. MaySearch 42Links 18Direct 12
  2. JunSearch 48Links 22Direct 14
  3. JulSearch 51Links 30Direct 13
  4. AugSearch 58Links 26Direct 17
  5. SepSearch 63Links 34Direct 19
  6. OctSearch 71Links 39Direct 22
  • Search
  • Links
  • Direct
  • Phone
  • Desktop
  • Tablet
A river valley between mountains

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.

FilmArchive

Tonight, Screen 2, 21:15

The Long Field

A restored 35mm print, followed by a Q&A with the restorer.

Runtime
112 min
Year
1961
Seats left
38
As it should appear on the ticket.

Theming

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.

  1. Accent Set by you
  2. Bloom Accent + 27°
  3. Gold Set by you
  4. Olive Set by you
  5. Cool Accent + 209°

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">
PrairieMade for this pageOn this page
ForestGreen, with a copper bloomOn this page
BoneBlue on a warm, light groundOn this page
CaféBlue on a warm amber groundOn this page
DuskViolet on a violet-grey groundOn this page
PressRed-orange on a warm, light groundOn this page

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.

The photo's palette

Stock walnut components using the numbers read from the photo, as they would look on your site.

AccentGoldOliveCool

Choose a photo to read its colours.

The photo's colours in hue order, each as wide as its share of the photo. A brass bar marks the ones used.
ColourHueChromaLightDarkAs text, light / darkText on it, light / dark

                  
                

How a photo becomes a palette

  1. The photo is read at 72 pixels across, each pixel as an oklch colour.
  2. Pixels are grouped by hue, 10° to a group, leaving out greys, very light or dark pixels, and groups under 0.4% of the photo. The ground's faint tint is the average of every pixel.
  3. The strongest group becomes the accent. Up to four more are taken in order of strength, each at least 28° from any already taken, and the rest are filled in around the accent. The nearest to the accent becomes bloom, the furthest cool, and of the other two the one nearer yellow is gold.
  4. Each colour is kept inside sRGB and every text pair is measured. A colour that fails is made less vivid, or its lightness moves the least amount that passes.

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.

Checking what can answer…
Finish
Corners

Plate 14

Walnut sideboard

Tambour doors, brass pulls.

In stockModel 211
For the order confirmation.
38Slats per door

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.

Light

Tonight

Same markup as its neighbour.

38 seats
Dark

Tonight

Same markup as its neighbour.

38 seats
--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.

Components

Each component is one class on standard HTML. None of them depends on which palette, finish or mode is in use.

Buttons .wal-btn

Badges and chips .wal-badge .wal-chip

AccentBloomGoldOliveCoolChip

Card and metric .wal-card .wal-metric

640Orders shipped this week

A card chooses light or dark text to suit its own surface, so it stays readable on any background.

Fields .wal-field .wal-input

Shown on your public profile.
Markup
<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>

Dialog .wal-dialog

A native dialog. Focus, Escape and the backdrop are the browser's.

Popover popover

Uses the popover attribute. No script.

Tip .wal-tip

Shows on hover and focus, and Escape hides it. Chrome and Edge only, so keep what the reader needs in the button itself.

Saves a draft

Menu .wal-menu

A popover placed next to its button, on whichever side has room.

Markup
<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>

Nav, select and carousel are native elements styled by walnut, so keyboard and screen reader support comes from the browser. Tabs follow the ARIA tabs pattern, built by walnut-tabs.js.

Ships in
CSS onlywalnut-scroll.css adds the carousel's dots and arrows.
Tabs
Optional scriptwalnut-tabs.js turns sections into tabs, with arrow keys and a link for each tab. Without it the sections stack.
Browsers
Chrome and EdgeSwatches in the select, and the carousel's dots. Other browsers show the system select and a carousel you swipe.
Markup
<nav class="wal-nav">…</nav>
<select class="wal-select">
  <button><selectedcontent></selectedcontent></button>
  <option><i class="swatch"></i> Café</option>
</select>
<ul class="wal-carousel" aria-label="Colourways"><li>…</li></ul>

<div class="wal-tabs" data-label="Install">
  <section data-tab="CDN" id="install-cdn">…</section>
  <section data-tab="Download" id="install-download">…</section>
</div>

/* any element can have its own palette */
<div class="wal-palette wal-cafe">…</div>
Design Research Ops Writing Support
Design Research Ops Writing Support
Your labelYour label

walnut picks dark or light text from the colour's lightness, so labels stay readable in any colour and in both modes.

Use it for
Tags, categories, colours your users pick.
Ships in
CSS onlyCSS functions in Chrome and Edge
Markup
<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 and photos

Charts are plain HTML styled by walnut. Photos are tinted with CSS. Both update when the palette or mode changes.

Where visitors came from

Example data, in hundreds, May to October

  1. MaySearch 42Links 18Direct 12
  2. JunSearch 48Links 22Direct 14
  3. JulSearch 51Links 30Direct 13
  4. AugSearch 58Links 26Direct 17
  5. SepSearch 63Links 34Direct 19
  6. OctSearch 71Links 39Direct 22
  • Search
  • Links
  • Direct
Markup
<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>

How people visit

Share of visits by device

  • Phone
  • Desktop
  • Tablet

A new value moves the columns and turns the ring. Try the year switch.

Markup
<div class="wal-ring" style="--a: 58; --b: 34">
  58%<small>on a phone</small>
</div>
A field at sunset with a line of trees
Accent
Hills under morning mist
Cool
A walker in a misty canyon
Olive

Duotone photos

Tints a photo with one palette colour using CSS blend modes. In forced colours mode the photo shows in greyscale.

Ink for every print
Ships in
CSS onlyAdd wal-reveal-develop to fade a print in as it scrolls into view.
Markup
<figure class="wal-duotone" data-ink="cool">
  <img src="photo.jpg" alt="…">
</figure>

Photos from Unsplash, via picsum.photos.

Motion

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.

How long does delivery take?

Each sideboard is made to order in six to eight weeks, then delivered by two people who carry it inside.

Can I choose the door colours?

Yes. Each tambour door is lacquered to order, in any of the six colourways.

How do I look after oiled walnut?

Wipe it with a dry cloth. Oil it again once a year, or when the wood looks dry.

Material

Accordion .wal-chest

Rows share a name, so opening one closes the others.

Use it for
FAQs, project lists, pricing.
Ships in
CSS onlyNative details, so find-in-page opens a closed row.
Browsers
Chrome and EdgeRows slide open. Other browsers open them at once.
Markup
<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.

Side panel .wal-door

Slides in from the edge, or up from the bottom on phones. With walnut-motion.js you can drag it closed by its handle.

Use it for
Menus, baskets, filters, settings.
Ships in
CSS onlywalnut-motion.js adds the drag
Markup
<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.

Gatefold 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.

Use it for
Product details, case studies.
Ships in
walnut-motion.jsWithout it the window opens with a short fade.
Shadow
A CSS drop shadow under the spread. With walnut-motion.js it grows as the spread lifts off the card.
Markup
<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.

Full-page view

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.

Use it for
Product pages, collections, case studies.
Ships in
walnut-recolour.jswalnut-acts.js for the acts
Markup
<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 });

Walnut Opens in 1050 ms, closes in 800 ms

Walnut drawer

Starts slowly and ends with a small bounce.

Brass Opens in 480 ms, closes in 380 ms

Brass drawer

Opens fast and bounces three times.

Stone Opens in 1200 ms, closes in 1000 ms

Stone drawer

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.

Materials12 of 12
Show
Sort
  • WalnutWood, lightness 42
  • TeakWood, lightness 56
  • White oakWood, lightness 79
  • TravertineStone, lightness 87
  • SlateStone, lightness 43
  • Verde marbleStone, lightness 47
  • BrassMetal, lightness 79
  • CopperMetal, lightness 62
  • Blackened steelMetal, lightness 30
  • Mustard woolCloth, lightness 75
  • Teal boucléCloth, lightness 55
  • Cognac leatherCloth, lightness 50

Shuffle .wal-shuffle

Filter or sort a grid and each card moves to its new place. Cards that leave fade out, and cards that arrive fade in.

Use it for
Catalogues, portfolios, shops, any grid you filter.
Ships in
walnut-motion.jsWithout view transitions the grid changes at once.
Markup
<ul class="wal-shuffle" id="materials">…</ul>

walnut.shuffle(materials, () => showOnly("Wood"));

Browser features

Four states that used to need a script are now handled by the browser. The old classes still work as a fallback.

  • Sticky header shadow

    CSS only

    .is-scrolled is now a scroll-driven animation, as on this page's header. Chrome, Edge and Safari 26.

  • Active nav link

    Chrome and Edge

    .is-active is now scroll-target-group in walnut-scroll.css. This page sets the class itself in other browsers.

  • Dismissible tip

    Chrome and Edge

    .is-dismissed is now interestfor with popover="hint".

  • Open menu

    Optional script

    .is-open is now a dialog opened with commandfor. This page opens it from a script in browsers without commandfor.

Print styles

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.

When to use walnut

walnut styles HTML. It doesn't provide app logic or complex widgets.

A good fit for

  • Portfolios, docs and content sites
  • Product and marketing pages that need dark mode and brand colours
  • Prototypes and small apps without a build step

Look elsewhere if

  • You need data grids, date pickers or a large component library
  • Your team uses utility-class CSS
  • You need identical behaviour in older Safari and Firefox

Requires Chrome or Edge 123, Safari 17.5 or Firefox 120. Fallbacks for newer features are listed in the reference.

Get started

One stylesheet, under 18 KB gzipped. No build step.

  1. Add the stylesheet

    <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">
  2. Set your colours

    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.

  3. Add classes

    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>

Optional files

AddFor
themes/forest.cssA shipped palette: Forest, Bone, Café, Dusk or Press. One class on a page or a section.
walnut-tabs.jsTabs from plain sections with a data-tab label, with arrow keys and a link to each tab.
walnut-recolour.jsPalette buttons that recolour the page, opening out from the button pressed.
walnut-motion.jsSide panels you drag closed by the handle, windows that open as a gatefold, and lists that shuffle.
walnut-acts.jsA page, or a full-page view, that changes palette section by section as you scroll.
walnut-scroll.cssCarousel dots and a nav that highlights the section in view, with no script. Chrome and Edge.
ReferenceTokens, colour forms, cascade layers, naming, scripting and browser support

Tokens you can set

TokenControls
--wal-hue, --wal-chromaThe ground: every neutral
--wal-<cue>-hue, -chromaEach of the five colours
--wal-<cue>-inkThe colour as text, not as a fill
--wal-<cue>-soft, -fgA pale tint of the colour, and text on the colour
--wal-cue-1 to --wal-cue-5The five colours by position, for looping over them
--wal-l-*-light, -darkThe lightness of each surface, in each mode
--wal-cx-*Each surface's chroma, as a fraction of the ground's
--wal-elevation, --wal-washShadow depth, and the tinted light on the page
--wal-line-boostHow strong hairlines are
--wal-radius and its variantsCorner radius, per kind of component
--wal-corner-shapeRound, squircle or bevel corners
--wal-open-duration, --wal-open-easeHow openings move, with matching close tokens

Fills and text use different colours

A colour that works as a fill is often too light to read as text. Each colour comes in four forms.

TokenUseExample
--wal-goldThe colour as a fill
--wal-gold-inkThe colour as textgold text
--wal-gold-softA pale tint, for behind textsoft
--wal-gold-fgText on the fillon gold

Lower the chroma when you move a hue

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.

Your CSS wins

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; }

Naming

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>

Opening things from a script

  • Doors and windows are dialogs, so 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-motion.js is optional. Without it every window still opens.
  • Your own movement: walnut.open.styles.mine = { open(dialog, from) {…}, close(dialog, from) {…} }, then data-wal-open="mine".
  • From a script: walnut.open(dialog, button) and walnut.open.close(dialog).

Browser support

light-dark() sets the floor: Chrome and Edge 123, Safari 17.5, Firefox 120. Newer features are added where the browser has them.

FeatureNeedsWithout it
Text on each colour (-fg tokens)Relative colour syntaxFirefox 120 to 127 gets a fixed value
Palette changes that animate@property, Firefox 128The palette changes at once
Dialog and popover entrances@starting-style: Chrome 117, Safari 17.5, Firefox 129They appear at once
Page-to-page transitionsCross-document view transitions: Chrome 126, Safari 18.2A link just navigates
Recolour from the buttonView transition types: Chrome 125, Safari 18.2The colours fade across instead
Scroll reveals, sticky header shadowScroll-driven animations: Chrome 115, Safari 26The content is simply there
Menus that open by their buttonAnchor positioning from a popover's button: Chrome 133, Safari 26The 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.

Palette

Changes apply to the whole page, in light and dark.

Ground
Accent
Gold
Olive

Bloom follows the accent at +27° and cool at +209°, unless a palette sets them.

Shipped palettes

Make a palette from a photo

How colours are derived

You set three colours and walnut makes five. Drag the accent round the wheel: bloom and cool follow it.

  • Accent: the hue and chroma you set. Links and the main button.
  • Bloom: the accent plus 27°. Washes and highlights.
  • Gold: the hue and chroma you set. Rules and markers.
  • Olive: the hue and chroma you set. Tags and quiet states.
  • Cool: the accent plus 209°, nearly opposite it. The contrast colour.
  • Neutrals: the ground hue at nine lightness steps, from page to text.
  • Each colour keeps its lightness when its hue changes, so text contrast stays about the same.

Your basket

Example
  • Walnut sideboardPlate 14, sample swatch£12
  • Brass pullSet of five£40

Subtotal£52

An example basket. Nothing is sold here.

Colourway

Forest

Fern green with copper accents. The neutrals carry a slight green tint.

<html class="wal-forest">

Reserve Plate 14?

We'll hold it for 48 hours. You won't be charged.

Native popover

No JavaScript. Escape or clicking outside closes it.

Example page using walnut-acts.js

Plate 14, Café palette

Walnut sideboard

Tambour doors, brass pulls and tapered legs. Scroll down: each section sets its own palette.

Width
180 cm
Depth
45 cm
Height
72 cm
Model
211
data-act="wal-cafe"

Dusk palette

Tambour, in quarter-sawn walnut

Same markup as the section above. The palette changed when this section reached the middle of the screen.

38Slats per door

The brass pulls stay brass. The doors take the new palette.

data-act="wal-dusk"

Forest palette

Tapered, turned by hand

Add walnut-acts.js and a data-act attribute to each section. Scrolling back up reverses the changes.

Finish
Oil
Pulls
Brass
Plate
14
data-act="wal-forest"