idiom
/
Search
T
Next theme
Shift T
Previous theme
D
Light, dark, system

Ctrl or Cmd K also opens search.

Theme

FolioPatchMatineeBrickInkFootlights

Used on dannybrown.devUsed on deluge.questUsed on hamilhamilhamil.comUsed on maadgoat.comNot used on any site yetNot used on any site yet

My working set of interface parts. Each one is plain HTML and CSS that reads only theme tokens, so the theme switch above restyles everything on this page. Copy whatever you like: the code is MIT licensed.

6 themes35 elements

Foundations

Tokens
Colour
  • --bgPage ground
  • --surfaceRaised: panels, inputs
  • --sunkenRecessed: wells, code, hover
  • --lineHairlines and dividers
  • --line-strongControl borders
  • --textBody text
  • --text-mutedSecondary text
  • --text-faintLabels and metadata
  • --accentThe one accent: selected, primary, on
  • --accent-inkAccent as text, contrast-safe
  • --posSuccess
  • --negError, destructive
Type

Quarterly numbers

Body copy sits at 16px with a line height around 1.55, held to a measure of roughly 65 characters so the eye can find the start of the next line.

git log --oneline -n 3 · 0.875rem · 1,204.50

Space
--space-1--space-2--space-3--space-4--space-5--space-6--space-7--space-8
Shape and depth
--radius-s
--radius-m
--radius-control
--shadow-control
--shadow-raise

Buttons

3 elements

Button

buttons/button
Code
HTML
<button class="btn btn-primary" type="button">Save changes</button>
<button class="btn" type="button">Preview</button>
<button class="btn btn-ghost" type="button">Cancel</button>
<button class="btn" type="button" disabled>Archived</button>
<button class="btn btn-primary btn-s" type="button">Small</button>
CSS
.btn {
  --btn-bg: var(--surface);
  --btn-fg: var(--text);
  --btn-line: var(--line-strong);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 2.5rem;
  padding: 0 var(--space-4);
  font: var(--control-weight, 500) var(--text-sm) / 1 var(--font-body);
  text-transform: var(--control-case, none);
  letter-spacing: var(--control-tracking, normal);
  color: var(--btn-fg);
  background: var(--btn-bg);
  border: 1px solid var(--btn-line);
  border-radius: var(--radius-control);
  box-shadow: var(--shadow-control);
  cursor: pointer;
  transition:
    translate var(--dur-fast) var(--ease),
    box-shadow var(--dur-fast) var(--ease),
    background-color var(--dur-fast) var(--ease);
}

.btn:hover {
  --btn-bg: var(--sunken);
}

.btn:active {
  translate: var(--press) var(--press);
  box-shadow: none;
}

.btn-primary {
  --btn-bg: var(--accent);
  --btn-fg: var(--on-accent);
  --btn-line: var(--accent);
}

.btn-primary:hover {
  --btn-bg: var(--accent-hover);
  --btn-line: var(--accent-hover);
}

.btn-ghost {
  --btn-bg: transparent;
  --btn-line: transparent;
  box-shadow: none;
}

.btn-s {
  min-height: 2rem;
  padding: 0 var(--space-3);
  font-size: var(--text-xs);
}

.btn:disabled {
  --btn-bg: var(--sunken);
  --btn-fg: var(--text-faint);
  --btn-line: var(--line);
  box-shadow: none;
  translate: none;
  cursor: not-allowed;
}

@media (prefers-reduced-motion: reduce) {
  .btn {
    transition: none;
  }
}

Icon button

buttons/icon-button
Code
HTML
<button class="icon-btn" type="button" aria-label="Edit" title="Edit">
  <svg viewBox="0 0 16 16" aria-hidden="true">
    <path d="M10.5 2.5l3 3L5 14H2v-3z" />
  </svg>
</button>
<button class="icon-btn" type="button" aria-label="Copy" title="Copy">
  <svg viewBox="0 0 16 16" aria-hidden="true">
    <rect x="5" y="5" width="9" height="9" rx="1" />
    <path d="M11 5V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h2" />
  </svg>
</button>
<button class="icon-btn" type="button" aria-label="Delete" title="Delete">
  <svg viewBox="0 0 16 16" aria-hidden="true">
    <path d="M2.5 4h11M6 4V2.5h4V4M4 4l.8 9.5h6.4L12 4" />
  </svg>
</button>
CSS
.icon-btn {
  display: inline-grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  color: var(--text-muted);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  cursor: pointer;
  transition:
    color var(--dur-fast) var(--ease),
    background-color var(--dur-fast) var(--ease);
}

.icon-btn:hover {
  color: var(--text);
  background: var(--sunken);
}

.icon-btn:active {
  translate: 0 var(--press);
}

