API
Button
Actions and links as buttons: variants, sizes, and icons.
Installation
pnpm dlx zazz-ui add buttonnpx zazz-ui add buttonyarn dlx zazz-ui add buttonbunx zazz-ui add buttonAlso adds fields, kbd, which it depends on. Run zazz-ui init first if the project has no zazz/ folder yet.
Examples
Default
<div
style="
--display: flex;
--flex-wrap: wrap;
--items: center;
--gap: 4;
"
>
<button data-ui="button" type="button">Default</button>
<button data-ui="button" data-button-variant="primary" type="button">Primary</button>
<button data-ui="button" data-button-variant="muted" type="button">Muted</button>
<button data-ui="button" data-button-variant="ghost" type="button">Ghost</button>
<button data-ui="button" data-button-variant="outline" type="button">Outline</button>
<button data-ui="button" data-button-variant="info" type="button">Info</button>
<button data-ui="button" data-button-variant="success" type="button">Success</button>
<button data-ui="button" data-button-variant="warning" type="button">Warning</button>
<button data-ui="button" data-button-variant="destructive" type="button">Destructive</button>
<button data-ui="button" data-button-variant="link" type="button">Link</button>
</div>/**
* button.css — Button ([data-ui~="button"])
*
* @layer variables, components
* @requires _layers.css, _variables.css, _reset.css, _properties.css (--_*-resolved, --_focus-shadow), fields.css (--ui-field-* metrics), kbd.css (--ui-kbd-*)
* @uses color-mix(), oklch(from …) — variant hover/active tints
* @uses text-box — vertical trim (patchy browser support)
* @tokens --ui-button-* (@layer variables; hooks are named after the utilities they back; metrics default to the --ui-field-* family)
* @consumedby toggle.css, select.css, button-group.css, autocomplete.css, combobox.css,
* command.css, lightbox.css, menu.css, navigation-menu.css, popover.css
*
* Every utility-backed declaration reads one chain, highest first:
* var(--_<utility>-resolved, var(--_button-<utility>, var(--ui-button-<utility>)))
* The resolver is set by the utilities layer from the element's `style` gate
* (inline utilities and their tiers), the private by a variant or size on this
* element, and the hook by :root, a subtree, or an inline hook.
*/
@layer variables {
:root {
/* metrics follow --ui-field-* so buttons match adjacent fields; remap a
--ui-button-* token to diverge. Colors stay independent: a button is not a
field surface, and its border doesn't go primary on hover. */
--ui-button-ring-color: var(--ui-field-ring-color);
--ui-button-font-family: var(--font-family-body);
--ui-button-font-size: var(--font-size-sm);
--ui-button-font-weight: var(--font-weight-strong);
--ui-button-leading: 1;
--ui-button-bg: var(--color-background);
--ui-button-bg--hover: oklch(from var(--color-background) calc(l - 0.025) c h);
--ui-button-bg--active: oklch(from var(--color-background) calc(l - 0.05) c h);
--ui-button-text: var(--color-foreground);
--ui-button-text--hover: var(--color-foreground);
--ui-button-text--active: var(--color-foreground);
--ui-button-border-width: var(--ui-field-border-width);
--ui-button-border-style: var(--ui-field-border-style);
--ui-button-border-color: var(--ui-field-border-color);
--ui-button-border-color--hover: var(--ui-field-border-color);
--ui-button-border-color--active: var(--ui-field-border-color);
/* dual-mode: a scale number or a length */
--ui-button-gap: var(--spacing);
--ui-button-px: var(--ui-field-px);
--ui-button-min-h: var(--ui-field-h);
--ui-button-rounded: var(--ui-field-rounded);
--ui-button-icon-size: var(--ui-field-icon-size);
/* never `none`: the focus ring layers into this box-shadow list */
--ui-button-shadow: 0 0 transparent;
}
}
/* Variant privates: non-inheriting so a variant never cascades into a nested
button. Unset, they are guaranteed-invalid and the chain falls to the hook. */
@property --_button-ring-color {
syntax: "*";
inherits: false;
}
@property --_button-font-family {
syntax: "*";
inherits: false;
}
@property --_button-font-size {
syntax: "*";
inherits: false;
}
@property --_button-font-weight {
syntax: "*";
inherits: false;
}
@property --_button-leading {
syntax: "*";
inherits: false;
}
@property --_button-bg {
syntax: "*";
inherits: false;
}
@property --_button-bg--hover {
syntax: "*";
inherits: false;
}
@property --_button-bg--active {
syntax: "*";
inherits: false;
}
@property --_button-text {
syntax: "*";
inherits: false;
}
@property --_button-text--hover {
syntax: "*";
inherits: false;
}
@property --_button-text--active {
syntax: "*";
inherits: false;
}
@property --_button-border-width {
syntax: "*";
inherits: false;
}
@property --_button-border-style {
syntax: "*";
inherits: false;
}
@property --_button-border-color {
syntax: "*";
inherits: false;
}
@property --_button-border-color--hover {
syntax: "*";
inherits: false;
}
@property --_button-border-color--active {
syntax: "*";
inherits: false;
}
@property --_button-gap {
syntax: "*";
inherits: false;
}
@property --_button-px {
syntax: "*";
inherits: false;
}
@property --_button-min-h {
syntax: "*";
inherits: false;
}
@property --_button-rounded {
syntax: "*";
inherits: false;
}
@property --_button-icon-size {
syntax: "*";
inherits: false;
}
@property --_button-shadow {
syntax: "*";
inherits: false;
}
@layer ui.components {
:where([data-ui~="button"]) {
font-family: var(
--_font-family-resolved,
var(--_button-font-family, var(--ui-button-font-family))
);
font-size: var(--_font-size-resolved, var(--_button-font-size, var(--ui-button-font-size)));
font-weight: var(
--_font-weight-resolved,
var(--_button-font-weight, var(--ui-button-font-weight))
);
line-height: var(--_leading-resolved, var(--_button-leading, var(--ui-button-leading)));
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
--_gap-len: var(--_gap-resolved, var(--_button-gap, var(--ui-button-gap)));
--_gap-num: var(--_gap-resolved, var(--_button-gap, var(--ui-button-gap)));
column-gap: calc(var(--_gap-len) + var(--_gap-num) * var(--spacing));
--_min-h-len: var(--_min-h-resolved, var(--_button-min-h, var(--ui-button-min-h)));
--_min-h-num: var(--_min-h-resolved, var(--_button-min-h, var(--ui-button-min-h)));
min-block-size: calc(var(--_min-h-len) + var(--_min-h-num) * var(--spacing));
min-inline-size: max-content;
--_px-len: var(--_px-resolved, var(--_button-px, var(--ui-button-px)));
--_px-num: var(--_px-resolved, var(--_button-px, var(--ui-button-px)));
padding-inline: calc(var(--_px-len) + var(--_px-num) * var(--spacing));
border-width: var(
--_border-width-resolved,
var(--_button-border-width, var(--ui-button-border-width))
);
border-style: var(
--_border-style-resolved,
var(--_button-border-style, var(--ui-button-border-style))
);
border-color: var(
--_border-color-resolved,
var(--_button-border-color, var(--ui-button-border-color))
);
border-radius: var(--_rounded-resolved, var(--_button-rounded, var(--ui-button-rounded)));
color: var(--_text-resolved, var(--_button-text, var(--ui-button-text)));
background-color: var(--_bg-resolved, var(--_button-bg, var(--ui-button-bg)));
background-clip: padding-box;
white-space-collapse: collapse;
/* The ring is published to --_focus-shadow so the utilities layer's
box-shadow emission keeps it above a --shadow or --ring utility; the ring
widths grow from 0 on :focus-visible so the ring eases in. */
--_ring-offset-width: 0px;
--_ring-width: 0px;
--_focus-in: max(0, -1 * sign(var(--_focus-ring-w, 1px)));
--_ring-fill: color-mix(
in oklch,
var(--_ring-color-resolved, var(--_button-ring-color, var(--ui-button-ring-color)))
var(--ring-opacity),
transparent
);
--_focus-shadow:
0 0 0 calc(var(--_ring-offset-width) * (1 - var(--_focus-in))) var(--ring-offset-shadow-color),
0 0 0 calc((var(--_ring-offset-width) + var(--_ring-width)) * (1 - var(--_focus-in)))
var(--_ring-fill, var(--ring-shadow-color)),
inset 0 0 0 calc(var(--_ring-offset-width) * var(--_focus-in)) var(--ring-offset-shadow-color),
inset 0 0 0 calc((var(--_ring-offset-width) + var(--_ring-width)) * var(--_focus-in))
var(--_ring-fill, var(--ring-shadow-color));
box-shadow:
var(--_focus-shadow, 0 0 #0000),
var(--_shadow-resolved, var(--_button-shadow, var(--ui-button-shadow)));
cursor: default;
text-decoration: none;
/* transparent twin of the ring for forced-colors / high-contrast modes */
outline: abs(var(--_focus-ring-w, var(--outline-width))) var(--outline-style) transparent;
outline-offset: calc(
var(--outline-offset) - var(--_focus-in) *
(2 * var(--outline-offset) + abs(var(--_focus-ring-w, var(--outline-width))))
);
transition: var(--default-transition);
user-select: none;
text-box: trim-both ex alphabetic;
}
a[href] :where([data-ui~="button"]):not(:disabled, [aria-disabled="true"]),
a[href]:where([data-ui~="button"]):not(:disabled, [aria-disabled="true"]) {
cursor: pointer;
}
/* ===========================================================================
BUTTON STATES
=========================================================================== */
@media (hover: hover) {
:where([data-ui~="button"]):hover:not(:disabled, [aria-disabled="true"]) {
color: var(--_text-resolved, var(--_button-text--hover, var(--ui-button-text--hover)));
background-color: var(--_bg-resolved, var(--_button-bg--hover, var(--ui-button-bg--hover)));
border-color: var(
--_border-color-resolved,
var(--_button-border-color--hover, var(--ui-button-border-color--hover))
);
}
}
:where([data-ui~="button"]):active:not(:disabled, [aria-disabled="true"]) {
color: var(--_text-resolved, var(--_button-text--active, var(--ui-button-text--active)));
background-color: var(--_bg-resolved, var(--_button-bg--active, var(--ui-button-bg--active)));
border-color: var(
--_border-color-resolved,
var(--_button-border-color--active, var(--ui-button-border-color--active))
);
}
:where([data-ui~="button"]):focus-visible:not(:disabled, [aria-disabled="true"]) {
--_ring-offset-width: var(--ring-offset-width);
--_ring-width: abs(var(--_focus-ring-w, var(--ring-width)));
outline-color: transparent;
}
/* before the variants at the same specificity: a disabled primary keeps its color; only the default surface goes muted */
:where([data-ui~="button"]:is(:disabled, [aria-disabled="true"])) {
--_button-text: var(--color-muted-foreground);
--_button-bg: var(--color-muted);
--_button-shadow: 0 0 transparent;
--decoration-line: none;
cursor: default;
opacity: 0.5;
outline: none;
}
:where([data-ui~="button"])[aria-busy="true"] {
cursor: progress;
}
/* ===========================================================================
BUTTON ICONS
=========================================================================== */
:where([data-ui~="button"]) > svg {
inline-size: var(--ui-button-icon-size);
block-size: var(--ui-button-icon-size);
pointer-events: none;
justify-content: center;
align-items: center;
display: flex;
padding: 0;
}
/* ===========================================================================
BUTTON VARIANTS — data-button-variant writes the privates
=========================================================================== */
:where([data-ui~="button"][data-button-variant="outline"]) {
--_button-bg: transparent;
--_button-shadow: 0 0 transparent;
}
:where([data-ui~="button"][data-button-variant="primary"]) {
--_button-bg: var(--color-primary);
--_button-bg--hover: oklch(from var(--color-primary) l c h / 0.9);
--_button-bg--active: oklch(from var(--color-primary) l c h / 0.8);
--_button-text: var(--color-primary-foreground);
--_button-text--hover: var(--color-primary-foreground);
--_button-text--active: var(--color-primary-foreground);
--_button-border-color: transparent;
--_button-border-color--hover: transparent;
--_button-border-color--active: transparent;
--_button-shadow: 0 0 transparent;
}
:where([data-ui~="button"][data-button-variant="secondary"]) {
--_button-bg: var(--color-secondary);
--_button-bg--hover: oklch(from var(--color-secondary) l c h / 0.9);
--_button-bg--active: oklch(from var(--color-secondary) l c h / 0.8);
--_button-text: var(--color-secondary-foreground);
--_button-text--hover: var(--color-secondary-foreground);
--_button-text--active: var(--color-secondary-foreground);
--_button-border-color: transparent;
--_button-border-color--hover: transparent;
--_button-border-color--active: transparent;
--_button-shadow: 0 0 transparent;
}
:where([data-ui~="button"][data-button-variant="tertiary"]) {
--_button-bg: var(--color-tertiary);
--_button-bg--hover: oklch(from var(--color-tertiary) l c h / 0.9);
--_button-bg--active: oklch(from var(--color-tertiary) l c h / 0.8);
--_button-text: var(--color-tertiary-foreground);
--_button-text--hover: var(--color-tertiary-foreground);
--_button-text--active: var(--color-tertiary-foreground);
--_button-border-color: transparent;
--_button-border-color--hover: transparent;
--_button-border-color--active: transparent;
--_button-shadow: 0 0 transparent;
}
:where([data-ui~="button"][data-button-variant="muted"]) {
--_button-bg: var(--color-muted);
--_button-bg--hover: oklch(from var(--color-muted) l c h / calc(alpha - 0.02));
--_button-bg--active: var(--color-muted);
--_button-text: var(--color-foreground);
--_button-text--hover: var(--color-foreground);
--_button-text--active: var(--color-foreground);
--_button-border-color: transparent;
--_button-border-color--hover: transparent;
--_button-border-color--active: transparent;
--_button-shadow: 0 0 transparent;
}
:where([data-ui~="button"][data-button-variant="ghost"]) {
--_button-bg: transparent;
--_button-bg--hover: var(--color-muted);
--_button-bg--active: oklch(from var(--color-muted) l c h / calc(alpha - 0.02));
--_button-text: var(--color-foreground);
--_button-text--hover: var(--color-foreground);
--_button-text--active: var(--color-foreground);
--_button-border-color: transparent;
--_button-border-color--hover: transparent;
--_button-border-color--active: transparent;
--_button-shadow: 0 0 transparent;
}
/* Status variants (info, success, warning, destructive) share one soft
design: the status color at 10% alpha (20% in dark) as the surface, and
the status hue as the text with its lightness clamped (at most 0.45 in
light, at least 0.82 in dark) so it holds 4.5:1 on the tint and on the
hover and active steps; status-contrast.browser.test.ts checks it. No border. One rule derives the states from the
surface; each variant names only its colors. */
:where(
[data-ui~="button"]:is(
[data-button-variant="info"],
[data-button-variant="success"],
[data-button-variant="warning"],
[data-button-variant="destructive"]
)
) {
--_button-bg--hover: oklch(from var(--_button-bg) l c h / calc(alpha + 0.1));
--_button-bg--active: oklch(from var(--_button-bg) l c h / calc(alpha + 0.15));
--_button-text--hover: var(--_button-text);
--_button-text--active: var(--_button-text);
--_button-border-color: transparent;
--_button-border-color--hover: transparent;
--_button-border-color--active: transparent;
--_button-shadow: 0 0 transparent;
}
:where([data-ui~="button"][data-button-variant="info"]) {
--_button-ring-color: var(--color-info);
--_button-bg: light-dark(
oklch(from var(--color-info) l c h / 0.1),
oklch(from var(--color-info) l c h / 0.2)
);
--_button-text: light-dark(
oklch(from var(--color-info) min(l, 0.45) c h),
oklch(from var(--color-info) max(l, 0.82) c h)
);
}
:where([data-ui~="button"][data-button-variant="success"]) {
--_button-ring-color: var(--color-success);
--_button-bg: light-dark(
oklch(from var(--color-success) l c h / 0.1),
oklch(from var(--color-success) l c h / 0.2)
);
--_button-text: light-dark(
oklch(from var(--color-success) min(l, 0.45) c h),
oklch(from var(--color-success) max(l, 0.82) c h)
);
}
:where([data-ui~="button"][data-button-variant="warning"]) {
--_button-ring-color: var(--color-warning);
--_button-bg: light-dark(
oklch(from var(--color-warning) l c h / 0.1),
oklch(from var(--color-warning) l c h / 0.2)
);
--_button-text: light-dark(
oklch(from var(--color-warning) min(l, 0.45) c h),
oklch(from var(--color-warning) max(l, 0.82) c h)
);
}
:where([data-ui~="button"][data-button-variant="destructive"]) {
--_button-ring-color: var(--color-destructive);
--_button-bg: light-dark(
oklch(from var(--color-destructive) l c h / 0.1),
oklch(from var(--color-destructive) l c h / 0.2)
);
--_button-text: light-dark(
oklch(from var(--color-destructive) min(l, 0.45) c h),
oklch(from var(--color-destructive) max(l, 0.82) c h)
);
}
:where([data-ui~="button"][data-button-variant="link"]) {
--_button-bg: transparent;
--_button-bg--hover: transparent;
--_button-bg--active: transparent;
--_button-text: var(--color-foreground);
--_button-text--hover: oklch(from var(--color-primary) l c h / 0.6);
--_button-text--active: oklch(from var(--color-primary) l c h / 0.5);
--_button-border-style: none;
--_button-min-h: 0;
--_button-px: 0;
--_button-rounded: 0;
--_button-shadow: 0 0 transparent;
}
/* ===========================================================================
BUTTON SIZES — data-button-size
=========================================================================== */
:where([data-ui~="button"][data-button-size="sm"]) {
--_button-min-h: calc(var(--spacing) * 6);
--_button-px: calc(var(--spacing) * 2);
--_button-rounded: var(--radius-sm);
--_button-font-size: var(--font-size-xs);
}
:where(
:is(ui-button-group, [data-ui~="button-group"]):has([data-ui~="button"][data-button-size="sm"])
) {
--_button-group-rounded: var(--radius-sm);
}
:where([data-ui~="button"][data-button-size="icon"]) {
block-size: calc(var(--_min-h-len) + var(--_min-h-num) * var(--spacing));
inline-size: calc(var(--_min-h-len) + var(--_min-h-num) * var(--spacing));
padding: 0;
aspect-ratio: 1 / 1;
}
:where([data-ui~="button"][data-button-size="icon-sm"]) {
--_button-min-h: calc(var(--spacing) * 6);
--_button-px: calc(var(--spacing) * 2);
--_button-rounded: var(--radius-sm);
/* the hook, not a private: it must inherit to reach the icon */
--ui-button-icon-size: calc(var(--spacing) * 3.5);
block-size: calc(var(--_min-h-len) + var(--_min-h-num) * var(--spacing));
inline-size: calc(var(--_min-h-len) + var(--_min-h-num) * var(--spacing));
padding: 0;
aspect-ratio: 1 / 1;
}
/* ===========================================================================
BUTTON KBD
=========================================================================== */
/* Descendant (not child) so keys wrapped in a ui-kbd-group still resize */
:where([data-ui~="button"]) :where(kbd) {
--ui-kbd-min-w: calc(var(--spacing) * 4.5);
--ui-kbd-min-h: calc(var(--spacing) * 4.5);
--ui-kbd-icon-size: calc(var(--spacing) * 3);
}
:where([data-ui~="button"]):has(
> :where(kbd, ui-kbd-group, [data-ui~="kbd-group"]):last-child:not(:only-child)
) {
padding-inline-end: calc(var(--spacing) * 1.5);
}
:where([data-ui~="button"]):has(
> :where(kbd, ui-kbd-group, [data-ui~="kbd-group"]):first-child:not(:only-child)
) {
padding-inline-start: calc(var(--spacing) * 1.5);
}
:where([data-ui~="button"][data-button-size="sm"]):has(
> :where(kbd, ui-kbd-group, [data-ui~="kbd-group"]):last-child:not(:only-child)
) {
padding-inline-end: calc(var(--spacing) * 0.5);
}
:where([data-ui~="button"][data-button-size="sm"]):has(
> :where(kbd, ui-kbd-group, [data-ui~="kbd-group"]):first-child:not(:only-child)
) {
padding-inline-start: calc(var(--spacing) * 0.5);
}
}Icon
<div
style="
--display: flex;
--items: center;
--gap: 4;
"
>
<button data-ui="button" data-button-variant="primary" type="button">
Start free trial
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="none" />
<line
x1="40"
y1="128"
x2="216"
y2="128"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="16"
/>
<polyline
points="144 56 216 128 144 200"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="16"
/>
</svg>
</button>
<button data-ui="button" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="none" />
<path
d="M128,88a32,32,0,0,1,32-32h72V200H160a32,32,0,0,0-32,32"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="16"
/>
<path
d="M24,200H96a32,32,0,0,1,32,32V88A32,32,0,0,0,96,56H24Z"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="16"
/>
<line
x1="160"
y1="96"
x2="200"
y2="96"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="16"
/>
<line
x1="160"
y1="128"
x2="200"
y2="128"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="16"
/>
<line
x1="160"
y1="160"
x2="200"
y2="160"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="16"
/>
</svg>
Read article
</button>
</div>/**
* button.css — Button ([data-ui~="button"])
*
* @layer variables, components
* @requires _layers.css, _variables.css, _reset.css, _properties.css (--_*-resolved, --_focus-shadow), fields.css (--ui-field-* metrics), kbd.css (--ui-kbd-*)
* @uses color-mix(), oklch(from …) — variant hover/active tints
* @uses text-box — vertical trim (patchy browser support)
* @tokens --ui-button-* (@layer variables; hooks are named after the utilities they back; metrics default to the --ui-field-* family)
* @consumedby toggle.css, select.css, button-group.css, autocomplete.css, combobox.css,
* command.css, lightbox.css, menu.css, navigation-menu.css, popover.css
*
* Every utility-backed declaration reads one chain, highest first:
* var(--_<utility>-resolved, var(--_button-<utility>, var(--ui-button-<utility>)))
* The resolver is set by the utilities layer from the element's `style` gate
* (inline utilities and their tiers), the private by a variant or size on this
* element, and the hook by :root, a subtree, or an inline hook.
*/
@layer variables {
:root {
/* metrics follow --ui-field-* so buttons match adjacent fields; remap a
--ui-button-* token to diverge. Colors stay independent: a button is not a
field surface, and its border doesn't go primary on hover. */
--ui-button-ring-color: var(--ui-field-ring-color);
--ui-button-font-family: var(--font-family-body);
--ui-button-font-size: var(--font-size-sm);
--ui-button-font-weight: var(--font-weight-strong);
--ui-button-leading: 1;
--ui-button-bg: var(--color-background);
--ui-button-bg--hover: oklch(from var(--color-background) calc(l - 0.025) c h);
--ui-button-bg--active: oklch(from var(--color-background) calc(l - 0.05) c h);
--ui-button-text: var(--color-foreground);
--ui-button-text--hover: var(--color-foreground);
--ui-button-text--active: var(--color-foreground);
--ui-button-border-width: var(--ui-field-border-width);
--ui-button-border-style: var(--ui-field-border-style);
--ui-button-border-color: var(--ui-field-border-color);
--ui-button-border-color--hover: var(--ui-field-border-color);
--ui-button-border-color--active: var(--ui-field-border-color);
/* dual-mode: a scale number or a length */
--ui-button-gap: var(--spacing);
--ui-button-px: var(--ui-field-px);
--ui-button-min-h: var(--ui-field-h);
--ui-button-rounded: var(--ui-field-rounded);
--ui-button-icon-size: var(--ui-field-icon-size);
/* never `none`: the focus ring layers into this box-shadow list */
--ui-button-shadow: 0 0 transparent;
}
}
/* Variant privates: non-inheriting so a variant never cascades into a nested
button. Unset, they are guaranteed-invalid and the chain falls to the hook. */
@property --_button-ring-color {
syntax: "*";
inherits: false;
}
@property --_button-font-family {
syntax: "*";
inherits: false;
}
@property --_button-font-size {
syntax: "*";
inherits: false;
}
@property --_button-font-weight {
syntax: "*";
inherits: false;
}
@property --_button-leading {
syntax: "*";
inherits: false;
}
@property --_button-bg {
syntax: "*";
inherits: false;
}
@property --_button-bg--hover {
syntax: "*";
inherits: false;
}
@property --_button-bg--active {
syntax: "*";
inherits: false;
}
@property --_button-text {
syntax: "*";
inherits: false;
}
@property --_button-text--hover {
syntax: "*";
inherits: false;
}
@property --_button-text--active {
syntax: "*";
inherits: false;
}
@property --_button-border-width {
syntax: "*";
inherits: false;
}
@property --_button-border-style {
syntax: "*";
inherits: false;
}
@property --_button-border-color {
syntax: "*";
inherits: false;
}
@property --_button-border-color--hover {
syntax: "*";
inherits: false;
}
@property --_button-border-color--active {
syntax: "*";
inherits: false;
}
@property --_button-gap {
syntax: "*";
inherits: false;
}
@property --_button-px {
syntax: "*";
inherits: false;
}
@property --_button-min-h {
syntax: "*";
inherits: false;
}
@property --_button-rounded {
syntax: "*";
inherits: false;
}
@property --_button-icon-size {
syntax: "*";
inherits: false;
}
@property --_button-shadow {
syntax: "*";
inherits: false;
}
@layer ui.components {
:where([data-ui~="button"]) {
font-family: var(
--_font-family-resolved,
var(--_button-font-family, var(--ui-button-font-family))
);
font-size: var(--_font-size-resolved, var(--_button-font-size, var(--ui-button-font-size)));
font-weight: var(
--_font-weight-resolved,
var(--_button-font-weight, var(--ui-button-font-weight))
);
line-height: var(--_leading-resolved, var(--_button-leading, var(--ui-button-leading)));
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
--_gap-len: var(--_gap-resolved, var(--_button-gap, var(--ui-button-gap)));
--_gap-num: var(--_gap-resolved, var(--_button-gap, var(--ui-button-gap)));
column-gap: calc(var(--_gap-len) + var(--_gap-num) * var(--spacing));
--_min-h-len: var(--_min-h-resolved, var(--_button-min-h, var(--ui-button-min-h)));
--_min-h-num: var(--_min-h-resolved, var(--_button-min-h, var(--ui-button-min-h)));
min-block-size: calc(var(--_min-h-len) + var(--_min-h-num) * var(--spacing));
min-inline-size: max-content;
--_px-len: var(--_px-resolved, var(--_button-px, var(--ui-button-px)));
--_px-num: var(--_px-resolved, var(--_button-px, var(--ui-button-px)));
padding-inline: calc(var(--_px-len) + var(--_px-num) * var(--spacing));
border-width: var(
--_border-width-resolved,
var(--_button-border-width, var(--ui-button-border-width))
);
border-style: var(
--_border-style-resolved,
var(--_button-border-style, var(--ui-button-border-style))
);
border-color: var(
--_border-color-resolved,
var(--_button-border-color, var(--ui-button-border-color))
);
border-radius: var(--_rounded-resolved, var(--_button-rounded, var(--ui-button-rounded)));
color: var(--_text-resolved, var(--_button-text, var(--ui-button-text)));
background-color: var(--_bg-resolved, var(--_button-bg, var(--ui-button-bg)));
background-clip: padding-box;
white-space-collapse: collapse;
/* The ring is published to --_focus-shadow so the utilities layer's
box-shadow emission keeps it above a --shadow or --ring utility; the ring
widths grow from 0 on :focus-visible so the ring eases in. */
--_ring-offset-width: 0px;
--_ring-width: 0px;
--_focus-in: max(0, -1 * sign(var(--_focus-ring-w, 1px)));
--_ring-fill: color-mix(
in oklch,
var(--_ring-color-resolved, var(--_button-ring-color, var(--ui-button-ring-color)))
var(--ring-opacity),
transparent
);
--_focus-shadow:
0 0 0 calc(var(--_ring-offset-width) * (1 - var(--_focus-in))) var(--ring-offset-shadow-color),
0 0 0 calc((var(--_ring-offset-width) + var(--_ring-width)) * (1 - var(--_focus-in)))
var(--_ring-fill, var(--ring-shadow-color)),
inset 0 0 0 calc(var(--_ring-offset-width) * var(--_focus-in)) var(--ring-offset-shadow-color),
inset 0 0 0 calc((var(--_ring-offset-width) + var(--_ring-width)) * var(--_focus-in))
var(--_ring-fill, var(--ring-shadow-color));
box-shadow:
var(--_focus-shadow, 0 0 #0000),
var(--_shadow-resolved, var(--_button-shadow, var(--ui-button-shadow)));
cursor: default;
text-decoration: none;
/* transparent twin of the ring for forced-colors / high-contrast modes */
outline: abs(var(--_focus-ring-w, var(--outline-width))) var(--outline-style) transparent;
outline-offset: calc(
var(--outline-offset) - var(--_focus-in) *
(2 * var(--outline-offset) + abs(var(--_focus-ring-w, var(--outline-width))))
);
transition: var(--default-transition);
user-select: none;
text-box: trim-both ex alphabetic;
}
a[href] :where([data-ui~="button"]):not(:disabled, [aria-disabled="true"]),
a[href]:where([data-ui~="button"]):not(:disabled, [aria-disabled="true"]) {
cursor: pointer;
}
/* ===========================================================================
BUTTON STATES
=========================================================================== */
@media (hover: hover) {
:where([data-ui~="button"]):hover:not(:disabled, [aria-disabled="true"]) {
color: var(--_text-resolved, var(--_button-text--hover, var(--ui-button-text--hover)));
background-color: var(--_bg-resolved, var(--_button-bg--hover, var(--ui-button-bg--hover)));
border-color: var(
--_border-color-resolved,
var(--_button-border-color--hover, var(--ui-button-border-color--hover))
);
}
}
:where([data-ui~="button"]):active:not(:disabled, [aria-disabled="true"]) {
color: var(--_text-resolved, var(--_button-text--active, var(--ui-button-text--active)));
background-color: var(--_bg-resolved, var(--_button-bg--active, var(--ui-button-bg--active)));
border-color: var(
--_border-color-resolved,
var(--_button-border-color--active, var(--ui-button-border-color--active))
);
}
:where([data-ui~="button"]):focus-visible:not(:disabled, [aria-disabled="true"]) {
--_ring-offset-width: var(--ring-offset-width);
--_ring-width: abs(var(--_focus-ring-w, var(--ring-width)));
outline-color: transparent;
}
/* before the variants at the same specificity: a disabled primary keeps its color; only the default surface goes muted */
:where([data-ui~="button"]:is(:disabled, [aria-disabled="true"])) {
--_button-text: var(--color-muted-foreground);
--_button-bg: var(--color-muted);
--_button-shadow: 0 0 transparent;
--decoration-line: none;
cursor: default;
opacity: 0.5;
outline: none;
}
:where([data-ui~="button"])[aria-busy="true"] {
cursor: progress;
}
/* ===========================================================================
BUTTON ICONS
=========================================================================== */
:where([data-ui~="button"]) > svg {
inline-size: var(--ui-button-icon-size);
block-size: var(--ui-button-icon-size);
pointer-events: none;
justify-content: center;
align-items: center;
display: flex;
padding: 0;
}
/* ===========================================================================
BUTTON VARIANTS — data-button-variant writes the privates
=========================================================================== */
:where([data-ui~="button"][data-button-variant="outline"]) {
--_button-bg: transparent;
--_button-shadow: 0 0 transparent;
}
:where([data-ui~="button"][data-button-variant="primary"]) {
--_button-bg: var(--color-primary);
--_button-bg--hover: oklch(from var(--color-primary) l c h / 0.9);
--_button-bg--active: oklch(from var(--color-primary) l c h / 0.8);
--_button-text: var(--color-primary-foreground);
--_button-text--hover: var(--color-primary-foreground);
--_button-text--active: var(--color-primary-foreground);
--_button-border-color: transparent;
--_button-border-color--hover: transparent;
--_button-border-color--active: transparent;
--_button-shadow: 0 0 transparent;
}
:where([data-ui~="button"][data-button-variant="secondary"]) {
--_button-bg: var(--color-secondary);
--_button-bg--hover: oklch(from var(--color-secondary) l c h / 0.9);
--_button-bg--active: oklch(from var(--color-secondary) l c h / 0.8);
--_button-text: var(--color-secondary-foreground);
--_button-text--hover: var(--color-secondary-foreground);
--_button-text--active: var(--color-secondary-foreground);
--_button-border-color: transparent;
--_button-border-color--hover: transparent;
--_button-border-color--active: transparent;
--_button-shadow: 0 0 transparent;
}
:where([data-ui~="button"][data-button-variant="tertiary"]) {
--_button-bg: var(--color-tertiary);
--_button-bg--hover: oklch(from var(--color-tertiary) l c h / 0.9);
--_button-bg--active: oklch(from var(--color-tertiary) l c h / 0.8);
--_button-text: var(--color-tertiary-foreground);
--_button-text--hover: var(--color-tertiary-foreground);
--_button-text--active: var(--color-tertiary-foreground);
--_button-border-color: transparent;
--_button-border-color--hover: transparent;
--_button-border-color--active: transparent;
--_button-shadow: 0 0 transparent;
}
:where([data-ui~="button"][data-button-variant="muted"]) {
--_button-bg: var(--color-muted);
--_button-bg--hover: oklch(from var(--color-muted) l c h / calc(alpha - 0.02));
--_button-bg--active: var(--color-muted);
--_button-text: var(--color-foreground);
--_button-text--hover: var(--color-foreground);
--_button-text--active: var(--color-foreground);
--_button-border-color: transparent;
--_button-border-color--hover: transparent;
--_button-border-color--active: transparent;
--_button-shadow: 0 0 transparent;
}
:where([data-ui~="button"][data-button-variant="ghost"]) {
--_button-bg: transparent;
--_button-bg--hover: var(--color-muted);
--_button-bg--active: oklch(from var(--color-muted) l c h / calc(alpha - 0.02));
--_button-text: var(--color-foreground);
--_button-text--hover: var(--color-foreground);
--_button-text--active: var(--color-foreground);
--_button-border-color: transparent;
--_button-border-color--hover: transparent;
--_button-border-color--active: transparent;
--_button-shadow: 0 0 transparent;
}
/* Status variants (info, success, warning, destructive) share one soft
design: the status color at 10% alpha (20% in dark) as the surface, and
the status hue as the text with its lightness clamped (at most 0.45 in
light, at least 0.82 in dark) so it holds 4.5:1 on the tint and on the
hover and active steps; status-contrast.browser.test.ts checks it. No border. One rule derives the states from the
surface; each variant names only its colors. */
:where(
[data-ui~="button"]:is(
[data-button-variant="info"],
[data-button-variant="success"],
[data-button-variant="warning"],
[data-button-variant="destructive"]
)
) {
--_button-bg--hover: oklch(from var(--_button-bg) l c h / calc(alpha + 0.1));
--_button-bg--active: oklch(from var(--_button-bg) l c h / calc(alpha + 0.15));
--_button-text--hover: var(--_button-text);
--_button-text--active: var(--_button-text);
--_button-border-color: transparent;
--_button-border-color--hover: transparent;
--_button-border-color--active: transparent;
--_button-shadow: 0 0 transparent;
}
:where([data-ui~="button"][data-button-variant="info"]) {
--_button-ring-color: var(--color-info);
--_button-bg: light-dark(
oklch(from var(--color-info) l c h / 0.1),
oklch(from var(--color-info) l c h / 0.2)
);
--_button-text: light-dark(
oklch(from var(--color-info) min(l, 0.45) c h),
oklch(from var(--color-info) max(l, 0.82) c h)
);
}
:where([data-ui~="button"][data-button-variant="success"]) {
--_button-ring-color: var(--color-success);
--_button-bg: light-dark(
oklch(from var(--color-success) l c h / 0.1),
oklch(from var(--color-success) l c h / 0.2)
);
--_button-text: light-dark(
oklch(from var(--color-success) min(l, 0.45) c h),
oklch(from var(--color-success) max(l, 0.82) c h)
);
}
:where([data-ui~="button"][data-button-variant="warning"]) {
--_button-ring-color: var(--color-warning);
--_button-bg: light-dark(
oklch(from var(--color-warning) l c h / 0.1),
oklch(from var(--color-warning) l c h / 0.2)
);
--_button-text: light-dark(
oklch(from var(--color-warning) min(l, 0.45) c h),
oklch(from var(--color-warning) max(l, 0.82) c h)
);
}
:where([data-ui~="button"][data-button-variant="destructive"]) {
--_button-ring-color: var(--color-destructive);
--_button-bg: light-dark(
oklch(from var(--color-destructive) l c h / 0.1),
oklch(from var(--color-destructive) l c h / 0.2)
);
--_button-text: light-dark(
oklch(from var(--color-destructive) min(l, 0.45) c h),
oklch(from var(--color-destructive) max(l, 0.82) c h)
);
}
:where([data-ui~="button"][data-button-variant="link"]) {
--_button-bg: transparent;
--_button-bg--hover: transparent;
--_button-bg--active: transparent;
--_button-text: var(--color-foreground);
--_button-text--hover: oklch(from var(--color-primary) l c h / 0.6);
--_button-text--active: oklch(from var(--color-primary) l c h / 0.5);
--_button-border-style: none;
--_button-min-h: 0;
--_button-px: 0;
--_button-rounded: 0;
--_button-shadow: 0 0 transparent;
}
/* ===========================================================================
BUTTON SIZES — data-button-size
=========================================================================== */
:where([data-ui~="button"][data-button-size="sm"]) {
--_button-min-h: calc(var(--spacing) * 6);
--_button-px: calc(var(--spacing) * 2);
--_button-rounded: var(--radius-sm);
--_button-font-size: var(--font-size-xs);
}
:where(
:is(ui-button-group, [data-ui~="button-group"]):has([data-ui~="button"][data-button-size="sm"])
) {
--_button-group-rounded: var(--radius-sm);
}
:where([data-ui~="button"][data-button-size="icon"]) {
block-size: calc(var(--_min-h-len) + var(--_min-h-num) * var(--spacing));
inline-size: calc(var(--_min-h-len) + var(--_min-h-num) * var(--spacing));
padding: 0;
aspect-ratio: 1 / 1;
}
:where([data-ui~="button"][data-button-size="icon-sm"]) {
--_button-min-h: calc(var(--spacing) * 6);
--_button-px: calc(var(--spacing) * 2);
--_button-rounded: var(--radius-sm);
/* the hook, not a private: it must inherit to reach the icon */
--ui-button-icon-size: calc(var(--spacing) * 3.5);
block-size: calc(var(--_min-h-len) + var(--_min-h-num) * var(--spacing));
inline-size: calc(var(--_min-h-len) + var(--_min-h-num) * var(--spacing));
padding: 0;
aspect-ratio: 1 / 1;
}
/* ===========================================================================
BUTTON KBD
=========================================================================== */
/* Descendant (not child) so keys wrapped in a ui-kbd-group still resize */
:where([data-ui~="button"]) :where(kbd) {
--ui-kbd-min-w: calc(var(--spacing) * 4.5);
--ui-kbd-min-h: calc(var(--spacing) * 4.5);
--ui-kbd-icon-size: calc(var(--spacing) * 3);
}
:where([data-ui~="button"]):has(
> :where(kbd, ui-kbd-group, [data-ui~="kbd-group"]):last-child:not(:only-child)
) {
padding-inline-end: calc(var(--spacing) * 1.5);
}
:where([data-ui~="button"]):has(
> :where(kbd, ui-kbd-group, [data-ui~="kbd-group"]):first-child:not(:only-child)
) {
padding-inline-start: calc(var(--spacing) * 1.5);
}
:where([data-ui~="button"][data-button-size="sm"]):has(
> :where(kbd, ui-kbd-group, [data-ui~="kbd-group"]):last-child:not(:only-child)
) {
padding-inline-end: calc(var(--spacing) * 0.5);
}
:where([data-ui~="button"][data-button-size="sm"]):has(
> :where(kbd, ui-kbd-group, [data-ui~="kbd-group"]):first-child:not(:only-child)
) {
padding-inline-start: calc(var(--spacing) * 0.5);
}
}Icon only
<div
style="
--display: flex;
--items: center;
--gap: 4;
"
>
<button type="button" data-ui="button" data-button-size="icon" aria-label="Open menu">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<line x1="4" y1="6" x2="20" y2="6" />
<line x1="4" y1="12" x2="20" y2="12" />
<line x1="4" y1="18" x2="20" y2="18" />
</svg>
</button>
<button
type="button"
data-ui="button"
data-button-variant="primary"
data-button-size="icon"
aria-label="Open menu"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<line x1="4" y1="6" x2="20" y2="6" />
<line x1="4" y1="12" x2="20" y2="12" />
<line x1="4" y1="18" x2="20" y2="18" />
</svg>
</button>
<button
type="button"
data-ui="button"
data-button-variant="muted"
data-button-size="icon"
aria-label="Open menu"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<line x1="4" y1="6" x2="20" y2="6" />
<line x1="4" y1="12" x2="20" y2="12" />
<line x1="4" y1="18" x2="20" y2="18" />
</svg>
</button>
<button
type="button"
data-ui="button"
data-button-variant="ghost"
data-button-size="icon"
aria-label="Open menu"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<line x1="4" y1="6" x2="20" y2="6" />
<line x1="4" y1="12" x2="20" y2="12" />
<line x1="4" y1="18" x2="20" y2="18" />
</svg>
</button>
</div>/**
* button.css — Button ([data-ui~="button"])
*
* @layer variables, components
* @requires _layers.css, _variables.css, _reset.css, _properties.css (--_*-resolved, --_focus-shadow), fields.css (--ui-field-* metrics), kbd.css (--ui-kbd-*)
* @uses color-mix(), oklch(from …) — variant hover/active tints
* @uses text-box — vertical trim (patchy browser support)
* @tokens --ui-button-* (@layer variables; hooks are named after the utilities they back; metrics default to the --ui-field-* family)
* @consumedby toggle.css, select.css, button-group.css, autocomplete.css, combobox.css,
* command.css, lightbox.css, menu.css, navigation-menu.css, popover.css
*
* Every utility-backed declaration reads one chain, highest first:
* var(--_<utility>-resolved, var(--_button-<utility>, var(--ui-button-<utility>)))
* The resolver is set by the utilities layer from the element's `style` gate
* (inline utilities and their tiers), the private by a variant or size on this
* element, and the hook by :root, a subtree, or an inline hook.
*/
@layer variables {
:root {
/* metrics follow --ui-field-* so buttons match adjacent fields; remap a
--ui-button-* token to diverge. Colors stay independent: a button is not a
field surface, and its border doesn't go primary on hover. */
--ui-button-ring-color: var(--ui-field-ring-color);
--ui-button-font-family: var(--font-family-body);
--ui-button-font-size: var(--font-size-sm);
--ui-button-font-weight: var(--font-weight-strong);
--ui-button-leading: 1;
--ui-button-bg: var(--color-background);
--ui-button-bg--hover: oklch(from var(--color-background) calc(l - 0.025) c h);
--ui-button-bg--active: oklch(from var(--color-background) calc(l - 0.05) c h);
--ui-button-text: var(--color-foreground);
--ui-button-text--hover: var(--color-foreground);
--ui-button-text--active: var(--color-foreground);
--ui-button-border-width: var(--ui-field-border-width);
--ui-button-border-style: var(--ui-field-border-style);
--ui-button-border-color: var(--ui-field-border-color);
--ui-button-border-color--hover: var(--ui-field-border-color);
--ui-button-border-color--active: var(--ui-field-border-color);
/* dual-mode: a scale number or a length */
--ui-button-gap: var(--spacing);
--ui-button-px: var(--ui-field-px);
--ui-button-min-h: var(--ui-field-h);
--ui-button-rounded: var(--ui-field-rounded);
--ui-button-icon-size: var(--ui-field-icon-size);
/* never `none`: the focus ring layers into this box-shadow list */
--ui-button-shadow: 0 0 transparent;
}
}
/* Variant privates: non-inheriting so a variant never cascades into a nested
button. Unset, they are guaranteed-invalid and the chain falls to the hook. */
@property --_button-ring-color {
syntax: "*";
inherits: false;
}
@property --_button-font-family {
syntax: "*";
inherits: false;
}
@property --_button-font-size {
syntax: "*";
inherits: false;
}
@property --_button-font-weight {
syntax: "*";
inherits: false;
}
@property --_button-leading {
syntax: "*";
inherits: false;
}
@property --_button-bg {
syntax: "*";
inherits: false;
}
@property --_button-bg--hover {
syntax: "*";
inherits: false;
}
@property --_button-bg--active {
syntax: "*";
inherits: false;
}
@property --_button-text {
syntax: "*";
inherits: false;
}
@property --_button-text--hover {
syntax: "*";
inherits: false;
}
@property --_button-text--active {
syntax: "*";
inherits: false;
}
@property --_button-border-width {
syntax: "*";
inherits: false;
}
@property --_button-border-style {
syntax: "*";
inherits: false;
}
@property --_button-border-color {
syntax: "*";
inherits: false;
}
@property --_button-border-color--hover {
syntax: "*";
inherits: false;
}
@property --_button-border-color--active {
syntax: "*";
inherits: false;
}
@property --_button-gap {
syntax: "*";
inherits: false;
}
@property --_button-px {
syntax: "*";
inherits: false;
}
@property --_button-min-h {
syntax: "*";
inherits: false;
}
@property --_button-rounded {
syntax: "*";
inherits: false;
}
@property --_button-icon-size {
syntax: "*";
inherits: false;
}
@property --_button-shadow {
syntax: "*";
inherits: false;
}
@layer ui.components {
:where([data-ui~="button"]) {
font-family: var(
--_font-family-resolved,
var(--_button-font-family, var(--ui-button-font-family))
);
font-size: var(--_font-size-resolved, var(--_button-font-size, var(--ui-button-font-size)));
font-weight: var(
--_font-weight-resolved,
var(--_button-font-weight, var(--ui-button-font-weight))
);
line-height: var(--_leading-resolved, var(--_button-leading, var(--ui-button-leading)));
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
--_gap-len: var(--_gap-resolved, var(--_button-gap, var(--ui-button-gap)));
--_gap-num: var(--_gap-resolved, var(--_button-gap, var(--ui-button-gap)));
column-gap: calc(var(--_gap-len) + var(--_gap-num) * var(--spacing));
--_min-h-len: var(--_min-h-resolved, var(--_button-min-h, var(--ui-button-min-h)));
--_min-h-num: var(--_min-h-resolved, var(--_button-min-h, var(--ui-button-min-h)));
min-block-size: calc(var(--_min-h-len) + var(--_min-h-num) * var(--spacing));
min-inline-size: max-content;
--_px-len: var(--_px-resolved, var(--_button-px, var(--ui-button-px)));
--_px-num: var(--_px-resolved, var(--_button-px, var(--ui-button-px)));
padding-inline: calc(var(--_px-len) + var(--_px-num) * var(--spacing));
border-width: var(
--_border-width-resolved,
var(--_button-border-width, var(--ui-button-border-width))
);
border-style: var(
--_border-style-resolved,
var(--_button-border-style, var(--ui-button-border-style))
);
border-color: var(
--_border-color-resolved,
var(--_button-border-color, var(--ui-button-border-color))
);
border-radius: var(--_rounded-resolved, var(--_button-rounded, var(--ui-button-rounded)));
color: var(--_text-resolved, var(--_button-text, var(--ui-button-text)));
background-color: var(--_bg-resolved, var(--_button-bg, var(--ui-button-bg)));
background-clip: padding-box;
white-space-collapse: collapse;
/* The ring is published to --_focus-shadow so the utilities layer's
box-shadow emission keeps it above a --shadow or --ring utility; the ring
widths grow from 0 on :focus-visible so the ring eases in. */
--_ring-offset-width: 0px;
--_ring-width: 0px;
--_focus-in: max(0, -1 * sign(var(--_focus-ring-w, 1px)));
--_ring-fill: color-mix(
in oklch,
var(--_ring-color-resolved, var(--_button-ring-color, var(--ui-button-ring-color)))
var(--ring-opacity),
transparent
);
--_focus-shadow:
0 0 0 calc(var(--_ring-offset-width) * (1 - var(--_focus-in))) var(--ring-offset-shadow-color),
0 0 0 calc((var(--_ring-offset-width) + var(--_ring-width)) * (1 - var(--_focus-in)))
var(--_ring-fill, var(--ring-shadow-color)),
inset 0 0 0 calc(var(--_ring-offset-width) * var(--_focus-in)) var(--ring-offset-shadow-color),
inset 0 0 0 calc((var(--_ring-offset-width) + var(--_ring-width)) * var(--_focus-in))
var(--_ring-fill, var(--ring-shadow-color));
box-shadow:
var(--_focus-shadow, 0 0 #0000),
var(--_shadow-resolved, var(--_button-shadow, var(--ui-button-shadow)));
cursor: default;
text-decoration: none;
/* transparent twin of the ring for forced-colors / high-contrast modes */
outline: abs(var(--_focus-ring-w, var(--outline-width))) var(--outline-style) transparent;
outline-offset: calc(
var(--outline-offset) - var(--_focus-in) *
(2 * var(--outline-offset) + abs(var(--_focus-ring-w, var(--outline-width))))
);
transition: var(--default-transition);
user-select: none;
text-box: trim-both ex alphabetic;
}
a[href] :where([data-ui~="button"]):not(:disabled, [aria-disabled="true"]),
a[href]:where([data-ui~="button"]):not(:disabled, [aria-disabled="true"]) {
cursor: pointer;
}
/* ===========================================================================
BUTTON STATES
=========================================================================== */
@media (hover: hover) {
:where([data-ui~="button"]):hover:not(:disabled, [aria-disabled="true"]) {
color: var(--_text-resolved, var(--_button-text--hover, var(--ui-button-text--hover)));
background-color: var(--_bg-resolved, var(--_button-bg--hover, var(--ui-button-bg--hover)));
border-color: var(
--_border-color-resolved,
var(--_button-border-color--hover, var(--ui-button-border-color--hover))
);
}
}
:where([data-ui~="button"]):active:not(:disabled, [aria-disabled="true"]) {
color: var(--_text-resolved, var(--_button-text--active, var(--ui-button-text--active)));
background-color: var(--_bg-resolved, var(--_button-bg--active, var(--ui-button-bg--active)));
border-color: var(
--_border-color-resolved,
var(--_button-border-color--active, var(--ui-button-border-color--active))
);
}
:where([data-ui~="button"]):focus-visible:not(:disabled, [aria-disabled="true"]) {
--_ring-offset-width: var(--ring-offset-width);
--_ring-width: abs(var(--_focus-ring-w, var(--ring-width)));
outline-color: transparent;
}
/* before the variants at the same specificity: a disabled primary keeps its color; only the default surface goes muted */
:where([data-ui~="button"]:is(:disabled, [aria-disabled="true"])) {
--_button-text: var(--color-muted-foreground);
--_button-bg: var(--color-muted);
--_button-shadow: 0 0 transparent;
--decoration-line: none;
cursor: default;
opacity: 0.5;
outline: none;
}
:where([data-ui~="button"])[aria-busy="true"] {
cursor: progress;
}
/* ===========================================================================
BUTTON ICONS
=========================================================================== */
:where([data-ui~="button"]) > svg {
inline-size: var(--ui-button-icon-size);
block-size: var(--ui-button-icon-size);
pointer-events: none;
justify-content: center;
align-items: center;
display: flex;
padding: 0;
}
/* ===========================================================================
BUTTON VARIANTS — data-button-variant writes the privates
=========================================================================== */
:where([data-ui~="button"][data-button-variant="outline"]) {
--_button-bg: transparent;
--_button-shadow: 0 0 transparent;
}
:where([data-ui~="button"][data-button-variant="primary"]) {
--_button-bg: var(--color-primary);
--_button-bg--hover: oklch(from var(--color-primary) l c h / 0.9);
--_button-bg--active: oklch(from var(--color-primary) l c h / 0.8);
--_button-text: var(--color-primary-foreground);
--_button-text--hover: var(--color-primary-foreground);
--_button-text--active: var(--color-primary-foreground);
--_button-border-color: transparent;
--_button-border-color--hover: transparent;
--_button-border-color--active: transparent;
--_button-shadow: 0 0 transparent;
}
:where([data-ui~="button"][data-button-variant="secondary"]) {
--_button-bg: var(--color-secondary);
--_button-bg--hover: oklch(from var(--color-secondary) l c h / 0.9);
--_button-bg--active: oklch(from var(--color-secondary) l c h / 0.8);
--_button-text: var(--color-secondary-foreground);
--_button-text--hover: var(--color-secondary-foreground);
--_button-text--active: var(--color-secondary-foreground);
--_button-border-color: transparent;
--_button-border-color--hover: transparent;
--_button-border-color--active: transparent;
--_button-shadow: 0 0 transparent;
}
:where([data-ui~="button"][data-button-variant="tertiary"]) {
--_button-bg: var(--color-tertiary);
--_button-bg--hover: oklch(from var(--color-tertiary) l c h / 0.9);
--_button-bg--active: oklch(from var(--color-tertiary) l c h / 0.8);
--_button-text: var(--color-tertiary-foreground);
--_button-text--hover: var(--color-tertiary-foreground);
--_button-text--active: var(--color-tertiary-foreground);
--_button-border-color: transparent;
--_button-border-color--hover: transparent;
--_button-border-color--active: transparent;
--_button-shadow: 0 0 transparent;
}
:where([data-ui~="button"][data-button-variant="muted"]) {
--_button-bg: var(--color-muted);
--_button-bg--hover: oklch(from var(--color-muted) l c h / calc(alpha - 0.02));
--_button-bg--active: var(--color-muted);
--_button-text: var(--color-foreground);
--_button-text--hover: var(--color-foreground);
--_button-text--active: var(--color-foreground);
--_button-border-color: transparent;
--_button-border-color--hover: transparent;
--_button-border-color--active: transparent;
--_button-shadow: 0 0 transparent;
}
:where([data-ui~="button"][data-button-variant="ghost"]) {
--_button-bg: transparent;
--_button-bg--hover: var(--color-muted);
--_button-bg--active: oklch(from var(--color-muted) l c h / calc(alpha - 0.02));
--_button-text: var(--color-foreground);
--_button-text--hover: var(--color-foreground);
--_button-text--active: var(--color-foreground);
--_button-border-color: transparent;
--_button-border-color--hover: transparent;
--_button-border-color--active: transparent;
--_button-shadow: 0 0 transparent;
}
/* Status variants (info, success, warning, destructive) share one soft
design: the status color at 10% alpha (20% in dark) as the surface, and
the status hue as the text with its lightness clamped (at most 0.45 in
light, at least 0.82 in dark) so it holds 4.5:1 on the tint and on the
hover and active steps; status-contrast.browser.test.ts checks it. No border. One rule derives the states from the
surface; each variant names only its colors. */
:where(
[data-ui~="button"]:is(
[data-button-variant="info"],
[data-button-variant="success"],
[data-button-variant="warning"],
[data-button-variant="destructive"]
)
) {
--_button-bg--hover: oklch(from var(--_button-bg) l c h / calc(alpha + 0.1));
--_button-bg--active: oklch(from var(--_button-bg) l c h / calc(alpha + 0.15));
--_button-text--hover: var(--_button-text);
--_button-text--active: var(--_button-text);
--_button-border-color: transparent;
--_button-border-color--hover: transparent;
--_button-border-color--active: transparent;
--_button-shadow: 0 0 transparent;
}
:where([data-ui~="button"][data-button-variant="info"]) {
--_button-ring-color: var(--color-info);
--_button-bg: light-dark(
oklch(from var(--color-info) l c h / 0.1),
oklch(from var(--color-info) l c h / 0.2)
);
--_button-text: light-dark(
oklch(from var(--color-info) min(l, 0.45) c h),
oklch(from var(--color-info) max(l, 0.82) c h)
);
}
:where([data-ui~="button"][data-button-variant="success"]) {
--_button-ring-color: var(--color-success);
--_button-bg: light-dark(
oklch(from var(--color-success) l c h / 0.1),
oklch(from var(--color-success) l c h / 0.2)
);
--_button-text: light-dark(
oklch(from var(--color-success) min(l, 0.45) c h),
oklch(from var(--color-success) max(l, 0.82) c h)
);
}
:where([data-ui~="button"][data-button-variant="warning"]) {
--_button-ring-color: var(--color-warning);
--_button-bg: light-dark(
oklch(from var(--color-warning) l c h / 0.1),
oklch(from var(--color-warning) l c h / 0.2)
);
--_button-text: light-dark(
oklch(from var(--color-warning) min(l, 0.45) c h),
oklch(from var(--color-warning) max(l, 0.82) c h)
);
}
:where([data-ui~="button"][data-button-variant="destructive"]) {
--_button-ring-color: var(--color-destructive);
--_button-bg: light-dark(
oklch(from var(--color-destructive) l c h / 0.1),
oklch(from var(--color-destructive) l c h / 0.2)
);
--_button-text: light-dark(
oklch(from var(--color-destructive) min(l, 0.45) c h),
oklch(from var(--color-destructive) max(l, 0.82) c h)
);
}
:where([data-ui~="button"][data-button-variant="link"]) {
--_button-bg: transparent;
--_button-bg--hover: transparent;
--_button-bg--active: transparent;
--_button-text: var(--color-foreground);
--_button-text--hover: oklch(from var(--color-primary) l c h / 0.6);
--_button-text--active: oklch(from var(--color-primary) l c h / 0.5);
--_button-border-style: none;
--_button-min-h: 0;
--_button-px: 0;
--_button-rounded: 0;
--_button-shadow: 0 0 transparent;
}
/* ===========================================================================
BUTTON SIZES — data-button-size
=========================================================================== */
:where([data-ui~="button"][data-button-size="sm"]) {
--_button-min-h: calc(var(--spacing) * 6);
--_button-px: calc(var(--spacing) * 2);
--_button-rounded: var(--radius-sm);
--_button-font-size: var(--font-size-xs);
}
:where(
:is(ui-button-group, [data-ui~="button-group"]):has([data-ui~="button"][data-button-size="sm"])
) {
--_button-group-rounded: var(--radius-sm);
}
:where([data-ui~="button"][data-button-size="icon"]) {
block-size: calc(var(--_min-h-len) + var(--_min-h-num) * var(--spacing));
inline-size: calc(var(--_min-h-len) + var(--_min-h-num) * var(--spacing));
padding: 0;
aspect-ratio: 1 / 1;
}
:where([data-ui~="button"][data-button-size="icon-sm"]) {
--_button-min-h: calc(var(--spacing) * 6);
--_button-px: calc(var(--spacing) * 2);
--_button-rounded: var(--radius-sm);
/* the hook, not a private: it must inherit to reach the icon */
--ui-button-icon-size: calc(var(--spacing) * 3.5);
block-size: calc(var(--_min-h-len) + var(--_min-h-num) * var(--spacing));
inline-size: calc(var(--_min-h-len) + var(--_min-h-num) * var(--spacing));
padding: 0;
aspect-ratio: 1 / 1;
}
/* ===========================================================================
BUTTON KBD
=========================================================================== */
/* Descendant (not child) so keys wrapped in a ui-kbd-group still resize */
:where([data-ui~="button"]) :where(kbd) {
--ui-kbd-min-w: calc(var(--spacing) * 4.5);
--ui-kbd-min-h: calc(var(--spacing) * 4.5);
--ui-kbd-icon-size: calc(var(--spacing) * 3);
}
:where([data-ui~="button"]):has(
> :where(kbd, ui-kbd-group, [data-ui~="kbd-group"]):last-child:not(:only-child)
) {
padding-inline-end: calc(var(--spacing) * 1.5);
}
:where([data-ui~="button"]):has(
> :where(kbd, ui-kbd-group, [data-ui~="kbd-group"]):first-child:not(:only-child)
) {
padding-inline-start: calc(var(--spacing) * 1.5);
}
:where([data-ui~="button"][data-button-size="sm"]):has(
> :where(kbd, ui-kbd-group, [data-ui~="kbd-group"]):last-child:not(:only-child)
) {
padding-inline-end: calc(var(--spacing) * 0.5);
}
:where([data-ui~="button"][data-button-size="sm"]):has(
> :where(kbd, ui-kbd-group, [data-ui~="kbd-group"]):first-child:not(:only-child)
) {
padding-inline-start: calc(var(--spacing) * 0.5);
}
}Attributes
| Attribute | Kind | Values | Notes |
|---|---|---|---|
data-ui="button" | Identity | free value | Identity token in data-ui. |
data-button-size | Preset | icon icon-sm sm | Preset: a named bundle of values. |
data-button-variant | Preset | destructive ghost info link muted outline primary secondary success tertiary warning | Preset: a named bundle of values. |
Hooks
Set a hook on :root, on a subtree, or inline on one element. A state hook (--…--hover) themes that state only; the primitive keeps its other states.
| Hook | Default | State |
|---|---|---|
--ui-button-bg | var(--color-background) | — |
--ui-button-bg--active | oklch(from var(--color-background) calc(l - 0.05) c h) | active |
--ui-button-bg--hover | oklch(from var(--color-background) calc(l - 0.025) c h) | hover |
--ui-button-border-color | var(--ui-field-border-color) | — |
--ui-button-border-color--active | var(--ui-field-border-color) | active |
--ui-button-border-color--hover | var(--ui-field-border-color) | hover |
--ui-button-border-style | var(--ui-field-border-style) | — |
--ui-button-border-width | var(--ui-field-border-width) | — |
--ui-button-font-family | var(--font-family-body) | — |
--ui-button-font-size | var(--font-size-sm) | — |
--ui-button-font-weight | var(--font-weight-strong) | — |
--ui-button-gap | var(--spacing) | — |
--ui-button-icon-size | var(--ui-field-icon-size) | — |
--ui-button-leading | 1 | — |
--ui-button-min-h | var(--ui-field-h) | — |
--ui-button-px | var(--ui-field-px) | — |
--ui-button-ring-color | var(--ui-field-ring-color) | — |
--ui-button-rounded | var(--ui-field-rounded) | — |
--ui-button-shadow | 0 0 transparent | — |
--ui-button-text | var(--color-foreground) | — |
--ui-button-text--active | var(--color-foreground) | active |
--ui-button-text--hover | var(--color-foreground) | hover |