Utilities
Single-purpose helpers: classes that do one thing, and small functions for scripts.
css/utilities/ · js/
Classes
Named in Tailwind's shape, name or name-{value}, and they take
variants like any utility.
Tailwind's utilities
All of Tailwind's utilities are there too, built from struts' tokens:
p-16 is 1rem, text-h2 is the Heading 2 size,
bg-lilac is the palette's lilac. Tailwind's default colours and
containers are cleared, so only the project's exist.
Stack
stack-8
One fixed gap between every child,
whatever the element
Markup
<div class="stack-8">
<p class="demo-box">stack-8</p>
<p class="demo-box">One fixed gap between every child,</p>
<p class="demo-box">whatever the element</p>
</div>
Stack
One explicit gap between every child, in spacing units: stack-16 = 1rem.
Children's own block margins are cleared so the gap is exact.
The gap is written straight onto the children, not passed through an
inherited variable, so a nested stack can't pick up its parent's gap.
For per-element spacing, use flow.
Rules
rule-block-start
rule-block-end
rule-inline-start
rule-inline-end
Markup
<div class="grid-simple cols-2 md:cols-4">
<p class="rule-block-start p-12 type-label-sm">rule-block-start</p>
<p class="rule-block-end p-12 type-label-sm">rule-block-end</p>
<p class="rule-inline-start p-12 type-label-sm">rule-inline-start</p>
<p class="rule-inline-end p-12 type-label-sm">rule-inline-end</p>
</div>
Rules
Kite Co.'s keylines: a hairline on one edge, in the context's foreground at
half strength, so it follows every surface.
rule-block-start | rule-block-end | rule-inline-start | rule-inline-end
Reads the global knobs --rule-width (declared) and --rule-color (not
declared; set it on a context to change every rule inside). For the
keyline down a layout-grid's wide edge, see layout-grid--ruled.
Lists
- list-reset
- no markers
- list-styled
- markers back
Markup
<div class="grid-simple cols-1 md:cols-2">
<ul class="list-reset">
<li>list-reset</li>
<li>no markers</li>
</ul>
<ul class="list-styled">
<li>list-styled</li>
<li>markers back</li>
</ul>
</div>
Lists
list-reset: no markers, no indent.
list-styled: markers back, with spacing between items. prose applies it.
list-styled knobs: --list-styled--padding-inline-start, --list-styled--gap,
--list-styled__marker--color (read with a fallback to currentColor).
Image fit
Markup
<div class="img-fit aspect-[3/1] rounded-md max-w-(--container-2xs)">
<svg viewBox="0 0 300 100" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
<rect width="300" height="100" fill="var(--color-lilac)" />
<circle cx="150" cy="50" r="40" fill="var(--color-blue)" />
</svg>
</div>
A box whose child image or video covers it. Give the box a size or aspect ratio.
Hit area
The scheme switch in the header uses it: 32px to look at, 44px to tap.
Hit area
Grows the clickable area of a small control to at least 44px, centred,
without changing how big it looks. An invisible ::before does it, so the
element needs its ::before free.
<button class="btn btn--square hit-area [--btn--size:--spacing(32)]">…</button>
Keep 44px clear around the control's centre, or the areas overlap.
Knob, read with a fallback: --hit-area--size (44px).
Expanded state
Show or hide parts of a trigger by its aria-expanded, as the
disclosure does.
Show or hide parts of a disclosure trigger based on its aria-expanded state:
<button aria-controls="menu" aria-expanded="false">
<span data-show-collapsed>Open menu</span>
<span data-show-expanded>Close menu</span>
</button>
data-show-expanded / data-hide-collapsed: visible only when expanded.
data-hide-expanded / data-show-collapsed: visible only when collapsed.
Scroll lock
The site header's menu and the dialog both use it.
Scroll lock
The page stops scrolling while a modal dialog, or a popover with
data-scroll-lock, is open, with no JS. The gutter stays reserved so the
content doesn't shift sideways.
<nav popover data-scroll-lock>…</nav>
For anything else that should lock scrolling, add scroll-lock to <html>.
Resets
button-reset, margin-trim (and its -first and
-last) and focus-none.
Strip a button back to its content, for buttons that shouldn't look like buttons.
Remove the outer margins of a container's first and/or last child.
Remove focus styles. Only for elements that are never reached by keyboard,
or that draw their own focus indicator. Getting this wrong hides focus.
Scripts
TypeScript with no framework and no dependencies. Patterns and components import what they need; a project copies only those.
define()
How every pattern and component script sets itself up. Why: JS and dynamicElements.
dynamicElements: run setup code for every element matching a selector, now
and whenever one is added later. Like customElements.define(), but for any
markup.
import { define } from './dynamic-elements';
define('[data-disclosure]', (el) => new Disclosure(el));
- Elements already in the DOM are set up on DOMContentLoaded (or straight
away if that has passed).
- A MutationObserver sets up elements added later, checking each added node
and its descendants.
- Each element is set up once per selector, so one element can match several.
- `disconnected` runs when a set-up element leaves the DOM (not when it's
moved), and the element is set up again if it comes back.
debounce()
Delay calling `fn` until `wait` ms have passed without another call.
window.addEventListener('resize', debounce(measure, 150));
Focusable elements
focusableElements(), firstFocusable() and
lastFocusable().
Keyboard-focusable elements inside a container, in DOM order. Skips
disabled, inert and hidden elements (checkVisibility covers display: none,
content-visibility and ancestors).
Data attributes
getBooleanDataAttribute() and getNumberDataAttribute(), for
reading a hook's options.
Read a boolean data attribute. Present means true (whatever the value),
except an explicit "false". Missing means the default.
<div data-animate> → true
<div data-animate="false"> → false
<div> → defaultValue
@param name - The dataset key, e.g. "disclosureAnimate" for data-disclosure-animate.