.icon-btn svg {
  width: 1rem;
  height: 1rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

@media (prefers-reduced-motion: reduce) {
  .icon-btn {
    transition: none;
  }
}

Button group

buttons/button-group

Builds on Button.

Code
HTML
<fieldset class="btn-group" aria-label="Text alignment" data-exclusive>
  <button class="btn" type="button" aria-pressed="true">Left</button>
  <button class="btn" type="button" aria-pressed="false">Center</button>
  <button class="btn" type="button" aria-pressed="false">Right</button>
</fieldset>
<fieldset class="btn-group" aria-label="Formatting">
  <button class="btn" type="button" aria-pressed="true" aria-label="Bold"><b>B</b></button>
  <button class="btn" type="button" aria-pressed="false" aria-label="Italic"><i>I</i></button>
  <button class="btn" type="button" aria-pressed="false" aria-label="Underline"><u>U</u></button>
</fieldset>
CSS
/* Builds on button.css. Joined buttons share one border and one shadow. */

.btn-group {
  display: inline-flex;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: var(--radius-control);
  box-shadow: var(--shadow-control);
}

.btn-group .btn {
  border-radius: 0;
  box-shadow: none;
}

.btn-group .btn + .btn {
  margin-left: -1px;
}

.btn-group .btn:first-child {
  border-start-start-radius: var(--radius-control);
  border-end-start-radius: var(--radius-control);
}

.btn-group .btn:last-child {
  border-start-end-radius: var(--radius-control);
  border-end-end-radius: var(--radius-control);
}

.btn-group .btn:active {
  translate: none;
}

.btn-group .btn[aria-pressed="true"] {
  --btn-bg: var(--accent-soft);
  position: relative;
  z-index: 1;
  border-color: var(--accent);
}
JS
document.addEventListener("click", (event) => {
  const button = event.target.closest?.(".btn-group [aria-pressed]");
  if (!button) return;
  const group = button.closest(".btn-group");
  if (group.hasAttribute("data-exclusive")) {
    for (const other of group.querySelectorAll("[aria-pressed]")) {
      other.setAttribute("aria-pressed", String(other === button));
    }
  } else {
    const pressed = button.getAttribute("aria-pressed") === "true";
    button.setAttribute("aria-pressed", String(!pressed));
  }
});

Inputs

7 elements

Text field

inputs/text-field

Only used for receipts.

Lowercase letters, numbers and hyphens only.

Code
HTML
<div class="field">
  <label class="field-label" for="tf-name">Display name</label>
  <input class="input" id="tf-name" type="text" value="Danny" autocomplete="off" />
</div>
<div class="field">
  <label class="field-label" for="tf-email">Email</label>
  <input
    class="input"
    id="tf-email"
    type="email"
    placeholder="you@example.com"
    aria-describedby="tf-email-hint"
  />
  <p class="field-hint" id="tf-email-hint">Only used for receipts.</p>
</div>
<div class="field">
  <label class="field-label" for="tf-slug">URL slug</label>
  <input
    class="input"
    id="tf-slug"
    type="text"
    value="My Post!"
    aria-invalid="true"
    aria-describedby="tf-slug-error"
  />
  <p class="field-error" id="tf-slug-error">
    Lowercase letters, numbers and hyphens only.
  </p>
</div>
CSS
.field {
  display: grid;
  gap: var(--space-1);
  width: min(100%, 18rem);
  align-content: start;
}

.field-wide {
  width: min(100%, 28rem);
}

/* Label on the left edge, a live value or count on the right. */
.field-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}

.field-label {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text);
}

.field-hint,
.field-error {
  margin: 0;
  font-size: var(--text-xs);
  line-height: 1.4;
  color: var(--text-muted);
}

.field-error {
  color: var(--neg);
}

.input {
  width: 100%;
  min-height: 2.5rem;
  padding: 0 var(--space-3);
  font: inherit;
  font-size: var(--text-sm);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  /* Pill-shaped themes would turn a textarea into a capsule; cap the curve. */
  border-radius: min(var(--radius-control), var(--radius-m));
  transition: border-color var(--dur-fast) var(--ease);
}

.input::placeholder {
  color: var(--text-faint);
}

.input:hover {
  border-color: var(--text-muted);
}

.input:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 0;
  border-color: var(--accent);
}

.input[aria-invalid="true"] {
  border-color: var(--neg);
}

.input[aria-invalid="true"]:focus-visible {
  outline-color: var(--neg);
}

.input:disabled {
  color: var(--text-faint);
  background: var(--sunken);
  border-color: var(--line);
  cursor: not-allowed;
}

Textarea

inputs/textarea

Builds on Text field.

0 / 160
Code
HTML
<div class="field field-wide">
  <div class="field-row">
    <label class="field-label" for="ta-bio">Bio</label>
    <output class="field-count" for="ta-bio" data-count-for="ta-bio">0 / 160</output>
  </div>
  <textarea class="input textarea" id="ta-bio" maxlength="160" rows="4">
Writes software in Florida. Sees too many musicals.</textarea
  >
</div>
CSS
/* Builds on text-field.css. */

.field-count {
  font: var(--text-xs) var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
}

.textarea {
  min-height: 6rem;
  padding: var(--space-2) var(--space-3);
  line-height: 1.5;
  resize: vertical;
}
JS
const update = (output) => {
  const field = document.getElementById(output.dataset.countFor);
  if (field) output.value = `${field.value.length} / ${field.maxLength}`;
};

for (const output of document.querySelectorAll("[data-count-for]")) {
  update(output);
}

document.addEventListener("input", (event) => {
  const output = document.querySelector(
    `[data-count-for="${event.target.id}"]`,
  );
  if (output) update(output);
});

Select

inputs/select

Builds on Text field.

Code
HTML
<div class="field">
  <label class="field-label" for="sel-tz">Time zone</label>
  <span class="select">
    <select class="input" id="sel-tz">
      <option>America/New_York</option>
      <option>America/Chicago</option>
      <option>America/Los_Angeles</option>
      <option>Europe/London</option>
    </select>
  </span>
</div>
CSS
/* Builds on text-field.css. The native <select> stays, so the option list is
   the platform's own on every device. */

.select {
  position: relative;
  display: block;
}

.select .input {
  appearance: none;
  padding-right: var(--space-7);
  cursor: pointer;
}

.select::after {
  content: "";
  position: absolute;
  top: 50%;
  right: var(--space-4);
  width: 0.4rem;
  height: 0.4rem;
  border-right: 1.5px solid var(--text-muted);
  border-bottom: 1.5px solid var(--text-muted);
  translate: 0 -70%;
  rotate: 45deg;
  pointer-events: none;
}

Checkbox

inputs/checkbox
Notify me about
Code
HTML
<fieldset class="choices">
  <legend class="choices-legend">Notify me about</legend>
  <label class="check"><input type="checkbox" checked />Replies</label>
  <label class="check"><input type="checkbox" />Mentions</label>
  <label class="check"><input type="checkbox" disabled />Weekly summary</label>
</fieldset>
CSS
.choices {
  display: grid;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  border: 0;
  min-width: 0;
}

.choices-legend {
  margin-bottom: var(--space-2);
  padding: 0;
  font-size: var(--text-sm);
  font-weight: 500;
}

.check {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  cursor: pointer;
}

.check input {
  appearance: none;
  display: grid;
  place-items: center;
  flex: none;
  width: 1.125rem;
  height: 1.125rem;
  margin: 0;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: min(var(--radius-s), 4px);
  cursor: pointer;
}

/* The mark is drawn, not a glyph, so it inherits nothing from the font. */
.check input::before {
  content: "";
  width: 0.55rem;
  height: 0.3rem;
  border-left: 2px solid var(--on-accent);
  border-bottom: 2px solid var(--on-accent);
  translate: 0 -1px;
  rotate: -45deg;
  opacity: 0;
}

.check input:checked {
  background: var(--accent);
  border-color: var(--accent);
}

