Skip to content

Type to search the docs, API, and blog.

↑↓ navigate↵ openesc close
Zazz 0.7 alpha
API navigation

API

Button

Actions and links as buttons: variants, sizes, and icons.

Installation

pnpm dlx zazz-ui add button
npx zazz-ui add button
yarn dlx zazz-ui add button
bunx zazz-ui add button

Also 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

button attributes
AttributeKindValuesNotes
data-ui="button"Identityfree valueIdentity token in data-ui.
data-button-sizePreseticon icon-sm smPreset: a named bundle of values.
data-button-variantPresetdestructive ghost info link muted outline primary secondary success tertiary warningPreset: a named bundle of values.

Requires: kbd, fields.

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.

button hooks
HookDefaultState
--ui-button-bgvar(--color-background)—
--ui-button-bg--activeoklch(from var(--color-background) calc(l - 0.05) c h)active
--ui-button-bg--hoveroklch(from var(--color-background) calc(l - 0.025) c h)hover
--ui-button-border-colorvar(--ui-field-border-color)—
--ui-button-border-color--activevar(--ui-field-border-color)active
--ui-button-border-color--hovervar(--ui-field-border-color)hover
--ui-button-border-stylevar(--ui-field-border-style)—
--ui-button-border-widthvar(--ui-field-border-width)—
--ui-button-font-familyvar(--font-family-body)—
--ui-button-font-sizevar(--font-size-sm)—
--ui-button-font-weightvar(--font-weight-strong)—
--ui-button-gapvar(--spacing)—
--ui-button-icon-sizevar(--ui-field-icon-size)—
--ui-button-leading1—
--ui-button-min-hvar(--ui-field-h)—
--ui-button-pxvar(--ui-field-px)—
--ui-button-ring-colorvar(--ui-field-ring-color)—
--ui-button-roundedvar(--ui-field-rounded)—
--ui-button-shadow0 0 transparent—
--ui-button-textvar(--color-foreground)—
--ui-button-text--activevar(--color-foreground)active
--ui-button-text--hovervar(--color-foreground)hover