Skip to content

aesthetic — the misty step design system

Screen

.ae-screen / .ae-bar / .ae-stage

One viewport, never scrolling: a bar, a centered stage, a bar. Add .ae-wide to the screen for the 68rem catalog measure.

NAMEnav · mode
the stage — one view, centered
chromelinks
<div class="ae-screen">
  <header class="ae-bar">
    <a class="ae-name" href="/">NAME</a>
    <nav class="ae-nav">…</nav>
    <button class="ae-mode">…</button>
  </header>
  <main class="ae-stage">
    <div class="ae-view">…one screenful…</div>
  </main>
  <footer class="ae-bar">…</footer>
</div>

Stage scroll

.ae-stage-scroll

For genuinely long documents: the document scrolls inside the stage, the chrome stays put, the page itself never scrolls.

Legal pages, changelogs, long-form prose — the screen law holds, the inner column moves. This box scrolls; the bars around this page do not. The chrome register keeps utility text at 13px while content stays at the one size. Scroll on: nothing above or below this column will move, which is the entire point.

<main class="ae-stage ae-stage-scroll">
  <article>…the long document…</article>
</main>

Nav

.ae-nav + .ae-nav-ind + .ae-view

Quiet words with a sliding ink underline. Views remount on swap and replay their entrance.

<nav class="ae-nav">
  <button>work</button>
  <button>writing</button>
  <button>contact</button>
  <span class="ae-nav-ind"></span>
</nav>
// the site positions the indicator
// (left/width) on view change

Row link

.ae-row-link

In lists, the whole row is the link — the cursor never strobes across dead bands between rows.

<a class="ae-row-link" href="…">
  <span class="ae-item">A project that matters</span>
  <span class="ae-dim">— and its description</span>
</a>

Chrome

.ae-chrome / .ae-foot

The one sanctioned second size: 13px, muted ink, for footers, utility links, and metadata.

v2.8.1 · MIT githubnpm
<footer class="ae-foot">
  <span>v2.8.1 · MIT</span>
  <span class="ae-foot-links">
    <a href="…">github</a><a href="…">npm</a>
  </span>
</footer>

Icon

.ae-icon / .ae-mode

Lucide, inlined as SVG symbols: 1.5px stroke, round caps, sized to ride with text. The mode toggle rotate-crossfades sun and moon.

Icons ride with text , and the toggle is live:

<button class="ae-mode" aria-label="toggle color mode">
  <svg class="ae-icon ae-sun">…sun…</svg>
  <svg class="ae-icon ae-moon">…moon…</svg>
</button>
// toggle .dark/.light on the root;
// pin style.colorScheme to match

Panel

.ae-panel

Soft depth for genuinely dense content: light shadow in light mode, wash in dark, radius 0. Forms on a stage stay naked.

Dense content lives here

and earns its quiet lift off the surface.

<div class="ae-panel">
  …dense content…
</div>

Registers

Hierarchy is ink and weight, never scale. Faint ink is decoration only — placeholders and resting underlines.

THE NAME

The lede: the page's whole argument, plainly.

An item that matters

supporting matter, muted

The accent — yours to steer.

<p class="ae-name">THE NAME</p>
<p class="ae-lede">The argument, plainly.</p>
<p class="ae-item">An item that matters</p>
<p class="ae-dim">supporting matter</p>
<span class="ae-accent">accent</span>

Input

.ae-label / .ae-input

A line, not a box. Placeholders are decorative supplements to always-visible labels.

<label class="ae-label" for="name">Name</label>
<input class="ae-input" id="name" type="text">

<textarea class="ae-input"></textarea>

Button

.ae-button / .ae-send

Contained ink shapes with a small press — links navigate, buttons act. The send moment resolves exactly once; success persists and the control disables.

<button class="ae-button ae-send">
  <span class="ae-send-label">Send message</span>
  <span class="ae-send-done" aria-hidden="true">
    Sent <svg class="ae-icon">…check…</svg>
  </span>