.check input:checked::before {
  opacity: 1;
}

.check:has(:disabled) {
  color: var(--text-faint);
  cursor: not-allowed;
}

.check input:disabled {
  background: var(--sunken);
  border-color: var(--line);
  cursor: not-allowed;
}

Radio

inputs/radio

Builds on Checkbox.

Plan
Code
HTML
<fieldset class="choices">
  <legend class="choices-legend">Plan</legend>
  <label class="radio"><input type="radio" name="plan" value="free" />Free</label>
  <label class="radio">
    <input type="radio" name="plan" value="pro" checked />Pro, $8 a month
  </label>
  <label class="radio">
    <input type="radio" name="plan" value="team" disabled />Team (invite only)
  </label>
</fieldset>
CSS
/* Shares .choices / .choices-legend with checkbox.css. */

.radio {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  cursor: pointer;
}

.radio input {
  appearance: none;
  flex: none;
  width: 1.125rem;
  height: 1.125rem;
  margin: 0;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  cursor: pointer;
}

/* The dot is the surface showing through a thick inset ring. */
.radio input:checked {
  background: var(--accent);
  border-color: var(--accent);
  box-shadow: inset 0 0 0 3px var(--surface);
}

.radio:has(:disabled) {
  color: var(--text-faint);
  cursor: not-allowed;
}

.radio input:disabled {
  background: var(--sunken);
  border-color: var(--line);
  cursor: not-allowed;
}

Range

inputs/range

Builds on Text field.

60
Code
HTML
<div class="field field-wide">
  <div class="field-row">
    <label class="field-label" for="rg-volume">Volume</label>
    <output class="range-value" for="rg-volume">60</output>
  </div>
  <input class="range" id="rg-volume" type="range" min="0" max="100" value="60" />
</div>
CSS
/* --fill is set by range.js; without script the track simply shows unfilled. */

.range {
  --fill: 0%;
  appearance: none;
  width: 100%;
  height: 1.25rem;
  margin: 0;
  background: transparent;
  cursor: pointer;
}

.range::-webkit-slider-runnable-track {
  height: 4px;
  /* slop-ok: a hard two-stop progress fill, not a decorative blend */
  background: linear-gradient(
    to right,
    var(--accent) var(--fill),
    var(--line-strong) var(--fill)
  );
  border-radius: 2px;
}

.range::-moz-range-track {
  height: 4px;
  background: var(--line-strong);
  border-radius: 2px;
}

.range::-moz-range-progress {
  height: 4px;
  background: var(--accent);
  border-radius: 2px;
}

.range::-webkit-slider-thumb {
  appearance: none;
  width: 1rem;
  height: 1rem;
  margin-top: -6px;
  background: var(--surface);
  border: 2px solid var(--text);
  border-radius: 50%;
}

.range::-moz-range-thumb {
  width: 1rem;
  height: 1rem;
  background: var(--surface);
  border: 2px solid var(--text);
  border-radius: 50%;
}

.range:focus-visible {
  outline: none;
}

