Token specimen
Version 1.6.0. Every semantic token in both modes. The theme switch and width toggle are the shipped controls.js.
Use it
Pick one way to load the tokens. Most sites want the CDN link.
CDN link
<link rel="stylesheet" href="https://design.manjunathhk.in/v1/index.css">
npm CSS import
@import "@manjunathhk/design-tokens/index.css";
Theme override
<html data-theme="dark"></html>
Colour swatches and contrast ratios
Swatches show the emitted value. Each mode lists every foreground and background pair the contrast contract checks.
Dark swatches
Light contrast ratios32 pairs pass · lowest 4.40
| FG token | FG value | BG token | BG value | Ratio | Rule | Result |
|---|---|---|---|---|---|---|
| color.text | #1C1917 |
color.bg | #FAFAF9 |
16.74 | ≥ 4.5 | ✓ pass |
| color.text | #1C1917 |
color.bg-subtle | #F5F5F4 |
16.03 | ≥ 4.5 | ✓ pass |
| color.text | #1C1917 |
color.surface | #FFFFFF |
17.49 | ≥ 4.5 | ✓ pass |
| color.text-secondary | #44403C |
color.bg | #FAFAF9 |
9.84 | ≥ 4.5 | ✓ pass |
| color.text-secondary | #44403C |
color.bg-subtle | #F5F5F4 |
9.42 | ≥ 4.5 | ✓ pass |
| color.text-secondary | #44403C |
color.surface | #FFFFFF |
10.27 | ≥ 4.5 | ✓ pass |
| color.text-muted | #57534E |
color.bg | #FAFAF9 |
7.30 | ≥ 4.5 | ✓ pass |
| color.text-muted | #57534E |
color.bg-subtle | #F5F5F4 |
6.99 | ≥ 4.5 | ✓ pass |
| color.text-muted | #57534E |
color.surface | #FFFFFF |
7.63 | ≥ 4.5 | ✓ pass |
| color.danger | #B91C1C |
color.bg | #FAFAF9 |
6.19 | ≥ 4.5 | ✓ pass |
| color.danger | #B91C1C |
color.bg-subtle | #F5F5F4 |
5.93 | ≥ 4.5 | ✓ pass |
| color.danger | #B91C1C |
color.surface | #FFFFFF |
6.47 | ≥ 4.5 | ✓ pass |
| color.success | #15803D |
color.bg | #FAFAF9 |
4.80 | ≥ 4.5 | ✓ pass |
| color.success | #15803D |
color.bg-subtle | #F5F5F4 |
4.60 | ≥ 4.5 | ✓ pass |
| color.success | #15803D |
color.surface | #FFFFFF |
5.02 | ≥ 4.5 | ✓ pass |
| color.warning | #A16207 |
color.bg | #FAFAF9 |
4.71 | ≥ 4.5 | ✓ pass |
| color.warning | #A16207 |
color.bg-subtle | #F5F5F4 |
4.51 | ≥ 4.5 | ✓ pass |
| color.warning | #A16207 |
color.surface | #FFFFFF |
4.92 | ≥ 4.5 | ✓ pass |
| color.info | #1D4ED8 |
color.bg | #FAFAF9 |
6.42 | ≥ 4.5 | ✓ pass |
| color.info | #1D4ED8 |
color.bg-subtle | #F5F5F4 |
6.14 | ≥ 4.5 | ✓ pass |
| color.info | #1D4ED8 |
color.surface | #FFFFFF |
6.70 | ≥ 4.5 | ✓ pass |
| color.accent | #B45309 |
color.bg | #FAFAF9 |
4.81 | ≥ 4.5 | ✓ pass |
| color.accent | #B45309 |
color.surface | #FFFFFF |
5.02 | ≥ 4.5 | ✓ pass |
| color.on-accent | #FFFFFF |
color.accent | #B45309 |
5.02 | ≥ 4.5 | ✓ pass |
| color.on-accent | #FFFFFF |
color.accent-hover | #92400E |
7.09 | ≥ 4.5 | ✓ pass |
| color.on-danger | #FFFFFF |
color.danger | #B91C1C |
6.47 | ≥ 4.5 | ✓ pass |
| color.border-control | #78716C |
color.bg | #FAFAF9 |
4.59 | ≥ 3 | ✓ pass |
| color.border-control | #78716C |
color.bg-subtle | #F5F5F4 |
4.40 | ≥ 3 | ✓ pass |
| color.border-control | #78716C |
color.surface | #FFFFFF |
4.80 | ≥ 3 | ✓ pass |
| color.focus-ring | #B45309 |
color.bg | #FAFAF9 |
4.81 | ≥ 3 | ✓ pass |
| color.focus-ring | #B45309 |
color.bg-subtle | #F5F5F4 |
4.60 | ≥ 3 | ✓ pass |
| color.focus-ring | #B45309 |
color.surface | #FFFFFF |
5.02 | ≥ 3 | ✓ pass |
Dark contrast ratios32 pairs pass · lowest 3.16
| FG token | FG value | BG token | BG value | Ratio | Rule | Result |
|---|---|---|---|---|---|---|
| color.text | #FAFAF9 |
color.bg | #0C0A09 |
18.92 | ≥ 4.5 | ✓ pass |
| color.text | #FAFAF9 |
color.bg-subtle | #1C1917 |
16.74 | ≥ 4.5 | ✓ pass |
| color.text | #FAFAF9 |
color.surface | #292524 |
14.52 | ≥ 4.5 | ✓ pass |
| color.text-secondary | #D6D3D1 |
color.bg | #0C0A09 |
13.26 | ≥ 4.5 | ✓ pass |
| color.text-secondary | #D6D3D1 |
color.bg-subtle | #1C1917 |
11.74 | ≥ 4.5 | ✓ pass |
| color.text-secondary | #D6D3D1 |
color.surface | #292524 |
10.18 | ≥ 4.5 | ✓ pass |
| color.text-muted | #A8A29E |
color.bg | #0C0A09 |
7.83 | ≥ 4.5 | ✓ pass |
| color.text-muted | #A8A29E |
color.bg-subtle | #1C1917 |
6.93 | ≥ 4.5 | ✓ pass |
| color.text-muted | #A8A29E |
color.surface | #292524 |
6.01 | ≥ 4.5 | ✓ pass |
| color.danger | #F87171 |
color.bg | #0C0A09 |
7.14 | ≥ 4.5 | ✓ pass |
| color.danger | #F87171 |
color.bg-subtle | #1C1917 |
6.32 | ≥ 4.5 | ✓ pass |
| color.danger | #F87171 |
color.surface | #292524 |
5.48 | ≥ 4.5 | ✓ pass |
| color.success | #4ADE80 |
color.bg | #0C0A09 |
11.34 | ≥ 4.5 | ✓ pass |
| color.success | #4ADE80 |
color.bg-subtle | #1C1917 |
10.04 | ≥ 4.5 | ✓ pass |
| color.success | #4ADE80 |
color.surface | #292524 |
8.71 | ≥ 4.5 | ✓ pass |
| color.warning | #FB923C |
color.bg | #0C0A09 |
8.73 | ≥ 4.5 | ✓ pass |
| color.warning | #FB923C |
color.bg-subtle | #1C1917 |
7.73 | ≥ 4.5 | ✓ pass |
| color.warning | #FB923C |
color.surface | #292524 |
6.70 | ≥ 4.5 | ✓ pass |
| color.info | #60A5FA |
color.bg | #0C0A09 |
7.77 | ≥ 4.5 | ✓ pass |
| color.info | #60A5FA |
color.bg-subtle | #1C1917 |
6.88 | ≥ 4.5 | ✓ pass |
| color.info | #60A5FA |
color.surface | #292524 |
5.97 | ≥ 4.5 | ✓ pass |
| color.accent | #FBBF24 |
color.bg | #0C0A09 |
11.83 | ≥ 4.5 | ✓ pass |
| color.accent | #FBBF24 |
color.surface | #292524 |
9.09 | ≥ 4.5 | ✓ pass |
| color.on-accent | #1C1917 |
color.accent | #FBBF24 |
10.48 | ≥ 4.5 | ✓ pass |
| color.on-accent | #1C1917 |
color.accent-hover | #FCD34D |
12.13 | ≥ 4.5 | ✓ pass |
| color.on-danger | #1C1917 |
color.danger | #F87171 |
6.32 | ≥ 4.5 | ✓ pass |
| color.border-control | #78716C |
color.bg | #0C0A09 |
4.12 | ≥ 3 | ✓ pass |
| color.border-control | #78716C |
color.bg-subtle | #1C1917 |
3.65 | ≥ 3 | ✓ pass |
| color.border-control | #78716C |
color.surface | #292524 |
3.16 | ≥ 3 | ✓ pass |
| color.focus-ring | #FBBF24 |
color.bg | #0C0A09 |
11.83 | ≥ 3 | ✓ pass |
| color.focus-ring | #FBBF24 |
color.bg-subtle | #1C1917 |
10.48 | ≥ 3 | ✓ pass |
| color.focus-ring | #FBBF24 |
color.surface | #292524 |
9.09 | ≥ 3 | ✓ pass |
In use
Tokens working together. Each preview is its own page holding only the token rules, base.css and the snippets under it, so it shows what a site gets from index.css plus those snippets.
Copy the HTML and the group CSS into a site and rename the ex- classes to suit it. Copied code belongs to that site: it is not part of the versioned package and carries no compatibility promise (D53).
Actions
Buttons and links that look like buttons. One primary action per view.
Tokens used --mk-spacing-2 --mk-spacing-4 --mk-color-border-control --mk-radius-md --mk-color-surface --mk-color-text --mk-font-size-sm --mk-font-weight-semibold --mk-font-line-height-tight --mk-motion-duration-fast --mk-motion-ease --mk-color-accent-subtle --mk-color-accent --mk-color-on-accent --mk-color-accent-hover --mk-color-danger --mk-color-on-danger --mk-color-bg-subtle --mk-color-text-muted --mk-spacing-3
Actions CSS
/* Buttons: one base class plus a variant. Hover, focus and active come from
the same rules; the focus outline comes from base.css. */
.ex-button {
display: inline-flex;
gap: var(--mk-spacing-2);
align-items: center;
justify-content: center;
min-height: 2.5rem;
padding: var(--mk-spacing-2) var(--mk-spacing-4);
border: 1px solid var(--mk-color-border-control);
border-radius: var(--mk-radius-md);
background-color: var(--mk-color-surface);
color: var(--mk-color-text);
font-size: var(--mk-font-size-sm);
font-weight: var(--mk-font-weight-semibold);
line-height: var(--mk-font-line-height-tight);
text-decoration: none;
cursor: pointer;
transition:
background-color var(--mk-motion-duration-fast) var(--mk-motion-ease),
box-shadow var(--mk-motion-duration-fast) var(--mk-motion-ease),
transform var(--mk-motion-duration-fast) var(--mk-motion-ease);
}
.ex-button:hover {
background-color: var(--mk-color-accent-subtle);
}
.ex-button:active {
transform: translateY(1px);
}
/* Primary: the one main action on a view. */
.ex-button-primary {
border-color: var(--mk-color-accent);
background-color: var(--mk-color-accent);
color: var(--mk-color-on-accent);
}
.ex-button-primary:hover {
border-color: var(--mk-color-accent-hover);
background-color: var(--mk-color-accent-hover);
}
/* Quiet: low-emphasis actions next to a primary one. */
.ex-button-quiet {
border-color: transparent;
background-color: transparent;
color: var(--mk-color-accent);
}
/* accent on accent-subtle is 3.99:1 in light mode, below 4.5:1, so the label
switches to the text colour on hover. */
.ex-button-quiet:hover {
color: var(--mk-color-text);
}
/* Destructive, outlined: a lower-emphasis delete next to other actions. */
.ex-button-danger {
border-color: var(--mk-color-danger);
color: var(--mk-color-danger);
}
/* The fill would compete with the danger colour, so hover thickens the edge. */
.ex-button-danger:hover {
background-color: var(--mk-color-surface);
box-shadow: inset 0 0 0 1px var(--mk-color-danger);
}
/* Destructive, filled: the confirming action for something that cannot be
undone. No danger-hover token exists, so hover underlines the label. */
.ex-button-danger-solid {
border-color: var(--mk-color-danger);
background-color: var(--mk-color-danger);
color: var(--mk-color-on-danger);
}
.ex-button-danger-solid:hover {
background-color: var(--mk-color-danger);
text-decoration: underline;
}
/* Disabled: no disabled colour tokens exist yet, so it is drawn with the
subtle background, muted text and a dashed outline. */
.ex-button:disabled,
.ex-button[aria-disabled="true"] {
border-style: dashed;
border-color: var(--mk-color-border-control);
background-color: var(--mk-color-bg-subtle);
color: var(--mk-color-text-muted);
box-shadow: none;
transform: none;
cursor: not-allowed;
}
/* Icons inherit the label colour. */
.ex-button svg {
width: 1.125rem;
height: 1.125rem;
flex: none;
}
.ex-button-icon {
padding: var(--mk-spacing-2);
min-width: 2.5rem;
}
.ex-button-row {
display: flex;
flex-wrap: wrap;
gap: var(--mk-spacing-3);
align-items: center;
}
Button variants
Primary, secondary, quiet, and destructive outlined and filled. Forced below it: hover, keyboard focus, pressed.
Button variants HTML
<div class="ex-button-row"> <button type="button" class="ex-button ex-button-primary">Save changes</button> <button type="button" class="ex-button">Preview</button> <button type="button" class="ex-button ex-button-quiet">Cancel</button> <button type="button" class="ex-button ex-button-danger">Delete</button> <button type="button" class="ex-button ex-button-danger-solid">Delete forever</button> </div>
Buttons with icons
Icons take the label colour. An icon-only button needs an aria-label. Forced below it: hover, keyboard focus, pressed.
Buttons with icons HTML
<div class="ex-button-row">
<button type="button" class="ex-button ex-button-primary">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M12 5v14M5 12h14" />
</svg>
New post
</button>
<button type="button" class="ex-button">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M12 3v12M7 10l5 5 5-5M5 21h14" />
</svg>
Download
</button>
<button type="button" class="ex-button ex-button-icon" aria-label="Settings">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="3" />
<path
d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z"
/>
</svg>
</button>
</div>
Links styled as buttons
Use a link when the action navigates, a button when it does something. Forced below it: hover, keyboard focus, pressed.
Links styled as buttons HTML
<div class="ex-button-row"> <a class="ex-button ex-button-primary" href="#get-started">Get started</a> <a class="ex-button" href="#docs">Read the docs</a> <a class="ex-button ex-button-quiet" href="#changelog">Changelog</a> </div>
Disabled buttons
Prefer explaining why an action is unavailable over disabling it silently.
Disabled buttons HTML
<div class="ex-button-row"> <button type="button" class="ex-button ex-button-primary" disabled>Save changes</button> <button type="button" class="ex-button" disabled>Preview</button> <button type="button" class="ex-button ex-button-quiet" disabled>Cancel</button> <button type="button" class="ex-button ex-button-danger" disabled>Delete</button> <button type="button" class="ex-button ex-button-danger-solid" disabled>Delete forever</button> </div>
Forms
Labels above controls, help and messages below. Controls use border-control, which meets 3:1.
Tokens used --mk-spacing-1 --mk-spacing-5 --mk-color-text --mk-font-size-sm --mk-font-weight-semibold --mk-color-text-muted --mk-font-weight-regular --mk-spacing-2 --mk-spacing-3 --mk-color-border-control --mk-radius-md --mk-color-surface --mk-font-size-base --mk-font-line-height-normal --mk-motion-duration-fast --mk-motion-ease --mk-color-focus-ring --mk-color-danger --mk-color-success --mk-font-weight-medium --mk-color-bg-subtle --mk-spacing-4 --mk-color-border --mk-color-accent --mk-color-text-secondary
Forms CSS
/* A field is a label, a control and optional help or message text, stacked. */
.ex-field {
display: grid;
gap: var(--mk-spacing-1);
max-width: 28rem;
}
.ex-form {
display: grid;
gap: var(--mk-spacing-5);
}
.ex-label {
color: var(--mk-color-text);
font-size: var(--mk-font-size-sm);
font-weight: var(--mk-font-weight-semibold);
}
.ex-optional {
color: var(--mk-color-text-muted);
font-weight: var(--mk-font-weight-regular);
}
.ex-help {
color: var(--mk-color-text-muted);
font-size: var(--mk-font-size-sm);
}
/* Text-like controls share one look: surface fill, 3:1 control border. */
.ex-input,
.ex-select,
.ex-textarea {
width: 100%;
min-height: 2.5rem;
padding: var(--mk-spacing-2) var(--mk-spacing-3);
border: 1px solid var(--mk-color-border-control);
border-radius: var(--mk-radius-md);
background-color: var(--mk-color-surface);
color: var(--mk-color-text);
font-size: var(--mk-font-size-base);
line-height: var(--mk-font-line-height-normal);
transition: border-color var(--mk-motion-duration-fast) var(--mk-motion-ease);
}
.ex-textarea {
min-height: 6rem;
resize: vertical;
}
.ex-input::placeholder,
.ex-textarea::placeholder {
color: var(--mk-color-text-muted);
opacity: 1;
}
/* base.css draws the focus outline; the border follows it. */
.ex-input:focus-visible,
.ex-select:focus-visible,
.ex-textarea:focus-visible {
border-color: var(--mk-color-focus-ring);
}
/* Validation: the colour is never the only signal; the message says it too. */
.ex-input[aria-invalid="true"],
.ex-textarea[aria-invalid="true"] {
border-color: var(--mk-color-danger);
}
.ex-input.ex-is-valid {
border-color: var(--mk-color-success);
}
.ex-message {
display: flex;
gap: var(--mk-spacing-1);
align-items: center;
font-size: var(--mk-font-size-sm);
font-weight: var(--mk-font-weight-medium);
}
.ex-message svg {
width: 1rem;
height: 1rem;
flex: none;
}
.ex-message-error {
color: var(--mk-color-danger);
}
.ex-message-success {
color: var(--mk-color-success);
}
/* Disabled: drawn with existing tokens (see "Not yet expressible"). */
.ex-input:disabled,
.ex-select:disabled,
.ex-textarea:disabled {
border-style: dashed;
background-color: var(--mk-color-bg-subtle);
color: var(--mk-color-text-muted);
cursor: not-allowed;
}
/* Checkboxes, radios and ranges keep their native drawing; accent-color
tints them, and the browser picks a contrasting check mark. */
.ex-fieldset {
display: grid;
gap: var(--mk-spacing-2);
margin: 0;
padding: var(--mk-spacing-4);
border: 1px solid var(--mk-color-border);
border-radius: var(--mk-radius-md);
}
.ex-fieldset legend {
padding: 0 var(--mk-spacing-1);
font-size: var(--mk-font-size-sm);
font-weight: var(--mk-font-weight-semibold);
}
.ex-choice {
display: flex;
gap: var(--mk-spacing-2);
align-items: center;
font-size: var(--mk-font-size-base);
cursor: pointer;
}
.ex-choice input,
.ex-range {
margin: 0;
accent-color: var(--mk-color-accent);
}
.ex-choice input {
width: 1.125rem;
height: 1.125rem;
}
.ex-range {
width: 100%;
}
.ex-output {
color: var(--mk-color-text-secondary);
font-size: var(--mk-font-size-sm);
font-variant-numeric: tabular-nums;
}
Text fields
Label, input, optional marker and help text tied with aria-describedby. Forced below it: keyboard focus.
Text fields HTML
<div class="ex-form">
<div class="ex-field">
<label class="ex-label" for="ex-name">Full name</label>
<input class="ex-input" id="ex-name" name="name" autocomplete="name" />
</div>
<div class="ex-field">
<label class="ex-label" for="ex-email">Email</label>
<input
class="ex-input"
id="ex-email"
name="email"
type="email"
placeholder="you@example.com"
aria-describedby="ex-email-help"
/>
<p class="ex-help" id="ex-email-help">Only used to reply to you.</p>
</div>
<div class="ex-field">
<label class="ex-label" for="ex-site"
>Website <span class="ex-optional">(optional)</span></label
>
<input class="ex-input" id="ex-site" name="site" type="url" placeholder="https://" />
</div>
</div>
Validation messages
The message carries the meaning; the border colour only reinforces it.
Validation messages HTML
<div class="ex-form">
<div class="ex-field">
<label class="ex-label" for="ex-username">Username</label>
<input
class="ex-input"
id="ex-username"
value="mk"
aria-invalid="true"
aria-describedby="ex-username-error"
/>
<p class="ex-message ex-message-error" id="ex-username-error">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="9" />
<path d="M12 7v6M12 17h.01" />
</svg>
Use at least 3 characters.
</p>
</div>
<div class="ex-field">
<label class="ex-label" for="ex-handle">Handle</label>
<input
class="ex-input ex-is-valid"
id="ex-handle"
value="manjunathhk"
aria-describedby="ex-handle-ok"
/>
<p class="ex-message ex-message-success" id="ex-handle-ok">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M5 12l5 5 9-10" />
</svg>
Available.
</p>
</div>
</div>
Select and textarea
Native controls, styled like text fields. Forced below it: keyboard focus.
Select and textarea HTML
<div class="ex-form">
<div class="ex-field">
<label class="ex-label" for="ex-topic">Topic</label>
<select class="ex-select" id="ex-topic">
<option>General question</option>
<option>Project enquiry</option>
<option>Speaking</option>
</select>
</div>
<div class="ex-field">
<label class="ex-label" for="ex-message">Message</label>
<textarea
class="ex-textarea"
id="ex-message"
placeholder="What would you like to talk about?"
></textarea>
</div>
</div>
Checkboxes and radios
Native controls tinted with accent-color, grouped in a fieldset with a legend. Forced below it: keyboard focus.
Checkboxes and radios HTML
<div class="ex-form">
<fieldset class="ex-fieldset">
<legend>Notify me about</legend>
<label class="ex-choice"><input type="checkbox" checked /> New posts</label>
<label class="ex-choice"><input type="checkbox" /> Talks and events</label>
</fieldset>
<fieldset class="ex-fieldset">
<legend>Frequency</legend>
<label class="ex-choice"
><input type="radio" name="ex-frequency" checked /> Weekly digest</label
>
<label class="ex-choice"><input type="radio" name="ex-frequency" /> Every post</label>
</fieldset>
</div>
Range
A native slider with its value in an output element. Forced below it: keyboard focus.
Range HTML
<div class="ex-field">
<label class="ex-label" for="ex-speed">Reading speed</label>
<input
class="ex-range"
id="ex-speed"
type="range"
min="0"
max="100"
value="60"
aria-describedby="ex-speed-value"
/>
<output class="ex-output" id="ex-speed-value" for="ex-speed">60 percent</output>
</div>
Disabled fields
Drawn with existing tokens; see Not yet expressible.
Disabled fields HTML
<div class="ex-form">
<div class="ex-field">
<label class="ex-label" for="ex-plan">Plan</label>
<input class="ex-input" id="ex-plan" value="Personal" disabled />
<p class="ex-help">Plans change at renewal.</p>
</div>
<div class="ex-field">
<label class="ex-label" for="ex-region">Region</label>
<select class="ex-select" id="ex-region" disabled>
<option>Europe</option>
</select>
</div>
</div>
Feedback
Status colours always come with an icon or words, never colour alone.
Tokens used --mk-spacing-1 --mk-spacing-3 --mk-spacing-4 --mk-color-border --mk-radius-md --mk-color-surface --mk-font-weight-semibold --mk-color-text --mk-font-size-sm --mk-color-info --mk-color-success --mk-color-warning --mk-color-danger --mk-spacing-2 --mk-radius-pill --mk-font-size-xs --mk-font-letter-spacing-label --mk-font-line-height-normal --mk-color-border-control --mk-color-text-secondary --mk-color-accent-subtle --mk-z-index-toast --mk-radius-lg --mk-shadow-raised --mk-radius-sm --mk-color-text-muted --mk-font-weight-regular --mk-color-accent --mk-spacing-10 --mk-spacing-6 --mk-color-bg-subtle --mk-font-size-lg --mk-motion-duration-slower --mk-motion-ease
Feedback CSS
/* Alerts: surface fill with a status-coloured edge, icon and title. No
status tint tokens exist yet (see "Not yet expressible"). */
.ex-alert {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: var(--mk-spacing-1) var(--mk-spacing-3);
padding: var(--mk-spacing-3) var(--mk-spacing-4);
border: 1px solid var(--mk-color-border);
border-left: 4px solid currentcolor;
border-radius: var(--mk-radius-md);
background-color: var(--mk-color-surface);
}
.ex-alert svg {
grid-row: span 2;
width: 1.25rem;
height: 1.25rem;
margin-top: 0.125rem;
}
.ex-alert-title {
font-weight: var(--mk-font-weight-semibold);
}
.ex-alert-body {
grid-column: 2;
color: var(--mk-color-text);
font-size: var(--mk-font-size-sm);
}
.ex-alert-info {
color: var(--mk-color-info);
}
.ex-alert-success {
color: var(--mk-color-success);
}
.ex-alert-warning {
color: var(--mk-color-warning);
}
.ex-alert-danger {
color: var(--mk-color-danger);
}
.ex-stack {
display: grid;
gap: var(--mk-spacing-3);
}
/* Badges: outlined text in the status colour, which meets 4.5:1 on every
background. Filled badges need on-status tokens that do not exist yet. */
.ex-badge-row {
display: flex;
flex-wrap: wrap;
gap: var(--mk-spacing-2);
align-items: center;
}
.ex-badge {
display: inline-flex;
gap: var(--mk-spacing-1);
align-items: center;
padding: 0.125rem var(--mk-spacing-2);
border: 1px solid currentcolor;
border-radius: var(--mk-radius-pill);
font-size: var(--mk-font-size-xs);
font-weight: var(--mk-font-weight-semibold);
letter-spacing: var(--mk-font-letter-spacing-label);
line-height: var(--mk-font-line-height-normal);
white-space: nowrap;
}
.ex-badge-neutral {
border-color: var(--mk-color-border-control);
color: var(--mk-color-text-secondary);
}
.ex-badge-accent {
border-color: transparent;
background-color: var(--mk-color-accent-subtle);
color: var(--mk-color-text);
}
.ex-badge-info {
color: var(--mk-color-info);
}
.ex-badge-success {
color: var(--mk-color-success);
}
.ex-badge-warning {
color: var(--mk-color-warning);
}
.ex-badge-danger {
color: var(--mk-color-danger);
}
/* Toast: fixed to the corner, above page content. */
.ex-toast {
position: fixed;
right: var(--mk-spacing-4);
bottom: var(--mk-spacing-4);
z-index: var(--mk-z-index-toast);
display: flex;
gap: var(--mk-spacing-3);
align-items: center;
max-width: min(24rem, calc(100% - 2 * var(--mk-spacing-4)));
padding: var(--mk-spacing-3) var(--mk-spacing-4);
border: 1px solid var(--mk-color-border);
border-radius: var(--mk-radius-lg);
background-color: var(--mk-color-surface);
box-shadow: var(--mk-shadow-raised);
font-size: var(--mk-font-size-sm);
}
.ex-toast-icon {
width: 1.25rem;
height: 1.25rem;
flex: none;
color: var(--mk-color-success);
}
.ex-toast-text {
flex: 1;
}
.ex-toast-close {
display: inline-grid;
place-items: center;
width: 2rem;
height: 2rem;
padding: 0;
border: 0;
border-radius: var(--mk-radius-sm);
background-color: transparent;
color: var(--mk-color-text-muted);
cursor: pointer;
}
.ex-toast-close:hover {
background-color: var(--mk-color-accent-subtle);
color: var(--mk-color-text);
}
.ex-toast-close svg {
width: 1rem;
height: 1rem;
}
/* Progress keeps its native drawing, tinted by accent-color. */
.ex-progress-field {
display: grid;
gap: var(--mk-spacing-1);
max-width: 28rem;
}
.ex-progress-label {
display: flex;
justify-content: space-between;
font-size: var(--mk-font-size-sm);
font-weight: var(--mk-font-weight-semibold);
}
.ex-progress-value {
color: var(--mk-color-text-secondary);
font-weight: var(--mk-font-weight-regular);
font-variant-numeric: tabular-nums;
}
.ex-progress {
width: 100%;
height: var(--mk-spacing-2);
accent-color: var(--mk-color-accent);
}
/* Empty state: centred icon, heading, one sentence and one action. */
.ex-empty {
display: grid;
gap: var(--mk-spacing-3);
justify-items: center;
padding: var(--mk-spacing-10) var(--mk-spacing-6);
border: 1px dashed var(--mk-color-border-control);
border-radius: var(--mk-radius-lg);
background-color: var(--mk-color-bg-subtle);
text-align: center;
}
.ex-empty svg {
width: 2.5rem;
height: 2.5rem;
color: var(--mk-color-text-muted);
}
.ex-empty-title {
font-size: var(--mk-font-size-lg);
}
.ex-empty-text {
color: var(--mk-color-text-secondary);
}
/* Loading: a spinner beside words that say what is loading, and a skeleton
that holds the shape of the content to come. Under reduced motion base.css
stops both animations; the words still carry the state. */
.ex-loading {
display: flex;
gap: var(--mk-spacing-2);
align-items: center;
color: var(--mk-color-text-secondary);
font-size: var(--mk-font-size-sm);
}
.ex-spinner {
width: 1.25rem;
height: 1.25rem;
flex: none;
border: 2px solid var(--mk-color-border);
border-top-color: var(--mk-color-accent);
border-radius: var(--mk-radius-pill);
animation: ex-spin var(--mk-motion-duration-slower) linear infinite;
}
@keyframes ex-spin {
to {
transform: rotate(1turn);
}
}
.ex-skeleton-card {
display: grid;
gap: var(--mk-spacing-2);
max-width: 28rem;
padding: var(--mk-spacing-4);
border: 1px solid var(--mk-color-border);
border-radius: var(--mk-radius-lg);
background-color: var(--mk-color-surface);
}
.ex-skeleton {
display: block;
height: 0.75rem;
border-radius: var(--mk-radius-sm);
background-color: var(--mk-color-border);
animation: ex-pulse calc(2 * var(--mk-motion-duration-slower)) var(--mk-motion-ease) infinite
alternate;
}
.ex-skeleton-eyebrow {
width: 25%;
}
.ex-skeleton-title {
width: 75%;
height: 1.25rem;
margin-bottom: var(--mk-spacing-1);
}
.ex-skeleton-short {
width: 60%;
}
@keyframes ex-pulse {
to {
opacity: 0.4;
}
}
.ex-visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
Alerts
Info, success, warning and danger. Use role=alert only for danger.
Alerts HTML
<div class="ex-stack">
<div class="ex-alert ex-alert-info" role="status">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="9" />
<path d="M12 11v6M12 7h.01" />
</svg>
<p class="ex-alert-title">New version available</p>
<p class="ex-alert-body">Reload the page to get the latest changes.</p>
</div>
<div class="ex-alert ex-alert-success" role="status">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="9" />
<path d="M8 12l3 3 5-6" />
</svg>
<p class="ex-alert-title">Message sent</p>
<p class="ex-alert-body">Thanks for writing. Expect a reply within two days.</p>
</div>
<div class="ex-alert ex-alert-warning" role="status">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M12 3l9.5 17h-19L12 3Z" />
<path d="M12 10v4M12 17h.01" />
</svg>
<p class="ex-alert-title">Draft not saved</p>
<p class="ex-alert-body">You are offline. Changes stay in this tab until you reconnect.</p>
</div>
<div class="ex-alert ex-alert-danger" role="alert">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="9" />
<path d="M9 9l6 6M15 9l-6 6" />
</svg>
<p class="ex-alert-title">Payment failed</p>
<p class="ex-alert-body">The card was declined. Try another card or contact your bank.</p>
</div>
</div>
Badges and tags
Short labels for status or category.
Badges and tags HTML
<div class="ex-badge-row"> <span class="ex-badge ex-badge-neutral">Draft</span> <span class="ex-badge ex-badge-accent">Featured</span> <span class="ex-badge ex-badge-info">Beta</span> <span class="ex-badge ex-badge-success">Published</span> <span class="ex-badge ex-badge-warning">Review due</span> <span class="ex-badge ex-badge-danger">Archived</span> </div>
Toast
Fixed to the bottom corner on z-index.toast; shown here inside its preview.
Toast HTML
<div class="ex-toast" role="status">
<svg
class="ex-toast-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M5 12l5 5 9-10" />
</svg>
<p class="ex-toast-text">Link copied to the clipboard.</p>
<button type="button" class="ex-toast-close" aria-label="Dismiss">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
aria-hidden="true"
>
<path d="M6 6l12 12M18 6L6 18" />
</svg>
</button>
</div>
Progress
Native progress, determinate and indeterminate.
Progress HTML
<div class="ex-stack">
<div class="ex-progress-field">
<div class="ex-progress-label">
<label for="ex-upload">Uploading photos</label>
<span class="ex-progress-value">60%</span>
</div>
<progress class="ex-progress" id="ex-upload" max="100" value="60">60%</progress>
</div>
<div class="ex-progress-field">
<div class="ex-progress-label"><label for="ex-sync">Syncing</label></div>
<progress class="ex-progress" id="ex-sync">Syncing</progress>
</div>
</div>
Loading
A spinner beside words, and a skeleton in the shape of the content. Under reduced motion both stand still; the words carry the state.
Loading HTML
<div class="ex-stack">
<p class="ex-loading" role="status">
<span class="ex-spinner" aria-hidden="true"></span>
Loading articles…
</p>
<div class="ex-skeleton-card" role="status" aria-busy="true">
<span class="ex-visually-hidden">Loading article</span>
<span class="ex-skeleton ex-skeleton-eyebrow" aria-hidden="true"></span>
<span class="ex-skeleton ex-skeleton-title" aria-hidden="true"></span>
<span class="ex-skeleton" aria-hidden="true"></span>
<span class="ex-skeleton ex-skeleton-short" aria-hidden="true"></span>
</div>
</div>
Empty state
Says what is missing and offers the next step. Uses the Actions CSS for its button.
Empty state HTML
<div class="ex-empty">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M4 7h16v12H4zM4 7l2-3h12l2 3M9 12h6" />
</svg>
<h2 class="ex-empty-title">No saved articles yet</h2>
<p class="ex-empty-text">Save an article to read it later, even offline.</p>
<a class="ex-button ex-button-primary" href="#articles">Browse articles</a>
</div>
Content
Cards, long-form text and data tables. base.css already sets reading widths and tabular numerals.
Tokens used --mk-spacing-6 --mk-radius-lg --mk-color-bg-subtle --mk-spacing-4 --mk-spacing-2 --mk-spacing-5 --mk-color-border --mk-color-surface --mk-shadow-raised --mk-color-text-muted --mk-font-size-xs --mk-font-weight-semibold --mk-font-letter-spacing-label --mk-font-size-lg --mk-color-text-secondary --mk-color-border-control --mk-color-accent --mk-font-line-height-relaxed --mk-font-size-2xl --mk-font-letter-spacing-display --mk-font-line-height-tight --mk-font-size-xl --mk-font-size-md --mk-spacing-1 --mk-radius-sm --mk-radius-md --mk-font-size-sm --mk-font-line-height-normal --mk-spacing-3 --mk-color-accent-subtle
Content CSS
/* Cards sit on surface inside a bg-subtle section, raised by shadow.raised. */
.ex-section {
padding: var(--mk-spacing-6);
border-radius: var(--mk-radius-lg);
background-color: var(--mk-color-bg-subtle);
}
.ex-card-grid {
display: grid;
gap: var(--mk-spacing-4);
grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
}
.ex-card {
display: grid;
gap: var(--mk-spacing-2);
align-content: start;
padding: var(--mk-spacing-5);
border: 1px solid var(--mk-color-border);
border-radius: var(--mk-radius-lg);
background-color: var(--mk-color-surface);
box-shadow: var(--mk-shadow-raised);
}
.ex-eyebrow {
color: var(--mk-color-text-muted);
font-size: var(--mk-font-size-xs);
font-weight: var(--mk-font-weight-semibold);
letter-spacing: var(--mk-font-letter-spacing-label);
text-transform: uppercase;
}
.ex-card-title {
font-size: var(--mk-font-size-lg);
}
.ex-card-text {
color: var(--mk-color-text-secondary);
}
/* A whole card as one link: the title carries the link, the card shows it. */
.ex-card-link {
color: inherit;
text-decoration: none;
}
.ex-card:has(.ex-card-link:hover) {
border-color: var(--mk-color-border-control);
}
.ex-card-link:hover {
color: var(--mk-color-accent);
text-decoration: underline;
}
/* Long-form text. base.css already caps p and li at 65ch. */
.ex-prose {
display: grid;
gap: var(--mk-spacing-4);
line-height: var(--mk-font-line-height-relaxed);
}
.ex-prose h1 {
font-size: var(--mk-font-size-2xl);
letter-spacing: var(--mk-font-letter-spacing-display);
line-height: var(--mk-font-line-height-tight);
}
.ex-prose h2 {
margin-top: var(--mk-spacing-4);
font-size: var(--mk-font-size-xl);
}
.ex-prose h3 {
font-size: var(--mk-font-size-lg);
}
.ex-prose h4 {
font-size: var(--mk-font-size-md);
}
.ex-prose ul,
.ex-prose ol {
display: grid;
gap: var(--mk-spacing-1);
margin: 0;
padding-left: var(--mk-spacing-6);
}
.ex-prose li::marker {
color: var(--mk-color-text-muted);
}
.ex-prose blockquote {
padding-left: var(--mk-spacing-4);
border-left: 3px solid var(--mk-color-accent);
color: var(--mk-color-text-secondary);
font-style: italic;
}
.ex-prose code {
padding: 0.125em 0.375em;
border-radius: var(--mk-radius-sm);
background-color: var(--mk-color-bg-subtle);
font-size: 0.9em;
}
.ex-prose kbd {
padding: 0.125em 0.375em;
border: 1px solid var(--mk-color-border-control);
border-bottom-width: 2px;
border-radius: var(--mk-radius-sm);
background-color: var(--mk-color-surface);
font-size: 0.85em;
}
.ex-prose pre {
margin: 0;
padding: var(--mk-spacing-4);
overflow-x: auto;
border: 1px solid var(--mk-color-border);
border-radius: var(--mk-radius-md);
background-color: var(--mk-color-bg-subtle);
font-size: var(--mk-font-size-sm);
line-height: var(--mk-font-line-height-normal);
}
.ex-prose pre code {
padding: 0;
background-color: transparent;
font-size: inherit;
}
.ex-prose hr {
width: 100%;
margin: var(--mk-spacing-2) 0;
border: 0;
border-top: 1px solid var(--mk-color-border);
}
/* Data tables: base.css sets tabular numerals; numbers align right. */
.ex-table-wrap {
overflow-x: auto;
border: 1px solid var(--mk-color-border);
border-radius: var(--mk-radius-md);
}
.ex-table {
width: 100%;
border-collapse: collapse;
background-color: var(--mk-color-surface);
font-size: var(--mk-font-size-sm);
}
.ex-table caption {
padding: var(--mk-spacing-3) var(--mk-spacing-4);
color: var(--mk-color-text-muted);
font-size: var(--mk-font-size-sm);
text-align: left;
}
.ex-table th,
.ex-table td {
padding: var(--mk-spacing-2) var(--mk-spacing-4);
border-top: 1px solid var(--mk-color-border);
text-align: left;
}
.ex-table thead th {
background-color: var(--mk-color-bg-subtle);
color: var(--mk-color-text-muted);
font-size: var(--mk-font-size-xs);
font-weight: var(--mk-font-weight-semibold);
letter-spacing: var(--mk-font-letter-spacing-label);
text-transform: uppercase;
white-space: nowrap;
}
.ex-table .ex-num {
text-align: right;
}
.ex-table tbody tr:hover {
background-color: var(--mk-color-accent-subtle);
}
Cards on a subtle section
surface cards raised with shadow.raised on bg-subtle. The title is the link. Forced below it: hover, keyboard focus.
Cards on a subtle section HTML
<section class="ex-section">
<div class="ex-card-grid">
<article class="ex-card">
<p class="ex-eyebrow">Article</p>
<h3 class="ex-card-title">
<a class="ex-card-link" href="#tokens">Design tokens across four stacks</a>
</h3>
<p class="ex-card-text">One palette for Angular, WordPress, Razor and static pages.</p>
</article>
<article class="ex-card">
<p class="ex-eyebrow">Talk</p>
<h3 class="ex-card-title">
<a class="ex-card-link" href="#contrast">Contrast as a contract</a>
</h3>
<p class="ex-card-text">Testing WCAG ratios in CI instead of in review.</p>
</article>
<article class="ex-card">
<p class="ex-eyebrow">Project</p>
<h3 class="ex-card-title"><a class="ex-card-link" href="#cdn">A CDN for stylesheets</a></h3>
<p class="ex-card-text">Pinned versions that never change, and an alias that does.</p>
</article>
</div>
</section>
Prose
Headings, links, lists, a quotation, inline code, keys and a code block.
Prose HTML
<article class="ex-prose">
<h1>Shipping one palette to every site</h1>
<p>
Each site links one stylesheet. A change to a token reaches every site on the next release, and
<a href="#versioning">semantic versioning</a> keeps breaking changes out.
</p>
<h2>How a release works</h2>
<ol>
<li>Merge the change into <code>main</code>.</li>
<li>Push a tag such as <code>v1.5.0</code>.</li>
<li>The workflow uploads, publishes and verifies.</li>
</ol>
<h3>What sites receive</h3>
<ul>
<li>Every new token and value change on <code>/v1/</code>.</li>
<li>Nothing that renames or removes a token.</li>
</ul>
<blockquote>
<p>Change once, reflect everywhere.</p>
</blockquote>
<h4>Forcing a theme</h4>
<p>Press <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>L</kbd>, or set it in markup:</p>
<pre><code><html data-theme="dark"></code></pre>
<hr />
<p>Last updated 7 October 2026.</p>
</article>
Data table
Caption, column headers, row headers and right-aligned numbers.
Data table HTML
<div class="ex-table-wrap">
<table class="ex-table">
<caption>
Specimen page views by month
</caption>
<thead>
<tr>
<th scope="col">Month</th>
<th scope="col" class="ex-num">Views</th>
<th scope="col" class="ex-num">Visitors</th>
<th scope="col" class="ex-num">Change</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">July</th>
<td class="ex-num">1,204</td>
<td class="ex-num">811</td>
<td class="ex-num">+4.1%</td>
</tr>
<tr>
<th scope="row">August</th>
<td class="ex-num">1,377</td>
<td class="ex-num">902</td>
<td class="ex-num">+11.2%</td>
</tr>
<tr>
<th scope="row">September</th>
<td class="ex-num">1,118</td>
<td class="ex-num">760</td>
<td class="ex-num">−15.7%</td>
</tr>
</tbody>
</table>
</div>
Overlay
Layers above the page. Modal dialogs use the top layer; other overlays use z-index.overlay.
Tokens used --mk-spacing-4 --mk-spacing-6 --mk-color-border --mk-radius-lg --mk-color-surface --mk-shadow-raised --mk-color-text --mk-spacing-3 --mk-font-size-lg --mk-color-text-secondary --mk-spacing-1 --mk-z-index-overlay --mk-radius-md --mk-spacing-2 --mk-radius-sm --mk-font-size-sm --mk-color-accent-subtle --mk-color-danger
Overlay CSS
/* Dialog: open it with showModal() so it sits in the top layer and traps
focus. No scrim colour token exists yet, so the backdrop only blurs. */
.ex-dialog {
width: min(28rem, calc(100% - 2 * var(--mk-spacing-4)));
padding: var(--mk-spacing-6);
border: 1px solid var(--mk-color-border);
border-radius: var(--mk-radius-lg);
background-color: var(--mk-color-surface);
box-shadow: var(--mk-shadow-raised);
color: var(--mk-color-text);
}
.ex-dialog::backdrop {
backdrop-filter: blur(2px);
}
.ex-dialog-body {
display: grid;
gap: var(--mk-spacing-3);
}
.ex-dialog-title {
font-size: var(--mk-font-size-lg);
}
.ex-dialog-text {
color: var(--mk-color-text-secondary);
}
.ex-dialog-actions {
display: flex;
flex-wrap: wrap;
gap: var(--mk-spacing-3);
justify-content: flex-end;
margin-top: var(--mk-spacing-3);
}
/* Menu: a non-modal list under its button, above content on z-index.overlay.
Showing and hiding it, and arrow keys, are the site's script. */
.ex-menu-anchor {
position: relative;
display: inline-block;
}
.ex-menu {
position: absolute;
top: calc(100% + var(--mk-spacing-1));
left: 0;
z-index: var(--mk-z-index-overlay);
display: grid;
min-width: 12rem;
margin: 0;
padding: var(--mk-spacing-1);
border: 1px solid var(--mk-color-border);
border-radius: var(--mk-radius-md);
background-color: var(--mk-color-surface);
box-shadow: var(--mk-shadow-raised);
list-style: none;
}
.ex-menu-item {
display: block;
width: 100%;
padding: var(--mk-spacing-2) var(--mk-spacing-3);
border: 0;
border-radius: var(--mk-radius-sm);
background-color: transparent;
color: var(--mk-color-text);
font-size: var(--mk-font-size-sm);
text-align: left;
cursor: pointer;
}
.ex-menu-item:hover {
background-color: var(--mk-color-accent-subtle);
}
/* Items touch, so the focus ring is drawn inside them. */
.ex-menu-item:focus-visible {
outline-offset: -2px;
}
.ex-menu-item-danger {
color: var(--mk-color-danger);
}
/* danger on accent-subtle over a dark surface is 3.98:1, below 4.5:1, so
hover outlines the item instead of tinting it. */
.ex-menu-item-danger:hover {
background-color: transparent;
box-shadow: inset 0 0 0 1px var(--mk-color-danger);
}
.ex-menu-separator {
height: 1px;
margin: var(--mk-spacing-1) 0;
background-color: var(--mk-color-border);
}
Dialog
Shown open in place; a site opens it with showModal(). Uses the Actions CSS for its buttons. Forced below it: hover, keyboard focus.
Dialog HTML
<dialog class="ex-dialog" open aria-labelledby="ex-dialog-title">
<form class="ex-dialog-body" method="dialog">
<h2 class="ex-dialog-title" id="ex-dialog-title">Delete this draft?</h2>
<p class="ex-dialog-text">The draft and its notes are removed. This cannot be undone.</p>
<div class="ex-dialog-actions">
<button class="ex-button" value="cancel">Keep draft</button>
<button class="ex-button ex-button-danger-solid" value="delete">Delete draft</button>
</div>
</form>
</dialog>
Menu
Shown open under its button. Uses the Actions CSS for the button. Forced below it: hover, keyboard focus.
Menu HTML
<div class="ex-menu-anchor">
<button
class="ex-button"
type="button"
aria-haspopup="menu"
aria-expanded="true"
aria-controls="ex-menu"
>
Options
</button>
<ul class="ex-menu" id="ex-menu" role="menu">
<li role="none"><button class="ex-menu-item" type="button" role="menuitem">Edit</button></li>
<li role="none">
<button class="ex-menu-item" type="button" role="menuitem">Duplicate</button>
</li>
<li role="none">
<button class="ex-menu-item" type="button" role="menuitem">Share link</button>
</li>
<li class="ex-menu-separator" role="separator"></li>
<li role="none">
<button class="ex-menu-item ex-menu-item-danger" type="button" role="menuitem">Delete</button>
</li>
</ul>
</div>
Page
A small site built from the tokens, at phone width and at full width. It reflows at breakpoint.sm and breakpoint.lg.
Tokens used --mk-color-bg --mk-layout-container-max --mk-layout-gutter --mk-z-index-nav --mk-color-border --mk-spacing-2 --mk-spacing-6 --mk-spacing-3 --mk-color-text --mk-font-family-display --mk-font-weight-bold --mk-spacing-1 --mk-spacing-5 --mk-font-size-sm --mk-color-text-secondary --mk-color-accent --mk-spacing-16 --mk-spacing-12 --mk-font-size-3xl --mk-font-letter-spacing-display --mk-font-line-height-tight --mk-font-size-md --mk-color-bg-subtle --mk-font-size-xl --mk-spacing-4 --mk-radius-lg --mk-color-surface --mk-shadow-raised --mk-font-size-lg --mk-spacing-8 --mk-color-text-muted
Page CSS
/* A whole page from tokens alone. Media queries cannot read custom
properties, so they repeat the breakpoint tokens' values: 560px is
breakpoint.sm and 1000px is breakpoint.lg (a test keeps them in step). */
.ex-site {
min-height: 100%;
background-color: var(--mk-color-bg);
}
.ex-container {
width: 100%;
max-width: var(--mk-layout-container-max);
margin: 0 auto;
padding: 0 var(--mk-layout-gutter);
}
.ex-site-header {
position: sticky;
top: 0;
z-index: var(--mk-z-index-nav);
border-bottom: 1px solid var(--mk-color-border);
background-color: var(--mk-color-bg);
}
.ex-site-header .ex-container {
display: flex;
flex-wrap: wrap;
gap: var(--mk-spacing-2) var(--mk-spacing-6);
align-items: center;
justify-content: space-between;
padding-block: var(--mk-spacing-3);
}
.ex-site-brand {
color: var(--mk-color-text);
font-family: var(--mk-font-family-display);
font-weight: var(--mk-font-weight-bold);
text-decoration: none;
}
.ex-site-nav {
display: flex;
flex-wrap: wrap;
gap: var(--mk-spacing-1) var(--mk-spacing-5);
margin: 0;
padding: 0;
list-style: none;
font-size: var(--mk-font-size-sm);
}
.ex-site-nav a {
color: var(--mk-color-text-secondary);
text-decoration: none;
}
.ex-site-nav a:hover,
.ex-site-nav a[aria-current="page"] {
color: var(--mk-color-accent);
}
.ex-hero {
display: grid;
gap: var(--mk-spacing-5);
padding-block: var(--mk-spacing-16) var(--mk-spacing-12);
}
.ex-hero-title {
max-width: 14ch;
font-size: var(--mk-font-size-3xl);
letter-spacing: var(--mk-font-letter-spacing-display);
line-height: var(--mk-font-line-height-tight);
}
.ex-hero-lede {
color: var(--mk-color-text-secondary);
font-size: var(--mk-font-size-md);
}
.ex-hero-actions {
display: flex;
flex-wrap: wrap;
gap: var(--mk-spacing-3);
}
/* .mk-grid-bg comes from base.css: the drafting grid behind a section. */
.ex-band {
padding-block: var(--mk-spacing-12);
border-block: 1px solid var(--mk-color-border);
background-color: var(--mk-color-bg-subtle);
}
.ex-band-title {
margin-bottom: var(--mk-spacing-6);
font-size: var(--mk-font-size-xl);
}
.ex-site-grid {
display: grid;
gap: var(--mk-spacing-4);
grid-template-columns: minmax(0, 1fr);
}
@media (width >= 560px) {
.ex-site-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (width >= 1000px) {
.ex-site-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.ex-site-card {
display: grid;
gap: var(--mk-spacing-2);
padding: var(--mk-spacing-5);
border: 1px solid var(--mk-color-border);
border-radius: var(--mk-radius-lg);
background-color: var(--mk-color-surface);
box-shadow: var(--mk-shadow-raised);
}
.ex-site-card h3 {
font-size: var(--mk-font-size-lg);
}
.ex-site-card p {
color: var(--mk-color-text-secondary);
}
.ex-site-footer {
padding-block: var(--mk-spacing-8);
color: var(--mk-color-text-muted);
font-size: var(--mk-font-size-sm);
}
.ex-site-footer .ex-container {
display: flex;
flex-wrap: wrap;
gap: var(--mk-spacing-3) var(--mk-spacing-6);
justify-content: space-between;
}
.ex-site-footer-link {
color: var(--mk-color-text-secondary);
}
Mock site
Sticky header, hero in the display face, a bg-subtle band with .mk-grid-bg, cards and a footer. Uses the Actions CSS for its buttons.
Mock site HTML
<div class="ex-site">
<header class="ex-site-header">
<div class="ex-container">
<a class="ex-site-brand" href="#home">Manjunath</a>
<nav aria-label="Main">
<ul class="ex-site-nav">
<li><a href="#writing" aria-current="page">Writing</a></li>
<li><a href="#projects">Projects</a></li>
<li><a href="#talks">Talks</a></li>
<li><a href="#about">About</a></li>
</ul>
</nav>
</div>
</header>
<main>
<section class="ex-container ex-hero">
<h1 class="ex-hero-title">Building for the web, one token at a time</h1>
<p class="ex-hero-lede">
Notes on design systems, accessibility and shipping small things well.
</p>
<div class="ex-hero-actions">
<a class="ex-button ex-button-primary" href="#writing">Read the writing</a>
<a class="ex-button" href="#about">About me</a>
</div>
</section>
<section class="ex-band mk-grid-bg">
<div class="ex-container">
<h2 class="ex-band-title">Recent writing</h2>
<div class="ex-site-grid">
<article class="ex-site-card">
<h3>Design tokens across four stacks</h3>
<p>One palette for Angular, WordPress, Razor and static pages.</p>
</article>
<article class="ex-site-card">
<h3>Contrast as a contract</h3>
<p>Testing WCAG ratios in CI instead of in review.</p>
</article>
<article class="ex-site-card">
<h3>A CDN for stylesheets</h3>
<p>Pinned versions that never change, and an alias that does.</p>
</article>
</div>
</div>
</section>
</main>
<footer class="ex-site-footer">
<div class="ex-container">
<p>© 2026 Manjunath</p>
<p>
<a class="ex-site-footer-link" href="#source">Source</a> ·
<a class="ex-site-footer-link" href="#feed">Feed</a>
</p>
</div>
</footer>
</div>
Not yet expressible
Patterns the current tokens cannot draw directly. The examples use the fallback shown; adding the missing token is a separate decision.
| Pattern | Missing token | Fallback in the examples |
|---|---|---|
| Filled destructive button hover | A hover shade of color.danger. | Hover underlines the label. |
| Filled status badges | Text colours for use on success, warning and info fills, checked at 4.5:1 against them. | Outlined in the status colour, which already meets 4.5:1 on bg, bg-subtle and surface. |
| Tinted alert backgrounds | A subtle tint per status colour, like color.accent-subtle for accent. | Alerts sit on surface with a status-coloured edge, icon and title. Not planned: at 14% alpha, success and warning text fail 4.5:1 on their own tint in light mode. |
| Dialog backdrop | A scrim colour to dim the page behind a modal dialog. | The backdrop blurs the page (backdrop-filter) without dimming it. To be added when a site uses modal dialogs. |
| Disabled controls | A disabled foreground and background pair. | bg-subtle fill, text-muted label and a dashed border-control outline. Not planned: WCAG 2.2 exempts disabled controls from contrast. |
Type scale
| Token | Value | Sample |
|---|---|---|
| font.size.2xl | clamp(34px, 4vw, 64px) |
The quick brown fox |
| font.size.3xl | clamp(42px, 5.6vw, 98px) |
The quick brown fox |
| font.size.base | 17px |
The quick brown fox |
| font.size.display | clamp(50px, 8.1vw, 148px) |
The quick brown fox |
| font.size.lg | 21px |
The quick brown fox |
| font.size.md | 18px |
The quick brown fox |
| font.size.sm | 14px |
The quick brown fox |
| font.size.xl | clamp(28px, 3vw, 40px) |
The quick brown fox |
| font.size.xs | 12px |
The quick brown fox |
Spacing
| Token | Value | Sample |
|---|---|---|
| spacing.1 | 4px |
|
| spacing.10 | 40px |
|
| spacing.12 | 48px |
|
| spacing.16 | 64px |
|
| spacing.2 | 8px |
|
| spacing.20 | 80px |
|
| spacing.24 | 96px |
|
| spacing.3 | 12px |
|
| spacing.32 | 128px |
|
| spacing.4 | 16px |
|
| spacing.5 | 20px |
|
| spacing.6 | 24px |
|
| spacing.8 | 32px |
Radius
| Token | Value | Sample |
|---|---|---|
| radius.lg | 12px |
|
| radius.md | 6px |
|
| radius.pill | 999px |
|
| radius.sm | 4px |
|
| radius.xl | 16px |
Shadow review (D7)
shadow.raised = 0 16px 40px -20px #00000066
color.bg
Raised depth on the page background.
color.bg-subtle
The same token on the subtle background. Switch theme to review both modes.
Motion
motion.ease = cubic-bezier(0.2, 0.7, 0.1, 1) (hover samples)
| Token | Value | Sample |
|---|---|---|
| motion.duration.base | 300ms |
|
| motion.duration.fast | 150ms |
|
| motion.duration.slow | 500ms |
|
| motion.duration.slower | 900ms |