Button
Tier A. Follows the anatomy template and the conventions.
1. Purpose and native element
Section titled “1. Purpose and native element”Starts an action. Renders a native <button>, following the WAI-ARIA
Button pattern. type="button" is the default, so
a Button inside a form never submits by accident; type="submit" and type="reset" are passed through.
A Button that navigates is a Link, not a Button.
Variants: secondary (default), accent (the primary action) and danger (destructive). Sizes follow
the density’s control height (section 13), never a separate size scale.
2. Object
Section titled “2. Object”A key: a rounded, tapered block whose base is the button’s border box.
| Part | Shape | Height at 50% | Moves |
|---|---|---|---|
| Key body | Frustum from the base to the top, object.taper inset |
object.keyHeight (8) |
Down by object.keyTravel (6) on press; up by keyHeight / 4 on hover |
| Key top | The captured face, screen-locked (section 6) | — | With the body |
The label and icon are on the key top. The whole travel stays inside the border box.
3. Faces and materials
Section titled “3. Faces and materials”| Face | Colour role | Text role | Material role |
|---|---|---|---|
Key top, secondary |
secondary |
text |
control (matte) |
Key top, accent |
accent |
on-accent |
buttonPrimary (matte) |
Key top, danger |
danger |
on-danger |
control |
| Key sides (Objects) | The top’s colour, darkened by the bevel | — | Same as the top |
data-strata-material on the button overrides the role for one button.
4. Elevation per state
Section titled “4. Elevation per state”| State | Relief | Objects | CSS |
|---|---|---|---|
| Rest | elevation.raised (3) |
Key at keyHeight |
Relief recipe at raised |
| Hover | raised × state.hoverHeight; interior +state.hoverLift |
Key rises keyHeight / 4 |
Larger shadow, brightness(1.04) |
Pressed (:active) |
raised × state.pressHeight; interior −state.pressSink |
Key travels keyTravel |
Smaller shadow, brightness(0.96) |
| Focus-visible | Accent edge, focus.width |
Emissive glow on the key | outline: focus.width solid accent |
| Disabled | Height 0, no bevel gain, content at 38% | Key flattened to the face | Flat, content opacity: 0.38 |
Busy (aria-busy="true") |
Rest, with an emissive Spinner replacing the icon |
Same | Same |
Selected and invalid do not apply; a latching button is a ToggleButton.
5. Relief primitives
Section titled “5. Relief primitives”One raised primitive: the <button> itself, from its border box and border-radius
(radius.control).
6. Tokens it reads
Section titled “6. Tokens it reads”| Token | Prop |
|---|---|
colour.secondary, colour.accent, colour.danger, colour.text |
variant picks the role |
radius.control |
— (theme or scope) |
elevation.raised, state.*, object.keyHeight, object.keyTravel, object.taper |
— |
material.roles.control, material.roles.buttonPrimary |
material |
density (control height), space.* (padding) |
— |
font.size.body, font.weight.strong |
— |
focus.width, focus.offset, motion.fast, motion.easing.press |
— |
level |
level |
7. DOM structure and overlay needs
Section titled “7. DOM structure and overlay needs”<button type="button" class="ug-button ug-button--accent" data-strata-face="raised"> <span class="ug-button__icon" aria-hidden="true"><!-- optional SVG --></span> <span class="ug-button__label">Save</span></button>- The
<button>is the boundary element: React and Vue refs,classandstylereach it. - No overlay.
- Icon-only buttons are IconButton.
8. Keyboard and screen reader
Section titled “8. Keyboard and screen reader”| Key | Action |
|---|---|
| Enter, Space | Activate (native) |
| Tab, Shift+Tab | Move focus (native order) |
- Role
button(native). The accessible name is the label text. disableduses the native attribute, which removes the button from the tab order. A button that must stay focusable while unavailable usesaria-disabled="true", and the binding blocks activation.- Busy:
aria-busy="true"on the button, and the label stays.
9. CSS presentation
Section titled “9. CSS presentation”The relief recipe at elevation.raised, with box-shadow derived from the light (section 7), the bevel
gradient, and the material’s finish zones (section 4). Minimum size: the density’s control height, and never
below target.min (24) in either dimension. A flex row never squeezes a button below its label
(flex-shrink: 0): a row too narrow for its buttons should wrap. Under forced-colors: ButtonFace, ButtonText and a
ButtonBorder outline; no shadows.
10. Tests
Section titled “10. Tests”- Plain DOM: real pointer click and Enter and Space activate once; disabled does not activate;
typedefaults tobuttoninside a form. - GPU: at Relief and Objects, GPU stays active through hover, press and release; pixels are fresh after
each; a hit at the key’s edge pixel reaches the
<button>. - Accessibility: axe on every variant and state, in light, dark and high contrast; accessibility
snapshot equal in DOM and GPU modes; focus ring visible in decoded pixels; contrast of the label on the lit
top at
interior.max. - Parity: Relief and CSS within the P0.6 budget at rest, hover and pressed.
11. Push-button form: momentary
Section titled “11. Push-button form: momentary”Instrument, wave 2 (plan). form="momentary" draws the same native button as a push
button: a machined cap on a polished bezel, a lit ring between them that lights while the button is held, and the label on
the cap. Without form the button is the key above; any other form string is the native form attribute (the id of the
button’s form owner), as before. colour names the ring’s role (glowOn amber by default, or accent, success,
warning, danger, info); variant still picks the cap’s colour (accent and danger caps). The native <button> is the
control, its box the hit area, and the object is decoration inside it.
| Part | Shape | Height at 50% | Moves |
|---|---|---|---|
| Box (the native button) | The cap plus 24 px each way (64 at size.push 40), wider with the label |
0 | No |
| Bezel | A stadium inset 5 px for the bloom’s halo, the cap + 14 high, 4.4 px of polished metal round the ring | Relief: 3; Instrument: 3.2 | No |
| Socket, ring | The dark band under the ring (printed on the face) and the lit ring over it, the cap + 5.2 high; the cap leaves 2.6 px of it showing (a 2 px ring and a 0.6 px gap) | Instrument: 0.5 on the bezel | The ring is lit 0 to 1 by --strata-lit |
| Cap | A pill as tall as size.push (32, 40 or 48), 12 px of print each side, carrying the icon and the label |
Relief: 3; Instrument: 6, taper 0.6 | Down pushButton.travel.held (4) while held |
- Materials. A layer holds at most 16 materials (core’s cap,
INSTRUMENT_MATERIALS), and the helper keeps its scenes under it with named rules (gpu/index.ts): a push button’s parts are added only while the scene holds fewer thanINSTRUMENT_PUSH_MATERIALS(12) materials, and a plain Button’s key body only belowINSTRUMENT_KEY_BODY_MATERIALS(14); past a rule the button is a Relief key (a raised face, still the native button), and the lens, bezel and body materials that the push buttons add after the keys trim the later colours’ keys the same way until the scene fits 16. It is reported, not silent:strata.diagnostics(panel)gives{ materials: { count, cap }, keysAtRelief: [ids] }and the helper warns once per panel withconsole.warn. A scene that still passes the cap is drawn at Relief by core, withstate().instrumentsayingbudget. Split a panel with many colours or many component kinds. - Lit ring. The ring is lit (
--strata-lit: 1) while the button is held (the pointer down on it, or Enter or Space down) and unlit otherwise; a disabled oraria-disabledbutton is never held, never lit and never moves. The light is never the only cue: the cap goes down as well, and the click is the native one. - At every GPU level the ring is an emitter (
emitter.pushButton.*: reach 4 cap diameters, height 0.3, strength 2,400 for a 40 px cap): the panel takes a version 4 scene with itsemitters, theprotectedrects of its text and afeather, so core’s face pool lights the page round the ring within the contrast guard (the label stays withininterior.max), and a press is a uniform write with no capture. Relief draws the bezel and the cap as raised faces, Objects as slabs and a key that travelsobject.keyTravel. - At the Instrument level the button is
instrument-push.ts’s object: a polishedbezel, the ring a glasslensplate with its emitter (dark glass unlit, glowing and blooming lit), and aplinthbody in the cap’s colour with the capturedcapon it, both travelling 4 px (× 0.6, 1, 1.6 with depth) alongt, which follows the CSS throw--strata-push-tonspring.button(1,600 / 80, critically damped). The label is the capture itself (a captured cap), so it is exact. Bezel, ring and the CSS paint of the object are hidden while the parts draw them; the cap keeps its paint, which the captured cap shows, and its CSS motion is held, so a press repaints nothing. Underprefers-reduced-transparencythe ring is opaque lacquer. - Preferences.
prefers-reduced-motion: the ring and the cap change at once.prefers-contrast: more: emitters off, the ring flat colour, edges incolour.text.forced-colors: active:Canvasbezel and ring withButtonTextborders, aHighlightring while held, aButtonFacecap, aHighlightfocus ring,GrayTextdisabled. Reflow: the cap grows with the label (it wraps at 320 px and 200% text); the bezel and ring follow it.
<button type="button" class="ug-button ug-push" data-strata-form="momentary" data-strata-colour="danger"> <span class="ug-push__object" aria-hidden="true"> <span class="ug-push__bezel" data-strata-face="raised"></span> <span class="ug-push__socket"></span> <span class="ug-push__ring"></span> </span> <span class="ug-push__cap" data-strata-face="raised"><span class="ug-button__label">Record</span></span></button>- The
<button>is the boundary: refs,classandstylereach it, anddata-strata-colouris the ring’s role. The plain-DOM behaviour (attachStrataPushButtons, loaded byattachStrataand by the bindings) keepsdata-strata-down="true"on the button from the pointer or key going down to the pointer going up anywhere, the key going up, the button losing focus or the page being hidden; with no script the same look comes from:active. - Keys: Enter and Space activate, as on any button; both hold the ring lit while down. Nothing about the keys changes.
- Feedback: the opt-in
clickplays as the button goes down (one per press, by the visitor’s own gesture for the pulse); off by default. - Tests:
ui-push-button.test.mjs(plain DOM, accessibility, GPU at Relief, Objects and Instrument, feedback, touch and reflow),ui-push-button-bindings.test.mjs,ui-push-button-astro.test.mjs, and the unit tests instrata-rotary-push.unit.test.mjs.