.range:focus-visible::-webkit-slider-thumb {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.range:focus-visible::-moz-range-thumb {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.range-value {
  font: var(--text-xs) var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}
JS
const sync = (range) => {
  const span = range.max - range.min || 100;
  range.style.setProperty(
    "--fill",
    `${((range.value - range.min) / span) * 100}%`,
  );
  const output = document.querySelector(`output[for="${range.id}"]`);
  if (output) output.value = range.value;
};

for (const range of document.querySelectorAll(".range")) sync(range);

document.addEventListener("input", (event) => {
  if (event.target.matches?.(".range")) sync(event.target);
});

Selectors

2 elements

Segmented control

selectors/segmented
View
Code
HTML
<fieldset class="segmented">
  <legend class="segmented-legend">View</legend>
  <label><input type="radio" name="view" value="day" />Day</label>
  <label><input type="radio" name="view" value="week" checked />Week</label>
  <label><input type="radio" name="view" value="month" />Month</label>
</fieldset>
CSS
/* Native radios, so arrow keys, form submission and screen readers work
   with no script. */
.segmented {
  display: inline-flex;
  gap: 2px;
  margin: 0;
  padding: 2px;
  min-width: 0;
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
}

.segmented-legend {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.segmented label {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: 1.75rem;
  padding: 0 var(--space-3);
  font: 500 var(--text-xs) / 1 var(--font-body);
  color: var(--text-muted);
  border-radius: var(--radius-control);
  cursor: pointer;
  user-select: none;
}

.segmented label:hover {
  color: var(--text);
}

.segmented input {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.segmented label:has(:checked) {
  color: var(--bg);
  background: var(--text);
}

.segmented label:has(:focus-visible) {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

Filter chips

selectors/chips
Status
Code
HTML
<fieldset class="chips">
  <legend class="chips-legend">Status</legend>
  <label class="chip"><input type="checkbox" checked />Open</label>
  <label class="chip"><input type="checkbox" />Draft</label>
  <label class="chip"><input type="checkbox" checked />In review</label>
  <label class="chip"><input type="checkbox" />Closed</label>
</fieldset>
CSS
/* Filter chips: off is a bare outline, on earns the accent. */

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  border: 0;
  min-width: 0;
}

.chips-legend {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-height: 1.875rem;
  padding: 0 var(--space-3);
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--text-muted);
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  cursor: pointer;
  user-select: none;
}

.chip:hover {
  color: var(--text);
}

.chip input {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.chip::before {
  content: "";
  width: 0.45rem;
  height: 0.25rem;
  border-left: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  rotate: -45deg;
  translate: 0 -1px;
  display: none;
}

.chip:has(:checked) {
  color: var(--text);
  background: var(--accent-soft);
  border-color: var(--accent);
}

.chip:has(:checked)::before {
  display: block;
}

.chip:has(:focus-visible) {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

Toggles

1 element

Switch

toggles/switch
Code
HTML
<label class="switch">
  <input type="checkbox" role="switch" checked />
  <span class="switch-track" aria-hidden="true"></span>
  Email digests
</label>
<label class="switch">
  <input type="checkbox" role="switch" />
  <span class="switch-track" aria-hidden="true"></span>
  Sound
</label>
<label class="switch">
  <input type="checkbox" role="switch" disabled />
  <span class="switch-track" aria-hidden="true"></span>
  Sync (needs an account)
</label>
CSS
.switch {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--text-sm);
  color: var(--text);
  cursor: pointer;
}

.switch input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}

.switch-track {
  position: relative;
  flex: none;
  width: 2.25rem;
  height: 1.25rem;
  background: var(--sunken);
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  transition: background-color var(--dur) var(--ease);
}

.switch-track::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: calc(1.25rem - 6px);
  height: calc(1.25rem - 6px);
  background: var(--text-muted);
  border-radius: 50%;
  transition:
    translate var(--dur) var(--ease),
    background-color var(--dur) var(--ease);
}

/* Off is the bare outline; only "on" earns the accent. */
.switch input:checked + .switch-track {
  background: var(--accent);
  border-color: var(--accent);
}

.switch input:checked + .switch-track::after {
  translate: 1rem 0;
  background: var(--on-accent);
}

.switch input:focus-visible + .switch-track {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.switch:has(:disabled) {
  color: var(--text-faint);
  cursor: not-allowed;
}

.switch input:disabled + .switch-track {
  border-color: var(--line);
}

@media (prefers-reduced-motion: reduce) {
  .switch-track,
  .switch-track::after {
    transition: none;
  }
}

Feedback

6 elements

Notice

feedback/notice

Prices refresh hourly

The lottery and rush columns come from the box office feed.

Song exported

Copy SONG042.XML to the SONGS folder on the SD card.

Unsaved changes

Leaving this page discards the last 3 edits.

Code
HTML
<div class="notice" role="note">
  <svg class="notice-mark" viewBox="0 0 16 16" aria-hidden="true">
    <circle cx="8" cy="8" r="6.5" />
    <path d="M8 7.5v4M8 5v.01" />
  </svg>
  <div>
    <p class="notice-title">Prices refresh hourly</p>
    <p>The lottery and rush columns come from the box office feed.</p>
  </div>
</div>
<div class="notice notice-pos" role="status">
  <svg class="notice-mark" viewBox="0 0 16 16" aria-hidden="true">
    <circle cx="8" cy="8" r="6.5" />
    <path d="M5.5 8.2l1.8 1.8 3.2-3.6" />
  </svg>
  <div>
    <p class="notice-title">Song exported</p>
    <p>Copy SONG042.XML to the SONGS folder on the SD card.</p>
  </div>
</div>
<div class="notice notice-warn" role="status">
  <svg class="notice-mark" viewBox="0 0 16 16" aria-hidden="true">
    <path d="M8 1.8l6.5 12H1.5z" />
    <path d="M8 6.5v3.5M8 12v.01" />
  </svg>
  <div>
    <p class="notice-title">Unsaved changes</p>
    <p>Leaving this page discards the last 3 edits.</p>
  </div>
</div>
<div class="notice notice-neg" role="alert">
  <svg class="notice-mark" viewBox="0 0 16 16" aria-hidden="true">
    <circle cx="8" cy="8" r="6.5" />
    <path d="M5.8 5.8l4.4 4.4M10.2 5.8l-4.4 4.4" />
  </svg>
  <div>
    <p class="notice-title">Upload failed</p>
    <p>The file is over 20 MB. Try a shorter MIDI clip.</p>
  </div>
</div>
CSS
/* Tone is carried by the mark and a faint tint, not a coloured side stripe. */

.notice {
  --tone: var(--text-muted);
  display: flex;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-sm);
  color: var(--text-muted);
  background: color-mix(in oklab, var(--tone) 7%, var(--surface));
  border: 1px solid color-mix(in oklab, var(--tone) 28%, var(--line));
  border-radius: var(--radius-m);
}

.notice p {
  margin: 0;
}

.notice-title {
  font-weight: 600;
  color: var(--text);
}

.notice-mark {
  flex: none;
  width: 1.125rem;
  height: 1.125rem;
  margin-top: 1px;
  fill: none;
  stroke: var(--tone);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.notice-pos {
  --tone: var(--pos);
}

.notice-warn {
  --tone: var(--accent-ink);
}

.notice-neg {
  --tone: var(--neg);
}

Toast

feedback/toast

Builds on Button.

Code
HTML
<button class="btn" type="button" data-toast="Changes saved">Show toast</button>
<button
  class="btn"
  type="button"
  data-toast="Couldn't reach the server"
  data-toast-tone="neg"
>
  Show error toast
</button>
<div class="toast-region" role="status" aria-live="polite"></div>
CSS
/* Inverted against the page so it reads as transient, not as content. */

.toast-region {
  position: fixed;
  right: var(--space-5);
  bottom: var(--space-5);
  z-index: 50;
  display: grid;
  gap: var(--space-2);
  justify-items: end;
  pointer-events: none;
}

.toast {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  max-width: 22rem;
  padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4);
  font-size: var(--text-sm);
  color: var(--bg);
  background: var(--text);
  border-radius: var(--radius-m);
  box-shadow: var(--shadow-raise);
  pointer-events: auto;
  transition:
    opacity var(--dur) var(--ease),
    translate var(--dur) var(--ease);

  @starting-style {
    opacity: 0;
    translate: 0 var(--space-3);
  }
}

.toast-neg::before {
  content: "";
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  background: var(--neg);
  border-radius: 50%;
}

.toast-close {
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  margin-left: auto;
  padding: 0;
  color: inherit;
  background: none;
  border: 0;
  border-radius: var(--radius-s);
  opacity: 0.7;
  cursor: pointer;
}

.toast-close:hover {
  opacity: 1;
}

.toast-close svg {
  width: 0.75rem;
  height: 0.75rem;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
}

@media (prefers-reduced-motion: reduce) {
  .toast {
    transition: none;
  }
}

@media (max-width: 40rem) {
  .toast-region {
    right: var(--space-3);
    left: var(--space-3);
    bottom: var(--space-3);
  }
}
JS
const CLOSE_ICON =
  '<svg viewBox="0 0 12 12" aria-hidden="true"><path d="M2 2l8 8M10 2l-8 8"/></svg>';

const dismiss = (toast) => {
  clearTimeout(toast.timer);
  toast.remove();
};

// Hovering or focusing a toast pauses its timer, so nobody loses a message
// they're in the middle of reading.
const arm = (toast) => {
  clearTimeout(toast.timer);
  toast.timer = setTimeout(() => dismiss(toast), 5000);
};

export function showToast(message, tone) {
  const region = document.querySelector(".toast-region");
  if (!region) return;
  const toast = document.createElement("div");
  toast.className = tone ? `toast toast-${tone}` : "toast";
  const text = document.createElement("span");
  text.textContent = message;
  const close = document.createElement("button");
  close.type = "button";
  close.className = "toast-close";
  close.setAttribute("aria-label", "Dismiss");
  close.innerHTML = CLOSE_ICON;
  close.addEventListener("click", () => dismiss(toast));
  toast.append(text, close);
  toast.addEventListener("pointerenter", () => clearTimeout(toast.timer));
  toast.addEventListener("pointerleave", () => arm(toast));
  toast.addEventListener("focusin", () => clearTimeout(toast.timer));
  toast.addEventListener("focusout", () => arm(toast));
  region.append(toast);
  arm(toast);
  return toast;
}

document.addEventListener("click", (event) => {
  const trigger = event.target.closest?.("[data-toast]");
  if (trigger) showToast(trigger.dataset.toast, trigger.dataset.toastTone);
});

Progress

feedback/progress
64%
64%
Checkout Step 2 of 4
  1. Cart
  2. Shipping
  3. Payment
  4. Review
Code
HTML
<div class="progress-field">
  <div class="progress-row">
    <label for="pg-upload">Uploading 12 photos</label>
    <span class="progress-value">64%</span>
  </div>
  <progress class="progress" id="pg-upload" max="100" value="64">64%</progress>
</div>
<div class="progress-field">
  <div class="progress-row">
    <span id="pg-steps-label">Checkout</span>
    <span class="progress-value">Step 2 of 4</span>
  </div>
  <ol class="steps" aria-labelledby="pg-steps-label">
    <li class="step-done">Cart</li>
    <li class="step-current" aria-current="step">Shipping</li>
    <li>Payment</li>
    <li>Review</li>
  </ol>
</div>
CSS
.progress-field {
  display: grid;
  gap: var(--space-2);
  width: min(100%, 22rem);
  font-size: var(--text-sm);
}

.progress-row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
}

.progress-value {
  font: var(--text-xs) var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

.progress {
  appearance: none;
  width: 100%;
  height: 6px;
  overflow: hidden;
  background: var(--line);
  border: 0;
  border-radius: 3px;
}

.progress::-webkit-progress-bar {
  background: var(--line);
}

.progress::-webkit-progress-value {
  background: var(--accent);
  border-radius: 3px;
}

.progress::-moz-progress-bar {
  background: var(--accent);
  border-radius: 3px;
}

/* Steps: done and current fill, the rest stay as track. Labels sit under
   their own segment so the bar doubles as the legend. */
.steps {
  display: grid;
  grid-auto-columns: 1fr;
  grid-auto-flow: column;
  gap: var(--space-1);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--text-xs);
  color: var(--text-faint);
}

.steps li {
  padding-top: var(--space-2);
  border-top: 4px solid var(--line);
}

.steps .step-done {
  color: var(--text-muted);
  border-top-color: var(--text);
}

.steps .step-current {
  font-weight: 600;
  color: var(--text);
  border-top-color: var(--accent);
}

Spinner

feedback/spinner

Builds on Button.

Loading results…
Code
HTML
<span class="loading" role="status">
  <span class="spinner" aria-hidden="true"></span>
  Loading results…
</span>
<button class="btn btn-primary" type="button" aria-busy="true" disabled>
  <span class="spinner" aria-hidden="true"></span>
  Saving
</button>
CSS
/* The label always says what is happening, so reduced motion loses nothing:
   the ring simply stops turning. */

.loading {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.spinner {
  flex: none;
  width: 1em;
  height: 1em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  opacity: 0.8;
  animation: spin 0.8s linear infinite;
}

@keyframes spin {
  to {
    rotate: 1turn;
  }
}

@media (prefers-reduced-motion: reduce) {
  .spinner {
    animation: none;
  }
}

Skeleton

feedback/skeleton
Code
HTML
<div class="skeleton-row" role="status" aria-busy="true" aria-label="Loading">
  <span class="skeleton skeleton-circle"></span>
  <span class="skeleton-lines">
    <span class="skeleton" style="width: 40%"></span>
    <span class="skeleton" style="width: 75%"></span>
  </span>
</div>
CSS
/* A slow opacity pulse rather than a sweeping shimmer. */

.skeleton-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: min(100%, 24rem);
}

.skeleton-lines {
  display: grid;
  flex: 1;
  gap: var(--space-2);
}

.skeleton {
  display: block;
  height: 0.75rem;
  background: var(--line);
  border-radius: var(--radius-s);
  animation: skeleton-pulse 1.6s var(--ease) infinite alternate;
}

.skeleton-circle {
  flex: none;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
}

@keyframes skeleton-pulse {
  to {
    opacity: 0.45;
  }
}

@media (prefers-reduced-motion: reduce) {
  .skeleton {
    animation: none;
  }
}

Empty state

feedback/empty-state

Builds on Button.

No saved searches

Save a search from the results page and it shows up here with new matches counted.

Code
HTML
<div class="empty">
  <p class="empty-title">No saved searches</p>
  <p class="empty-text">
    Save a search from the results page and it shows up here with new matches
    counted.
  </p>
  <button class="btn" type="button">Browse shows</button>
</div>
CSS
/* Left-aligned like the content it stands in for; the dashed edge marks the
   space as a slot waiting to be filled. */

.empty {
  display: grid;
  justify-items: start;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-6);
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius-m);
}

