J3W1 / DESIGN SYSTEM
Button
The native push button in the four action tones; primary is the only filled button at rest, destructive fills on hover and always needs a confirmation or an undo.
stable · LIGHT DOM · 1.1.0
Light DOM retains static content, native semantics, label relationships and host composition without duplicating controls.
Live variants
default
<j3w1-button id="example-button-default"><button class="button" type="button">
<svg class="button-icon" aria-hidden="true" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square"><path d="M3 8.5l3 3 7-7"></path></svg>
<span class="button-loading" aria-hidden="true">⋯</span>
<span class="button-label">Save changes</span>
</button></j3w1-button>secondary
<j3w1-button id="example-button-secondary"><button class="button button-secondary" type="button">
<span class="button-loading" aria-hidden="true">⋯</span>
<span class="button-label">Preview</span>
</button></j3w1-button>tertiary
<j3w1-button id="example-button-tertiary"><button class="button button-tertiary" type="button">
<span class="button-loading" aria-hidden="true">⋯</span>
<span class="button-label">Cancel</span>
</button></j3w1-button>destructive
<j3w1-button id="example-button-destructive"><button class="button button-destructive" type="button">
<span class="button-loading" aria-hidden="true">⋯</span>
<span class="button-label">Delete workspace</span>
</button></j3w1-button>Install and register
Use the exact version. If npm publication is pending, install the release tarball from the installation guide.
npm install --save-exact @j3w1/ui@1.1.0import '@j3w1/ui/tokens.css';
import '@j3w1/ui/styles/button.css';
import '@j3w1/ui/register/button';Insert the maintained markup above after loading these imports. Its native children provide accessible names and form behavior; the empty custom tag alone does not create a control.
Copy the complete implementation
npx --package=@j3w1/ui@1.1.0 j3w1-ui copy button --out ./vendor/j3w1/buttonThe CLI verifies every file before writing and refuses overwrites. Serve the output over HTTP and open index.html. Keep its modules, CSS, tokens and licence notices together.
Public API
attributes
| Name | Type / signature | Meaning |
|---|---|---|
disabled | CustomEvent | Disable the native controls owned by this instance. |
loading | CustomEvent | Mark the primary control busy and prevent duplicate activation. |
tone | CustomEvent | Button tone: primary, secondary, tertiary or destructive. |
properties
| Name | Type / signature | Meaning |
|---|---|---|
disabled | boolean | Delegates disabled state to native children. |
name | string | Native control name; application form keys remain explicit. |
methods
| Name | Type / signature | Meaning |
|---|---|---|
refresh | (): void | Reconnect behavior after replacing native child structure. |
focus | (options?: FocusOptions): void | Focus the first usable native control. |
events
| Name | Type / signature | Meaning |
|---|---|---|
j3w1-action | CustomEvent | A native button activated. Prevent default on this event to cancel the associated native default action. |
Dependencies and verification
Registered components: none. Style dependencies: none.
Package presence establishes an available implementation. Actual passes depend on the tested artifact and environment. Read specification execution evidence; read packed-consumer evidence; inspect the complete state matrix.
Full implementation manifest, API and token dependency closure
Canonical specification
Purpose
The push button that triggers an action. Four tones and no more: primary
is the single filled button in a view, secondary is the outline for every
other action, tertiary is the ghost for low-emphasis and toolbar actions,
destructive is an outline in the danger tone that fills on hover. A
destructive button also requires a confirmation step or an undo affordance;
the colour is not the safeguard. Navigation is a link, not a button.
Anatomy
A native <button>; optional leading icon; the label. The loading glyph
occupies the icon slot so the size never changes. A filled destructive
modifier (.button-destructive-filled, color.action.destructive.filled-bg
with color.action.destructive.filled-text) exists only inside confirm
dialogs.
States
| State | Visual | Non-colour channel |
|---|---|---|
| default | primary: color.action.primary.bg fill, color.action.primary.text, 1px color.action.primary.border; secondary: 1px color.action.secondary.border, color.action.secondary.text; tertiary: color.action.tertiary.text, no border; destructive: 1px color.action.destructive.border, color.action.destructive.text | — |
| hover | primary → color.action.primary.hover-bg; secondary → color.action.secondary.hover-bg; tertiary → color.action.tertiary.hover-bg; destructive fills color.action.destructive.hover-bg with color.action.destructive.hover-text | cursor: pointer; hover-capable pointers only |
| focus-visible | ring 1px dashed at −2px: color.interaction.focus.ring-container on the primary fill, color.interaction.focus.ring on the outline tones, color.action.destructive.hover-text on the destructive hover fill | the ring |
| active | primary → color.action.primary.pressed-bg; secondary → color.action.secondary.pressed-bg; tertiary → color.interaction.pressed.bg; destructive → color.action.destructive.pressed-bg; primary keeps color.action.primary.border, secondary and tertiary use color.border.active, destructive keeps color.action.destructive.border (D-015) | border change on secondary and tertiary; no translation or scale |
| disabled | color.text.disabled on color.interaction.disabled.bg with 1px color.border.disabled, every tone alike; no hover or active styling | disabled; cursor: not-allowed; never opacity |
| loading | size unchanged; the static ⋯ glyph replaces the icon; the button keeps its tone and stays focusable but ignores activation | glyph; aria-busy |
Precedence: disabled > loading > active > hover; focus-visible is always drawn.
The primary edge stays unchanged when pressed (D-015). Its exact role is
root.border-active in the part map, just as root.border names the rest
edge. The default-tone state contrast map checks the text against the
pressed fill; it does not substitute color.border.active for this
unchanged edge. The existing primary-action contrast declaration identifies
the labelled button through its text, not through the fill’s boundary.
Keyboard
Native. Enter and Space activate; nothing else is bound. A loading button
stays in the tab order so focus is not lost mid-action, and the host ignores
activation until aria-busy clears.
Accessibility
The accessible name is the label text; an icon-only button is the
icon-button component. Never aria-disabled on a button that should be
skipped, and never disabled on a button whose reason a user needs to
discover: prefer keeping it enabled and explaining on activation. Contrast:
primary 9.28:1, secondary 10.37:1, tertiary 8.65:1, destructive 5.40:1 at
rest and 4.58:1 on the hover fill; every outline border ≥ 4.45:1; rings ≥
3:1 on every fill they sit on. The target is at least 24×24 CSS pixels in
compact density with 4px between neighbours.
Portability
Fill, border and outline express every state; no gradient, shadow or transform is used. Hosts without dashed outlines draw a solid 1px ring in the same colour and record the deviation. Hosts that cannot recolour the ring on a fill draw it outside the fill at +1px.
Non-examples
Rounded corners or pill shapes. Two filled buttons in one view. A red
filled destructive button at rest. A hover that only changes text colour.
Translating or scaling the button on press. A spinner inside the button.
Disabling with opacity. A <div> or <a> styled as a button. A gradient or
shadow to suggest depth.