</button>
// on success: add .is-sent, set disabled

STATES · the law holds in every one

default
the resting control.
disabled
add disabled — 0.7 opacity, default cursor. No shadow, no new shape.
resolved
.is-sent + disabled — the send moment, resolved once, success persists.

Hover (0.9 opacity), active (a 1px press), and focus (a 2px ink ring) are live on the controls — pseudo-states, never faked into a class.

Status

.ae-ok / .ae-warn / .ae-err

Status rides the glyph: success, warning, and danger color the icon — never the sentence, never a filled pill. The words stay ink.

Deploy succeeded.

Certificate expires soon.

Build failed.

<p>
  <svg class="ae-icon ae-ok">…check…</svg> Deploy succeeded.
</p>
<p>
  <svg class="ae-icon ae-warn">…triangle-alert…</svg> Certificate expires soon.
</p>
<p>
  <svg class="ae-icon ae-err">…x…</svg> Build failed.
</p>

Table

.ae-table / .ae-plate

Terminal density: 13px mono, lowercase headers on a wash band, hairline row rules — framed as a numbered plate. A column header shares its column's alignment: put .num on the th AND its tds. Add data-label to cells that should stack on phones.

TABLE 1 · EXAMPLE

endpoint status p99 ms
/send live 412
/verify live 88
/archive draining 1204

p99 over the trailing 24 hours.

<div class="ae-plate">
  <p class="ae-plate-cap">TABLE 1 · EXAMPLE</p>
  <table class="ae-table">
    <thead>
      <tr><th>endpoint</th><th>status</th>
          <th class="num">p99 ms</th></tr>
    </thead>
    <tbody>
      <tr><td class="ae-item" data-label="endpoint">/send</td>
          <td data-label="status">live</td>
          <td class="num" data-label="p99 ms">412</td></tr>
      …
    </tbody>
  </table>
  <p class="ae-plate-note">p99 over the trailing 24 hours.</p>
</div>

STATES · the law holds in every one

populated
endpoint ms
/send 412
/verify 88
13px mono, wash header band, hairline row rules — the instrument at rest.
loading
endpoint ms
   
   

loading endpoints…

skeleton holds the row shape — still wash, never a shimmer. Words to a status line.
empty

No endpoints.

Add one to begin.

an honest sentence where the rows will be — no illustration party.

Board

.ae-board / .ae-column

Comparable work lanes: ruled columns with quiet cards. They auto-fit when there is room and stack before narrow screens squeeze the task text.

Ready

2

Write acceptance packet

evidence first

Check mobile rail

390px law gate

Live

1

Run visual proof

site and fixture widths

<div class="ae-board">
  <section class="ae-column">
    <div class="ae-column-head">
      <p class="ae-column-title">Ready</p>
      <p class="ae-column-meta">2</p>
    </div>
    <div class="ae-column-body">
      <article class="ae-board-card">
        <p class="ae-item">Write acceptance packet</p>
        <p class="ae-dim">evidence first</p>
      </article>
    </div>
  </section>
</div>

Dialog

.ae-dialog

The panel costume in the top layer: a decision asked in a soft-depth panel over a whisper of paper dim. Escape and the quiet button decline it. Pair with <dialog> + showModal().

Archive this project?

It leaves the index but keeps its history.

<dialog class="ae-dialog">
  <p class="ae-dialog-title">Archive this project?</p>
  <p class="ae-dim">It leaves the index but keeps its history.</p>
  <div class="ae-dialog-acts">
    <button class="ae-button ae-button-quiet">Cancel</button>
    <button class="ae-button">Confirm</button>
  </div>
</dialog>
// open with dialog.showModal();
// both actions close()

Settings

.ae-settings / .ae-menu

A quiet hairline row — label left, current value right; activating it folds a chooser open beneath. Choosing writes the value and folds the menu; one row open at a time.

