Components

Every interactive element supports the same closed state set: default, hover, active, focus-visible, disabled, loading, selected, error. There is no ninth state; a control that seems to need one needs rethinking instead.

Heights are skeleton: 28 for the input family, 32 for buttons, segmented controls and icon buttons, 40 for the one large call to action, 24 for icon plates and swatches. Every component below has a class of the same name in the <style> block of every template — using those classes is the shortest path, and building the same sizes and states in your own framework is just as valid.

Buttons

The top row is six 32px shells, disabled and loading included; the bottom row is the 40px call to action, an icon-only button and the Tips capsule.

  • button-primary — near-black, 32px, answers most questions. The platform's own Export button has exactly this shape
  • button-primary-large — 40px and full width, for the one call to action in a lead rail
  • button-secondary — white, with a border that darkens on hover
  • button-ghost — transparent until hovered

Destructive actions use the secondary shell with red-default text and a confirmation, never a solid red fill. Loading keeps the button's width, swaps in a spinner and sets aria-busy.

Labels are verbs (Generate, Export, Reset), never OK or Submit. Icon-only buttons are 32px squares on an 8px radius, white on the canvas. The Tips button in the canvas's top-end corner is the system's second and last capsule: 32px, white, stroke-default border, a 20px lightbulb and the word — it has to read as help, not as one more action.

The input family: grey fill, no border

mm
mm
mm
mm
mm
mm
Width cannot exceed 300 mm.

input-field: 28px tall, 92px wide, bg-control fill, no border, 6px radius, text in text-secondary with tabular figures. The error message sits beside the field, not only in a toast.

The unit is pinned to the field's end, not carried inside the value: 10px in text-tertiary, a separate element. Numbers then line up at the start edge and units at the end, and switching mm ↔ inch rewrites the two independently. A unit inside the value moves as the number grows, and reorders under RTL bidi.

A numeric field carries its starting value in value and inputmode="decimal". Numeric fields support horizontal drag: press and drag to change the value, Shift for ten-times steps, Alt for fine steps, and a movement under 3px counts as a click. Use setPointerCapture so dragging outside the panel keeps the gesture, invert the direction in RTL, and keep Shift + ↑/↓ as the keyboard equivalent. Never show native spinners: the targets are tiny and the register is wrong.

The prompt box in a generate rail is the same family grown up: 8px radius, 10/12 padding, a character counter in text-disabled at the bottom-end corner.

Select

The trigger is the input box at 110px with a text-tertiary chevron that flips when the list opens. The list is a floating surface, so unlike the docked panels it takes a border, an 8px radius and shadow-200.

Circle
Square
Hexagon
Star
Click the trigger to close and reopen it, and click an option to select it. Open, the trigger goes white with a 1px `stroke-active` border — the same state as focus — and the chevron flips.

Two signals that never collide, and each is legible alone. The fill says where you are: pointer and keyboard share it, so exactly one row is ever filled. The brand-default checkmark plus 500 weight says what is chosen, and it still reads with no fill under it. Above, Circle is selected and Square is where the pointer sits.

The row highlight gets a 12% step of its own because the 6% and 8% panel fills both vanish on a popup's white ground.

Rows are 32px on a 4px radius, 10px inline padding, 12px text in text-primary. The list is 6px-padded, at least as wide as the trigger, and caps at seven rows before it scrolls with a thin scrollbar. Above three options a select beats a segmented control; below three, the segmented control beats a select.

The contract this owes

A native <select> gives the keyboard, the screen reader and the platform picker away for free. Styling the popup means writing all of that yourself. Every template already ships it as upgradeSelects in its script block: port that, do not re-derive it.

It is written as progressive enhancement, so the control works before the script runs: the markup ships a real <select> inside a <span class="select-field" data-select>. The script hides that element, builds the trigger and the list, and keeps the native one as the value's source of truth — form submission and your own change listeners keep working untouched.

1Focus never leaves the trigger. aria-activedescendant points at the active option — moving focus into the list is how you end up stranding it on body when the list closes.
2role="combobox" and aria-expanded on the trigger, role="listbox" on the list, role="option" and aria-selected on each row.
3Closed: ↓ ↑ Enter Space Home End or any character opens it.
4Open: ↓↑ move, Home/End jump to the ends, Enter or Space commits, Escape closes without changing the value, Tab closes, an outside click closes, type-ahead jumps.
5Focus returns to the trigger on close, and the active row scrolls inside the list's own box rather than the page's.
6The list mounts on body with position: fixed — left in place, a scrolling parameters rail clips it — and repositions on scroll and resize.
7It flips above the trigger when the space below runs out, and aligns to the trigger's end edge in RTL.
8On narrow touch screens fall back to the native <select>: you give up the system picker otherwise, and 32px rows are small under a thumb.

