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 shapebutton-primary-large— 40px and full width, for the one call to action in a lead railbutton-secondary— white, with a border that darkens on hoverbutton-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
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.
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.
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.role="combobox" and aria-expanded on the trigger, role="listbox" on the list, role="option" and aria-selected on each row.body with position: fixed — left in place, a scrolling parameters rail clips it — and repositions on scroll and resize.<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
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
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
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.
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, links, helper text
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.