<div class="ae-settings">
  <button class="ae-setting">
    Theme <span class="ae-setting-val">system</span>
  </button>
  <div class="ae-setting-panel">
    <div class="ae-menu">
      <button class="is-sel">system</button>
      <button>light</button>
      <button>dark</button>
    </div>
  </div>
  …more rows…
</div>
// recipes.js wires the fold, the choice,
// and the written value

STATES · the law holds in every one

closed
resting row — label left, value right in muted ink. The form is a column of lines.
open
.is-open — the chooser folds open beneath. No box, no shadow — a grid-rows expand.
written
after a choice — the value updates, the panel folds. One row open at a time.

Choice

.ae-choice / .ae-check / .ae-radio / .ae-switch

Choice marks are squares — a circle would be the only curve in the system, so there isn't one. The checkbox fills with ink and draws its check in surface; the radio holds an ink core; the switch slides an ink thumb along a hairline channel. State reads from position and fill, never from a green pill.

<label class="ae-choice">
  <input class="ae-check" type="checkbox" />
  Email me on deploy
</label>

<label class="ae-choice">
  <input class="ae-radio" type="radio" name="vis" />
  Public
</label>

<label class="ae-choice">
  <input class="ae-switch" type="checkbox" />
  Auto-deploy
</label>

STATES · the law holds in every one

switch · off
an ink thumb resting left on a hairline channel.
switch · on
the thumb slides right — never a filled green pill.
switch · disabled
add disabled — faded, the thumb holds.
checkbox · disabled
the ink fill intact, faded.
radio · disabled
the ink core intact, faded.

Validation

.ae-field-note + [aria-invalid]

Validation keeps the status law: the field's own line confesses in the status hue, the glyph carries it in the note, and the sentence stays ink.

Not a valid address.

<label class="ae-label" for="email">Email</label>
<input class="ae-input" id="email" type="email"
       aria-invalid="true"
       aria-describedby="email-note" />
<p class="ae-field-note" id="email-note">
  <svg class="ae-icon ae-err">…x…</svg> Not a valid address.
</p>

STATES · the law holds in every one

resting
a hairline underline; the label stays put.
invalid

Not a valid address.

aria-invalid — the line goes err, the glyph carries it, never a filled red box.
disabled
add disabled — the field rests, no edit.

Tabs

.ae-tabs

The nav instrument pointed at panels instead of views: the same quiet words, the same sliding ink underline, aria-selected marking the open one. Panels are views — remount to replay the entrance.

<div class="ae-tabs">
  <button aria-current="page">readme</button>
  <button>tokens</button>
  <button>releases</button>
  <span class="ae-nav-ind"></span>
</div>
// no wired panels here: drop role="tablist"/"tab"
// unless you build the roving-tabindex keyboard pattern

Tag

.ae-tag

The badge, refused. A tag is a word in the plate-caption voice — mono, spaced, hairline at most, never a filled pill. If a tag needs status, the glyph rides beside the word.

canary rust fly.io v1.18

deployed

<span class="ae-tag">rust</span>
<span class="ae-tag ae-tag-bare">v1.18</span>

// status joins as a glyph, never a fill:
<svg class="ae-icon ae-ok">…check…</svg>
<span class="ae-tag">deployed</span>

Tooltip

[data-ae-tip]

A whisper that answers hover: chrome-register words in a hairline slip, pure CSS. Patience built in — it waits a beat on hover and answers focus at once. Keep the words short; anything longer belongs in the prose.

The chrome register explains itself on hover.

<a href="…" data-ae-tip="the one sanctioned 13px register">
  chrome register
</a>
// pure CSS — no recipe required

Popover

.ae-pop

The popover: a slip of surface anchored to its invoker, hairline frame, soft depth, no dim. The native popover attribute owns the top layer, light dismiss, and Escape; the recipe only does the geometry.

