Skip to content

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>
css/utilities/stack.css
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>
css/utilities/rule.css
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>
css/utilities/list.css
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>
css/utilities/img-fit.css
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.

css/utilities/hit-area.css
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.

css/utilities/aria-expanded.css
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.

css/utilities/scroll-lock.css
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.

css/utilities/button-reset.css
Strip a button back to its content, for buttons that shouldn't look like buttons.
css/utilities/margin-trim.css
Remove the outer margins of a container's first and/or last child.
css/utilities/focus-none.css
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.

js/dynamic-elements.ts
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()

js/debounce.ts
Delay calling `fn` until `wait` ms have passed without another call.

  window.addEventListener('resize', debounce(measure, 150));

Focusable elements

focusableElements(), firstFocusable() and lastFocusable().

js/focusable.ts
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.

js/data-attributes.ts
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.

Cookies

setCookie(), getCookie() and deleteCookie().

js/cookies.ts
Minimal first-party cookie helpers. Names and values are URI-encoded, so
any string is safe to store.