.empty p {
  margin: 0;
}

.empty-title {
  font-weight: 600;
}

.empty-text {
  max-width: 34rem;
  margin-bottom: var(--space-2);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

Data

5 elements

Tag

data/tag
Draft In review Merged Failed Live Paused
Code
HTML
<span class="tag">Draft</span>
<span class="tag tag-accent">In review</span>
<span class="tag tag-pos">Merged</span>
<span class="tag tag-neg">Failed</span>
<span class="tag tag-dot tag-pos">Live</span>
<span class="tag tag-dot">Paused</span>
CSS
/* For enum values (status, category). The neutral tag is the default; colour
   only appears when the value itself means something. */

.tag {
  --tone: var(--text-muted);
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 2px var(--space-2);
  font-size: var(--text-xs);
  font-weight: 500;
  line-height: 1.4;
  white-space: nowrap;
  color: var(--tone);
  background: color-mix(in oklab, var(--tone) 12%, var(--bg));
  border-radius: min(var(--radius-s), 4px);
}

.tag-accent {
  --tone: var(--accent-ink);
}

.tag-pos {
  --tone: var(--pos);
}

.tag-neg {
  --tone: var(--neg);
}

.tag-dot::before {
  content: "";
  width: 0.4rem;
  height: 0.4rem;
  background: currentColor;
  border-radius: 50%;
}

Stat

data/stat
Avg. ticket
$142
+4.2% vs last week
Capacity
96.1%
−1.3% vs last week
Shows tracked
38
No change
Code
HTML
<dl class="stats">
  <div class="stat">
    <dt>Avg. ticket</dt>
    <dd class="stat-value">$142</dd>
    <dd class="stat-delta stat-up">+4.2% vs last week</dd>
  </div>
  <div class="stat">
    <dt>Capacity</dt>
    <dd class="stat-value">96.1%</dd>
    <dd class="stat-delta stat-down">−1.3% vs last week</dd>
  </div>
  <div class="stat">
    <dt>Shows tracked</dt>
    <dd class="stat-value">38</dd>
    <dd class="stat-delta">No change</dd>
  </div>
</dl>
CSS
/* Readouts separated by rules rather than boxed as cards. */

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  width: 100%;
  margin: 0;
}