<button popovertarget="menu">Options</button>
<div id="menu" popover class="ae-pop">
  <div class="ae-menu">
    <button>rename</button>
    <button>duplicate</button>
    <button>archive</button>
  </div>
</div>
// recipes/pop.js places the open slip

Toast

.ae-toasts / .ae-toast

News arrives at the edge and waits to be read: a slip with the status on its glyph, stacked bottom-right. It persists until dismissed or pushed out — never an anxious timer by default.

// recipes/toast.js:
aeToast('Deploy 4121 resolved.', { status: 'ok' });
aeToast('Build failed on preview.', { status: 'err' });
// opt-in self-dismissal, never the default:
aeToast('Copied.', { timeout: 4000 });

STATES · the law holds in every one

success
Deploy 4121 resolved.
a surface slip; the glyph carries the status.
failure
Build failed on preview.
err on the glyph — never a filled red pill.
neutral
Working set updated.
no status, no glyph — just the slip.

Fold

.ae-fold

Disclosure as a hairline row — native details in the settings costume. The mono plus quarter-turns into a close; that turn is the feedback, once per action.

What does the import cost?

One request, one file, no JavaScript required. The recipes are optional and dependency-free.

Can I change the accent?

The accent is yours — define your scheme and use it with judgment.

<details class="ae-fold">
  <summary>What does the import cost?</summary>
  <p class="ae-dim">One request, one file…</p>
</details>
<details class="ae-fold">…</details>

Breadcrumbs

.ae-crumbs

The path, in the chrome register: here is ink, the way back is muted, the separators are faint.

<nav class="ae-crumbs" aria-label="breadcrumb">
  <ol>
    <li><a href="…">projects</a></li>
    <li><a href="…">canary</a></li>
    <li><span aria-current="page">deploys</span></li>
  </ol>
</nav>

Waiting

.ae-skeleton / .ae-empty

Waiting is still wash, never a shimmer — motion is feedback and waiting is not an action; the view's entrance acknowledges arrival. Absence is an honest sentence with one quiet action, not an illustration party.

loading title

loading meta

loading projects…

No projects yet.

The first one you archive lands here.

// waiting: blocks hold the layout, words go to a status line
<p class="ae-skeleton" style="max-width: 24em">loading title</p>
<p class="ae-chrome" role="status">loading projects…</p>

// absence: an honest sentence, one quiet action
<div class="ae-empty">
  <p class="ae-item">No projects yet.</p>
  <p class="ae-dim">The first one you archive lands here.</p>
  <p><button class="ae-button ae-button-quiet">New project</button></p>
</div>

STATES · the law holds in every one

loading

loading title

loading meta

loading…

still wash holds the layout — no shimmer, no spinner. Words to a status line.
absent

Nothing here yet.

The first item lands here.

an honest sentence — muted words, one quiet action. No illustration party.
arrived

Project Atlas

archived 2 days ago

content replaces the skeleton — the wash held this shape, now it's ink.

Meter

.ae-meter / .ae-meter-mark

The gauge is a ruled line, never a glowing pill: a wash channel, an ink fill, hairline threshold ticks. The fill takes a status ink only when the level is the signal. Width changes are state resolutions — gentle, once.

turn spend · 62%

past the warn threshold

<div class="ae-meter" role="meter"
     aria-valuenow="62" aria-valuemin="0" aria-valuemax="100">
  <span class="ae-meter-fill" style="width: 62%"></span>
  <span class="ae-meter-mark" style="left: 80%"></span>
</div>
// the fill takes a status ink only when
// the level IS the signal:
<span class="ae-meter-fill ae-warn" style="width: 91%"></span>

STATES · the law holds in every one

empty
0% fill — the resting channel before data arrives. No shimmer, no spinner.
partial
ink fill — the normal operational read. The level is not the signal, so no status ink.
at threshold
ink fill with a hairline threshold tick — approaching the limit, still ink.
over limit
.ae-warn fill — the level IS the signal, so the fill takes the status ink. Never a colored pill.

