Button
Variants
Sizes
Shape
The main action carries a cut top-right corner, so the one primary button on a screen is recognizable by its shape as well as its fill. Every other button is square at rest. On hover, press and keyboard focus a button takes a second cut at the opposite corner; the first corner does not move. The cut is 40% of the button's height: 10, 13 and 16 px for the three sizes.
- A disabled main action keeps its cut: the shape says which button it is, not what state it is in. No disabled button takes the second cut.
- A destructive button that is armed (hovered, focused from the keyboard or pressed) fills with hazard stripes in place of a flat tint. The stripes are as faint as the tint they replace, so the red label reads as well as before.
- The segments of an attached ButtonGroup stay square: a cut inside the group would notch it.
- On a touch screen the second cut shows while the button is pressed.
- In forced colors the button is a plain bordered rectangle.
The fill and the edge are drawn by a layer behind the label, clipped to the shape. The button itself is never clipped, so its hit area, its size and its focus ring are those of the full rectangle. Because of that layer, bg-* and border-* classes on a Button do not change its fill or edge: set --oprt-cut-fill and --oprt-cut-edge instead, for example className="[--oprt-cut-fill:var(--oprt-surface-2)]". oprt-cut-none squares off one button, or every button inside the element that carries it.
Long labels
A label that does not fit wraps onto centered lines and the button grows with it, so no text is cut off or spills over a neighbor. A one-line button keeps its exact height: 24, 32 and 40 px for the three sizes, also in a row that stretches its items beside a button that wrapped. A label made of text and an element, such as an identifier in <code>, wraps as one block, and code inside it breaks anywhere, so an identifier never forces the button wider than its container.
With an icon
An icon at either end of a label sits a gap away from it and stays centered beside the label, whether the label is one line or several. Draw it at 12, 16 and 20 px for the small, medium and large button, so it fits the one-line height: a taller icon makes the button taller. Name an icon-only button on the button itself.
Disabled
A disabled button is clearly inactive: dim text on the control outline, no hover and no press. It never activates by click, Enter, Space or a form submission. Ghost stays outline-free.
When a Tooltip or a Popover explains why a button is unavailable, the button stays in the Tab order and announces itself as unavailable (aria-disabled) instead of leaving it, so keyboard and touch users can reach the explanation. It still never activates.
With link via asChild
A disabled asChild link is aria-disabled, leaves the Tab order and cancels the click and the middle click, so it neither navigates nor runs the link's own onClick. A browser still offers the link's address (open in a new tab from the context menu, copy link), and a child that sets its own props can override the block, so show navigation that is not available yet as plain text, and render the link when it is. The first disabled asChild Button reports this in development. The content of an asChild child is its own: put a label made of text and an element in one <span>.
Props
| PROP | TYPE | DEFAULT | REQUIRED |
|---|---|---|---|
motionTactile pointer/touch feedback. None disables motion; keyboard and system reduced motion are always immediate. | enumauto | none | "auto" | no |
variant | enumprimary | secondary | tertiary | danger | ghost | "primary" | no |
size | enumsm | md | lg | "md" | no |
disabledUnavailable: dim text on the control outline, no hover, and it never activates. A Button that is a Tooltip or Popover trigger with an explanation stays in the Tab order (aria-disabled) so the explanation can be reached. | boolean | false | no |
asChildRenders the child element in place of the button, for example a link; the child's own content is not laid out for you, so put a label made of text and an element in one span. A disabled asChild Button is aria-disabled, leaves the Tab order and blocks the click and the middle click, but a browser still offers the link's address (open in a new tab, copy link) and a child can override the block: show unavailable navigation as plain text. | boolean | false | no |
Accessibility
- Enter/Space activates;
disabledprevents pointer and keyboard activation. - Defaults to
type="button"; settype="submit"for form submission. - Visible keyboard focus ring and at least 24 × 24px targets. The cut is decoration: the ring surrounds the full rectangle, with the edge of the shape lit inside it, and the cut corners still take a click.
- A button that is unavailable but focusable carries
aria-disabled="true"and keeps its focus ring. - An icon-only button needs an
aria-label. - A disabled link is better presented as text than as an
asChildButton that cannot be used.
Manifest entry
<Button variant="primary" size="md">DEPLOY</Button>View agent manifest
{
"description": "Primary interactive element for triggering actions. Short action labels display uppercase. A label that does not fit wraps and centers, and the button grows with it; a one-line button keeps its exact height, also in a row that stretches its items. A label made of text and an element (an identifier in code, a case-sensitive name in a span) wraps as one block, and an icon at either end stays beside it, drawn at 12, 16 and 20 px for sm, md and lg so it fits the one-line height. Code and kbd retain their casing, and code breaks anywhere; wrap other case-sensitive names or IDs in a child with normal-case tracking-normal oprt-wrap-anywhere. Shape: the primary variant carries a cut top-right corner at rest, and every enabled button takes a second cut at the opposite corner on hover, press and keyboard focus; an armed danger button fills with hazard stripes; segments of an attached ButtonGroup stay square. The fill and edge are drawn by a layer clipped to that shape, so bg-* and border-* classes do not change them: set --oprt-cut-fill and --oprt-cut-edge instead. The oprt-cut-none class squares off one button, or every button inside the element that carries it.",
"props": {
"motion": {
"type": "enum",
"values": [
"auto",
"none"
],
"default": "auto",
"description": "Tactile pointer/touch feedback. None disables motion; keyboard and system reduced motion are always immediate."
},
"variant": {
"type": "enum",
"values": [
"primary",
"secondary",
"tertiary",
"danger",
"ghost"
],
"default": "primary"
},
"size": {
"type": "enum",
"values": [
"sm",
"md",
"lg"
],
"default": "md"
},
"disabled": {
"type": "boolean",
"default": false,
"description": "Unavailable: dim text on the control outline, no hover, and it never activates. A Button that is a Tooltip or Popover trigger with an explanation stays in the Tab order (aria-disabled) so the explanation can be reached."
},
"asChild": {
"type": "boolean",
"default": false,
"description": "Renders the child element in place of the button, for example a link; the child's own content is not laid out for you, so put a label made of text and an element in one span. A disabled asChild Button is aria-disabled, leaves the Tab order and blocks the click and the middle click, but a browser still offers the link's address (open in a new tab, copy link) and a child can override the block: show unavailable navigation as plain text."
}
},
"composable_with": [
"ButtonGroup",
"Toolbar",
"CardFooter"
],
"never_inside": [
"Button",
"Badge"
],
"example": "<Button variant=\"primary\" size=\"md\">DEPLOY</Button>"
}Motion
Pointer and touch presses compress the visual surface to 0.97 in 80ms and release in 120ms. The full hit area remains usable. Keyboard activation and focus are immediate. The second cut of the shape arrives and leaves on the same timings, and follows the same motion settings.
Use motion="none" for frequently operated controls. The default motion="auto" respects system reduced motion, skips keyboard and assistive activation, and starts from the supplied resting state. See Motion for the interactive playground.