Everything above is implemented in upgradeSelects, in every template's script block. It is the one piece of a template you keep: the rest is demo wiring you replace with your own state.

Switch

36 × 20 track · 14px knob · travels 16px

stroke-default off, brand-default on. The track is below the 24px target minimum, so the field row's <label for> is the larger target that makes it reachable — every template wires the switch that way.

The track and the knob are decoration painted over a real <input type="checkbox">; every layer needs pointer-events: none and aria-hidden.

Slider

6 mm
40%
40%

Never alone: the value always appears as a number at the end of the row, and when precision matters pair it with an input. A 3px track in track-off, the filled part in brand-default, a 12px thumb with a white centre and a 2px brand ring. Step and range come from the model's real constraints; a silently clamped value is a bug the user cannot see.

Segmented control

A 32px bg-hover track, 2px padding, 6px radius; items are 12-medium in text-secondary, and the active item is white on a 4px radius with shadow-100; a 1px stroke-divider line separates adjacent inactive items. The container takes role="group" with a name, each option a <button aria-pressed>. Use it for two or three mutually exclusive views; above three, use a select. It is not a tab strip and does not carry page navigation.

Thumbnails and the tool rail

A template thumbnail is a 140px white card: 8px radius, stroke-divider border, shadow-100. An inline SVG sits inset 14px; a photo fills the card edge to edge. Style tiles are the same card at 90px with no shadow (SVG inset 10px), and a round subject keeps the square card with a circular image inside it. Selection is a 2px brand-default ring drawn over the card, so the content never resizes; hover is stroke-default.

The tool rail is 64px wide with 40px plates. The selected tool is brand-default with a white glyph.

Platform-fed blocks

Two pieces of UI draw their data from the platform rather than from your generator: the material the preview is rendered on, and the user's history. The templates ship them working with placeholders so they sit in the right place from day one; wire the data when the capability is open to you.

Surface is a property card at the bottom of the stack: 57px tiles four to a row, one selected with a 2px text-primary ring 2px outside the tile. It changes how the preview renders, never what exports. History is a second level inside the lead rail: a back row, 90px tiles three to a row, Load more beneath. Tiles follow the thumbnail rule: an inline SVG sits inset 10px, a photo fills the tile.

Dialogs

Fabrication Tips1 / 3
Illustration

Real-world units

All dimensions are physical (mm / inch) and match the exported file 1:1 — what you set is what gets cut.

Native <dialog> with showModal(): 480px wide, 12px radius, shadow-300, scrim bg-overlay (70% black). On a short viewport the body scrolls; header and footer stay put. Header 16/24: title in 16-semibold, a 20px info glyph in text-tertiary beside it, the 24px close button at the end, hairline below. Then an optional media band in bg-control, and a footer with pagination dots at the start and buttons at the end. Most things put in dialogs do not belong there: errors go beside their control, progress goes in the preview, settings go in the parameters rail. Fabrication Tips is the canonical dialog.

Antique bronze1:1Transparent background
Reference image

The generate dialog (layout 4). The dark bg-inverse header repeats the pick: a 66px thumbnail, the name in 16-semibold, one tag per fact the choice already fixed. The body stacks form fields — the one place where a form-label sits above its control rather than beside it, because a 480px dialog has room for a full-width drop target and a 320px rail does not. Picking a style tile opens this, so the tile itself never starts the call.

Alerts put the tint behind the message and the solid on the icon, with text-primary words, next to whatever they describe (see Colour). Links take blue-default and keep their underline — colour alone is not a link affordance. Helper text under a field is 12-regular in text-tertiary and explains a constraint rather than restating the label.

Be stingy. Anything that fits in a unit suffix should not become a sentence — 20 % beats "percentage of a single tile's edge length". Anything readable from the results should not be repeated in the parameters rail. The ideal end state is a parameters rail with two or three genuinely constraining notes left in it.

Esc
Search all docs · to navigate · to open