Figures

.ae-num / .ae-strong / .ae-delta / .ae-spark

Figures are tabular numerals that tick without jitter. The hero figure is 800 and isolated — never larger: the instrument reads loud through weight, not scale. Deltas put the hue on the arrow; trends are pen strokes, no axes, no area fill.

requests · trailing hour

48,211 4.2%

<span class="ae-num ae-strong">48,211</span>
<span class="ae-delta ae-num ae-dim">
  <svg class="ae-icon ae-ok">…arrow-up…</svg>4.2%
</span>
<svg class="ae-spark" viewBox="0 0 100 24"
     preserveAspectRatio="none" aria-hidden="true">
  <polyline points="0,18 14,15 … 100,4" />
</svg>

Interval

.ae-ci / .ae-ci-band / .ae-ci-mean

The interval is the meter with a spread: a ruled track carrying a [lo,hi] range band and a mean tick, so a score reads as a level and its confidence at once. Where two bands share a column the lead is inside the spread — the certification read. The band draws in once from the mean; hover (or focus) a track for the exact figures. Every mark is a rectangle, so radius 0 holds.

cand·02 · 86.9 · the lead

cand·05 · inside the spread

cand·01 · whisker variant, no fill

<div class="ae-ci is-lead" role="img"
     aria-label="cand·02 mean 86.9, 95% CI 83.1 to 90.7">
  <span class="ae-ci-band" style="left: 66%; width: 22%"></span>
  <span class="ae-ci-mean" style="left: 77%"></span>
  <span class="ae-ci-detail">μ 86.9 <span class="ae-ci-bound">[83.1, 90.7]</span></span>
</div>
// the no-fill variant: a lo→hi rule with end caps
<span class="ae-ci-whisker" style="left: 60%; width: 18%"></span>
<span class="ae-ci-cap" style="left: 60%"></span>
<span class="ae-ci-cap" style="left: 78%"></span>

STATES · the law holds in every one

default
band + mean — a score and its confidence. Ink throughout, no accent.
lead
.is-lead — the mean tick takes the accent. One accent role per view.
whisker
no-fill variant — a lo→hi rule with end caps. Points, not mass.

Plot

.ae-plot / .ae-plot-frontier / .ae-plot-dot

The plane draws two dimensions at once: cost against quality, the candidates as round dots, the Pareto frontier — nobody both cheaper and better — as a dashed guide. Like the spark, the kit dresses a consumer-authored SVG; the dots are the pen, so radius 0 still governs the frame, axes, and ticks. The default composition is the frontier: the pick is the lone accent dot, the dominated drop to faint. The parts compose — an open .ae-plot-ring marks a reference baseline, a colored .ae-plot-glyph sits where a gate-failed datum would (status rides the glyph).

arena · cost × quality · the frontier

0 2 4 6 60 80 100 COST · $/1K QUALITY sonnet·v4
<svg class="ae-plot" viewBox="0 0 440 360" role="img">
  <path class="ae-plot-axis" d="M 46 16 L 46 320 L 422 320"/>
  <path class="ae-plot-frontier" d="M 100 206 L 186 100 L 358 46"/>
  <circle class="ae-plot-dot is-chosen" cx="186" cy="100" r="4.5"/>
  <circle class="ae-plot-dot is-dominated" cx="239" cy="153" r="4.5"/>
// a gate-failed datum: status rides the glyph
  <g class="ae-plot-glyph ae-err">…</g>
</svg>

Flow

.ae-flow / .ae-node / .ae-wire

The flow draws a system: hairline-framed stages (hard corners, radius 0) joined by orthogonal ink wires, so a pipeline reads as a diagram. State rides the frame and a glyph — a cleared stage carries ✓, the running stage takes the accent, locked stages go dashed-faint. Like the plot, the kit dresses a consumer-authored SVG so nodes and wires share one coordinate space, and non-scaling strokes hold the hairline at any fit. The default is the spine — the gate/lineage backbone; the same nodes compose into stations, ported nodes, or a branching DAG.