.stat {
  display: grid;
  gap: var(--space-1);
  padding: 0 var(--space-5);
  border-left: 1px solid var(--line);
}

.stat:first-child {
  padding-left: 0;
  border-left: 0;
}

.stat dt {
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.stat dd {
  margin: 0;
}

.stat-value {
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  font-stretch: var(--display-stretch, normal);
  font-size: var(--text-2xl);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--display-tracking);
  line-height: 1.1;
}

.stat-delta {
  font-size: var(--text-xs);
  color: var(--text-faint);
}

.stat-up {
  color: var(--pos);
}

.stat-down {
  color: var(--neg);
}

.stat-up::before {
  content: "▲ ";
}

.stat-down::before {
  content: "▼ ";
}

@media (max-width: 40rem) {
  .stats {
    grid-template-columns: minmax(0, 1fr);
  }

  .stat {
    padding: var(--space-3) 0;
    border-left: 0;
    border-top: 1px solid var(--line);
  }

  .stat:first-child {
    border-top: 0;
  }
}

Table

data/table
Lowest prices, Thursday evening
Show Theatre Seats Low Change
Hamilton Richard Rodgers 1,319 $179.00 +2.1%
Hadestown Walter Kerr 945 $89.50 −6.4%
Chicago Ambassador 1,088 $64.00 0.0%
Cats Winter Garden 1,526 n/a Closed
Code
HTML
<div class="table-wrap">
  <table class="table">
    <caption>
      Lowest prices, Thursday evening
    </caption>
    <thead>
      <tr>
        <th scope="col">Show</th>
        <th scope="col">Theatre</th>
        <th scope="col" class="num">Seats</th>
        <th scope="col" class="num">Low</th>
        <th scope="col" class="num">Change</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">Hamilton</th>
        <td>Richard Rodgers</td>
        <td class="num">1,319</td>
        <td class="num">$179.00</td>
        <td class="num delta-up">+2.1%</td>
      </tr>
      <tr>
        <th scope="row">Hadestown</th>
        <td>Walter Kerr</td>
        <td class="num">945</td>
        <td class="num">$89.50</td>
        <td class="num delta-down">−6.4%</td>
      </tr>
      <tr>
        <th scope="row">Chicago</th>
        <td>Ambassador</td>
        <td class="num">1,088</td>
        <td class="num">$64.00</td>
        <td class="num">0.0%</td>
      </tr>
      <tr class="row-muted">
        <th scope="row">Cats</th>
        <td>Winter Garden</td>
        <td class="num">1,526</td>
        <td class="num">n/a</td>
        <td class="num">Closed</td>
      </tr>
    </tbody>
  </table>
</div>
CSS
.table-wrap {
  width: 100%;
  overflow-x: auto;
}

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}

.table caption {
  padding-bottom: var(--space-3);
  font-size: var(--text-xs);
  color: var(--text-muted);
  text-align: left;
}

.table th,
.table td {
  padding: var(--space-2) var(--space-3);
  text-align: left;
  white-space: nowrap;
  border-bottom: 1px solid var(--line);
}

.table :is(th, td):first-child {
  padding-left: 0;
}

.table :is(th, td):last-child {
  padding-right: 0;
}

.table thead th {
  font: 500 var(--text-xs) var(--font-mono);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-faint);
  border-bottom-color: var(--line-strong);
}

.table tbody th {
  font-weight: 600;
}

/* Right-aligned and tabular so digits line up by place value. */
.table .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.table tbody tr:hover {
  background: var(--sunken);
}

.table .delta-up {
  color: var(--pos);
}

.table .delta-down {
  color: var(--neg);
}

.table .row-muted {
  color: var(--text-faint);
}

Key-value list

data/key-value
Order
#40213
Seats
Orchestra M 104–105
Tickets
$358.00
Fees
$41.20
Total
$399.20
Code
HTML
<dl class="kv">
  <div><dt>Order</dt><dd>#40213</dd></div>
  <div><dt>Seats</dt><dd>Orchestra M 104–105</dd></div>
  <div><dt>Tickets</dt><dd>$358.00</dd></div>
  <div><dt>Fees</dt><dd>$41.20</dd></div>
  <div class="kv-total"><dt>Total</dt><dd>$399.20</dd></div>
</dl>
CSS
/* A receipt: names on the left edge, values on the right edge. */

.kv {
  display: grid;
  width: min(100%, 22rem);
  margin: 0;
  font-size: var(--text-sm);
}

.kv > div {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-1) 0;
}

.kv dt {
  color: var(--text-muted);
}

.kv dd {
  margin: 0;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.kv > .kv-total {
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--line-strong);
  font-weight: 600;
}

.kv-total dt {
  color: var(--text);
}

Avatar

data/avatar
DB LM
Code
HTML
<span class="avatar" role="img" aria-label="Danny Brown">DB</span>
<span class="avatar avatar-s" role="img" aria-label="Lena Marsh">LM</span>
<span
  class="avatar-group"
  role="img"
  aria-label="Watching: Ada Moss, Sam Fox, Bea Park and 2 others"
