Demos / Design system
Built once, after the third time.
I don’t abstract until something shows up three times. The buttons, toggles and tags on this site hit three, so they became components.
Color
Every color is a CSS variable with a light and a dark value, and components only use the variables. Each swatch shows both: light on the left, dark on the right. In dark mode, raised things get lighter, so a card sits above the page. The printed résumé skips the variables and uses plain black on white.
--c-bgPage background--c-surfaceTinted band--c-cardCards, inputs--c-borderHairlines--c-inkBody text--c-mutedSecondary text--c-accentActions, focus--c-accent-softHover wash
Button
Starts an action. With href it becomes a link, because anything that goes to another page should be one.
Used in: primary for Download PDF on the résumé; secondary for the Résumé link in the header and Build palette
Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "secondary" | "primary" | "secondary" | Primary is for the one main action on a page. |
size | "sm" | "md" | "md" | sm for the header and toolbars, md next to other controls. |
icon | Icon name | — | Leading icon from the site's Icon set. |
loading | boolean | false | Shows a spinner, ignores clicks, stays focusable. |
disabled | boolean | false | Native disabled: skipped by Tab. |
href | string | — | Renders a link instead of a button. |
download | string | — | With href: the saved filename. Renders a plain <a>. |
Why loading isn’t disabled
A disabled button drops focus, so a keyboard user who presses Save loses their place and their next Tab starts from the top of the page. Loading uses aria-disabled instead: the button keeps focus and ignores clicks until it’s done.
IconButton
A button with only an icon. It’s separate from Button because it has no visible text, so label is required.
Used in: theme toggle, menu, close menu, clear text
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | required | What the button does, read out by screen readers. |
variant | "ghost" | "outline" | "ghost" | Outline gives it a frame when it sits alone. |
size | "sm" | "md" | "lg" | "md" | 28, 32 or 40px square. |
SegmentedControl
Picks one of a few options, all visible at once. Each option is a button with aria-pressed, so a screen reader says which one is on.
Used in: the Web Worker and List virtualization demos
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Names the group for screen readers. |
options | { id, label }[] | — | The choices, in order. |
value | string | — | The selected id. |
onChange | (id) => void | — | Called with the id that was pressed. |
disabled | boolean | — | Locks every option, e.g. while a demo runs. |
Tag
A small label. Not a button, so it has no hover state. There’s one look, and only the size changes: small for topics on a card, medium for a list of skills you’re meant to read.
Used in: demo topics, skills
Props
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "md" | "sm" | sm for metadata, md when the tags are the content. |
as | element | "span" | Use "li" inside a list. |
The mobile menu and the text box in the Web Worker demo are one-offs, so they live with the feature that uses them. If one shows up three times, it moves here.