foundry · cache-key-collision · the spine

01 arena 02 search 03 gates 04 launch
<svg class="ae-flow" viewBox="0 0 684 140" role="img">
  <path class="ae-wire is-active" d="M 138 70 L 194 70"/>
  <rect class="ae-node is-done" x="18" y="43" width="120" height="54"/>
  <text class="ae-node-label" x="78" y="80">arena</text>
  <path class="ae-icon ae-ok" d="M 117 54 L 120 57 L 126 49"/>
  <rect class="ae-node is-active" x="194" y="43" …/>
  <rect class="ae-node is-locked" x="370" y="43" …/>
</svg>

App shell

.ae-shell / .ae-rail / .ae-desk

The app shell: one viewport, hairline-divided — a 13px rail for places, a desk for work. The desk scrolls inside itself; the page never moves. On small screens the same rail becomes bottom chrome, not a hidden drawer.

rail
· places
· mode
the desk — meters, plates, work
<div class="ae-shell">
  <aside class="ae-rail">
    <p class="ae-h">PLACES</p>
    <a href="…">agents</a>
    <a href="…">history</a>
    <div class="ae-rail-foot">…mode toggle…</div>
  </aside>
  <main class="ae-desk">…the work…</main>
</div>

Document

.ae-doc

The document archetype: rendered markdown drops in unclassed. Headings are weight, never size; rules are hairlines; quotes are inset.

Install

Pin the tag; upgrades are deliberate.

One request, one file, no build step.
pnpm add github:misty-step/aesthetic
<article class="ae-doc">
  <h2>Install</h2>
  <p>Pin the tag; upgrades are deliberate.</p>
  <blockquote>One request, one file…</blockquote>
  <pre><code>pnpm add …</code></pre>
</article>
// markdown output needs no classes inside

Report

.ae-lede / .ae-findings / .ae-pull

The report sub-registers: opt-in additions for a generated document that is read and shared — all weight, space, and hairlines, never a new size or a serif. A <figure> in a doc auto-numbers (FIG n); the lede lifts the standfirst to a way in; the findings box is a hairline frame (never a filled card) with the recommendation on the one accent rule; the pull-quote is the closest call to the one-size law, held on the right side of it. The plain .ae-doc stays the default.

Across ten correctness hypotheses, opus·hi wins eight and clears every gate — but at 2.4× the field-median cost; the ensemble matches it within the spread at 40% of the cost.

gate-weighted pass rate, winner per row in weight

opus·hi 93 · ensemble 89 · sonnet·v4 90 · haiku·rag 44

FINDINGS

opus·hi wins 8 of 10 and clears all five gates, at 2.41× cost; the lead is inside the spread on six hypotheses.

RECOMMENDATION

Ship the ensemble as the default; reserve opus·hi for runs where the cost and latency tail are acceptable.

Where the lead is inside the spread, the ensemble delivers the same quality at 40% of the cost — and cost becomes the deciding axis. — gate-weighted spread analysis

<article class="ae-doc">
  <p class="ae-lede">… the conclusion, up front …</p>
  <figure>
    <figcaption>gate-weighted pass rate</figcaption>
    <div class="ae-plate">…</div>
  </figure>
// the <figure> auto-numbers: FIG 1 ·
  <div class="ae-findings">
    <p class="ae-findings-title">FINDINGS</p>
    <div class="ae-rec">… the one accent rule …</div>
  </div>
  <p class="ae-pull">… by weight + space …</p>
</article>

Screen reader

.ae-sr

Present to assistive tech, invisible on paper: live regions and labels the layout has no room to speak. The send recipe uses one to announce the resolved state.

There is an .ae-sr live region on this very page — you cannot see it, and that is the point.

<span class="ae-sr" role="status"></span>
// recipes/send.js writes "Sent" into it
// when the send moment resolves