>
  <span class="avatar avatar-s" aria-hidden="true">AM</span>
  <span class="avatar avatar-s" aria-hidden="true">SF</span>
  <span class="avatar avatar-s" aria-hidden="true">BP</span>
  <span class="avatar avatar-s avatar-more" aria-hidden="true">+2</span>
</span>
CSS
.avatar {
  display: inline-grid;
  place-items: center;
  flex: none;
  width: 2.5rem;
  height: 2.5rem;
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--text-muted);
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: 50%;
  user-select: none;
}

.avatar-s {
  width: 1.75rem;
  height: 1.75rem;
  font-size: 0.625rem;
}

.avatar-group {
  display: inline-flex;
}

/* The ring is the page colour, so overlaps read as a cut, not a border. */
.avatar-group .avatar {
  box-shadow: 0 0 0 2px var(--surface);
}

.avatar-group .avatar + .avatar {
  margin-left: calc(-1 * var(--space-1));
}

.avatar-more {
  color: var(--text-faint);
  background: var(--surface);
}

Surfaces

4 elements

Dialog

surfaces/dialog

Builds on Button.

Delete “Night Bus”?

The song and its 14 clips are removed from this project. Samples on the card stay where they are.

Code
HTML
<button class="btn" type="button" commandfor="dialog-demo" command="show-modal">
  Delete song
</button>
<dialog class="dialog" id="dialog-demo" aria-labelledby="dialog-demo-title" closedby="any">
  <h2 class="dialog-title" id="dialog-demo-title">Delete “Night Bus”?</h2>
  <p class="dialog-body">
    The song and its 14 clips are removed from this project. Samples on the
    card stay where they are.
  </p>
  <div class="dialog-actions">
    <button class="btn btn-ghost" type="button" commandfor="dialog-demo" command="close">
      Cancel
    </button>
    <button class="btn btn-primary" type="button" commandfor="dialog-demo" command="close">
      Delete
    </button>
  </div>
</dialog>
CSS
/* Native <dialog> opened with invoker commands: focus trap, Escape, inert
   page and top layer with no script. Builds on button.css. */

.dialog {
  width: min(100% - 2 * var(--space-4), 28rem);
  padding: var(--space-5);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-m);
  box-shadow: var(--shadow-raise);
  transition:
    opacity var(--dur) var(--ease),
    translate var(--dur) var(--ease),
    overlay var(--dur) allow-discrete,
    display var(--dur) allow-discrete;
  opacity: 0;
  translate: 0 var(--space-2);
}

.dialog[open] {
  opacity: 1;
  translate: none;

  @starting-style {
    opacity: 0;
    translate: 0 var(--space-2);
  }
}

.dialog::backdrop {
  background: color-mix(in oklab, var(--text) 35%, transparent);
}

.dialog-title {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--text-lg);
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
}

.dialog-body {
  margin: var(--space-2) 0 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  margin-top: var(--space-5);
}

@media (prefers-reduced-motion: reduce) {
  .dialog {
    transition: none;
  }
}

Tooltip

surfaces/tooltip

Builds on Button.

Code
HTML
<button class="btn tip" type="button" data-tip="Copies the HTML only">Copy</button>
<a class="tip" href="#surfaces-tooltip" data-tip="Updated 2 days ago">Release notes</a>
CSS
/* CSS-only, for supplementary hints. The tip is decoration for sighted
   pointer and keyboard users; anything a screen reader needs belongs in the
   control's own text or aria-label. Shown on focus too, and always shown
   above its trigger. */

.tip {
  position: relative;
}

.tip::after {
  content: attr(data-tip);
  position: absolute;
  bottom: calc(100% + var(--space-2));
  left: 50%;
  z-index: 20;
  padding: var(--space-1) var(--space-2);
  font: 500 var(--text-xs) / 1.4 var(--font-body);
  white-space: nowrap;
  color: var(--bg);
  background: var(--text);
  border-radius: min(var(--radius-s), 4px);
  translate: -50% var(--space-1);
  opacity: 0;
  pointer-events: none;
  transition:
    opacity var(--dur-fast) var(--ease),
    translate var(--dur-fast) var(--ease);
}

.tip:hover::after,
.tip:focus-visible::after {
  opacity: 1;
  translate: -50% 0;
  transition-delay: 300ms;
}

@media (prefers-reduced-motion: reduce) {
  .tip::after {
    transition: none;
  }
}

Accordion

surfaces/accordion
Can I return tickets?

Up to 48 hours before curtain, for credit toward another performance.

Is there a dress code?

No. Bring a layer; theatres run cold in summer.

When do doors open?

Thirty minutes before the listed start time.

Code
HTML
<div class="accordion">
  <details name="faq" open>
    <summary>Can I return tickets?</summary>
    <p>Up to 48 hours before curtain, for credit toward another performance.</p>
  </details>
  <details name="faq">
    <summary>Is there a dress code?</summary>
    <p>No. Bring a layer; theatres run cold in summer.</p>
  </details>
  <details name="faq">
    <summary>When do doors open?</summary>
    <p>Thirty minutes before the listed start time.</p>
  </details>
</div>
CSS
/* <details name="..."> makes the group exclusive with no script. */

.accordion {
  width: 100%;
  border-top: 1px solid var(--line);
}

.accordion details {
  border-bottom: 1px solid var(--line);
}

.accordion summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-3) 0;
  font-weight: 500;
  list-style: none;
  cursor: pointer;
}

.accordion summary::-webkit-details-marker {
  display: none;
}

.accordion summary::after {
  content: "";
  flex: none;
  width: 0.45rem;
  height: 0.45rem;
  border-right: 1.5px solid var(--text-muted);
  border-bottom: 1.5px solid var(--text-muted);
  rotate: 45deg;
  translate: 0 -25%;
  transition: rotate var(--dur) var(--ease);
}

.accordion details[open] summary::after {
  rotate: 225deg;
  translate: 0 25%;
}

.accordion summary:hover {
  color: var(--text-muted);
}

.accordion p {
  margin: 0;
  padding: 0 0 var(--space-4);
  max-width: 40rem;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

@media (prefers-reduced-motion: reduce) {
  .accordion summary::after {
    transition: none;
  }
}

List row

surfaces/list-row
  • Night Bus 124 BPM · C minor · edited 2h ago 3:42
  • Glasshouse 98 BPM · F major · edited yesterday 5:10
  • Sketch, untitled Archived 0:48
Code
HTML
<ul class="rows">
  <li class="row">
    <span class="row-mark" aria-hidden="true">NB</span>
    <span class="row-main">
      <span class="row-title">Night Bus</span>
      <span class="row-meta">124 BPM · C minor · edited 2h ago</span>
    </span>
    <span class="row-value">3:42</span>
    <span class="row-actions">
      <button class="row-action" type="button" aria-label="Rename Night Bus" title="Rename">
        <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M10.5 2.5l3 3L5 14H2v-3z" /></svg>
      </button>
      <button class="row-action" type="button" aria-label="Delete Night Bus" title="Delete">
        <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2.5 4h11M6 4V2.5h4V4M4 4l.8 9.5h6.4L12 4" /></svg>
      </button>
    </span>
  </li>
  <li class="row">
    <span class="row-mark" aria-hidden="true">GH</span>
    <span class="row-main">
      <span class="row-title">Glasshouse</span>
      <span class="row-meta">98 BPM · F major · edited yesterday</span>
    </span>
    <span class="row-value">5:10</span>
    <span class="row-actions">
      <button class="row-action" type="button" aria-label="Rename Glasshouse" title="Rename">
        <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M10.5 2.5l3 3L5 14H2v-3z" /></svg>
      </button>
      <button class="row-action" type="button" aria-label="Delete Glasshouse" title="Delete">
        <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2.5 4h11M6 4V2.5h4V4M4 4l.8 9.5h6.4L12 4" /></svg>
      </button>
    </span>
  </li>
  <li class="row row-dim">
    <span class="row-mark" aria-hidden="true">SW</span>
    <span class="row-main">
      <span class="row-title">Sketch, untitled</span>
      <span class="row-meta">Archived</span>
    </span>
    <span class="row-value">0:48</span>
    <span class="row-actions">
      <button class="row-action" type="button" aria-label="Restore sketch" title="Restore">
        <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 8a5 5 0 1 0 1.5-3.5M3 2.5V5h2.5" /></svg>
      </button>
    </span>
  </li>
</ul>
CSS
/* Each row is the edge for the next. Actions are secondary, so they appear
   on hover or keyboard focus, and are always shown on touch screens. */

.rows {
  width: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
}

.row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 3.5rem;
  padding: var(--space-2) var(--space-2);
  border-bottom: 1px solid var(--line);
}

.row:hover {
  background: var(--sunken);
}

.row-mark {
  display: grid;
  place-items: center;
  flex: none;
  width: 2.25rem;
  height: 2.25rem;
  font: 500 var(--text-xs) var(--font-mono);
  color: var(--text-muted);
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: min(var(--radius-s), 6px);
}

.row-main {
  display: grid;
  flex: 1;
  min-width: 0;
}

.row-title {
  overflow: hidden;
  font-size: var(--text-sm);
  font-weight: 500;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.row-meta {
  overflow: hidden;
  font-size: var(--text-xs);
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--text-muted);
}

.row-value {
  font: var(--text-sm) var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

/* Fixed width so rows with fewer actions keep the value column aligned. */
.row-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-1);
  width: calc(4rem + var(--space-1));
  opacity: 0;
}

.row:hover .row-actions,
.row:focus-within .row-actions {
  opacity: 1;
}

@media (hover: none) {
  .row-actions {
    opacity: 1;
  }
}

.row-action {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  color: var(--text-muted);
  background: none;
  border: 0;
  border-radius: min(var(--radius-control), var(--radius-m));
  cursor: pointer;
}

.row-action:hover {
  color: var(--text);
  background: var(--surface);
}

.row-action svg {
  width: 0.9rem;
  height: 0.9rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.row-dim .row-title,
.row-dim .row-value {
  color: var(--text-faint);
}

Text

3 elements

Keyboard key

text/kbd

Press / to search, or Ctrl K for the command menu.

Code
HTML
<p>
  Press <kbd class="kbd">/</kbd> to search, or
  <kbd class="kbd">Ctrl</kbd> <kbd class="kbd">K</kbd> for the command menu.
</p>
CSS
.kbd {
  display: inline-block;
  min-width: 1.6em;
  padding: 0 0.4em;
  font: 500 0.8em / 1.6 var(--font-mono);
  text-align: center;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-bottom-width: 2px;
  border-radius: min(var(--radius-s), 4px);
}

Prose

text/prose

Exporting a song

Songs save as XML in the SONGS folder. Each file carries its own tempo and key, so the stats page can sort by either.

  • Kits and synths are stored by reference.
  • Samples stay where they are on the card.
Renaming a sample folder breaks every song that points into it.
Code
HTML
<div class="prose">
  <h3>Exporting a song</h3>
  <p>
    Songs save as XML in the <code>SONGS</code> folder. Each file carries its
    own tempo and key, so the stats page can sort by either.
  </p>
  <ul>
    <li>Kits and synths are stored by reference.</li>
    <li>Samples stay where they are on the card.</li>
  </ul>
  <blockquote>
    Renaming a sample folder breaks every song that points into it.
  </blockquote>
</div>
CSS
/* Long-form text. Blockquotes are used as asides, so no quote marks or
   italics. */

.prose {
  max-width: 38rem;
  line-height: 1.65;
}

.prose > * + * {
  margin-top: var(--space-4);
}

.prose h3 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  font-stretch: var(--display-stretch, normal);
  letter-spacing: var(--display-tracking);
  text-transform: var(--display-case, none);
  font-size: var(--text-xl);
  line-height: 1.15;
}

.prose p,
.prose li {
  text-wrap: pretty;
}

.prose ul {
  padding-left: var(--space-5);
}

.prose li + li {
  margin-top: var(--space-1);
}

.prose li::marker {
  color: var(--text-faint);
}

.prose code {
  padding: 0.1em 0.35em;
  font: 500 0.875em var(--font-mono);
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: min(var(--radius-s), 4px);
}

.prose blockquote {
  margin-left: 0;
  margin-right: 0;
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-sm);
  color: var(--text-muted);
  background: var(--sunken);
  border-radius: var(--radius-m);
}