Skip to content
Primitiv home
Framework
Consumption mode

Button

stableSource Figma

A clickable action — a button, or a link styled as one via asChild.

Playground

Density

Preview

Size
Variant
import { Button } from "@/components/ui/button";
<Button size="md" variant="primary">Save changes</Button>

Density is set by a data-density ancestor — the Context system, not a Button prop.

Installation

npx primitiv add button

Import

import { Button } from "@/components/ui/button";

Copied into your project as .primitiv-button — you own the file afterwards, so upgrades are opt-in.

Headless mode installs the npm package instead: @primitiv-ui/react

Props

Button

Extends HTMLButtonElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRenders the child element instead of a native <button>, merging all props — aria-, data-, event handlers, ref — onto it via Slot. type is not forwarded in this mode; the child owns its own type semantics. See the asChild example on Button.
childrenReactNodeheadlessButton content. Under asChild, becomes the single child element Slot merges props onto.
refRef<HTMLButtonElement>headlessAllows getting a ref to the component instance. Once the component unmounts, React will set ref.current to null (or call the ref with null if you passed a callback ref). Forwarded to the underlying HTMLButtonElement, or — under asChild — merged onto the rendered child via Slot.
type"button" | "submit" | "reset""button"headlessThe button's native type attribute, restricted to the three valid values. Defaults to "button" (not the DOM's own default of "submit"), so a Button placed inside a <form> never triggers an accidental submit unless set explicitly.
variant"primary" | "secondary" | "danger" | "ghost" | "link"primarystyledVisual intent / emphasis.
size"xs" | "sm" | "md" | "lg" | "xl"mdstyledControl size; data-density scales each size further.

Styling contract

--primitiv-button-bg--primitiv-button-fg--primitiv-button-border-color--primitiv-button-border-width--primitiv-button-radius--primitiv-button-height--primitiv-button-padding-inline--primitiv-button-gap--primitiv-button-icon-size--primitiv-button-icon-optical-inset--primitiv-button-font-family--primitiv-button-font-size--primitiv-button-font-weight--primitiv-button-line-height--primitiv-button-shadow

Data attributes

Button

className: .primitiv-button

AttributeValueWhen
data-disabled""disabled

Accessibility

  • Space and Enter both activate the button — the native <button> behaviour, kept rather than re-implemented.
  • A visible focus ring is drawn via :focus-visible, so it appears for keyboard users and not on mouse click.
  • disabled exposes both the native attribute and data-disabled, so pointer events stop and assistive tech reports the state.
  • Under asChild the a11y wiring transfers to the element you supply, so a rendered <a> keeps role="link" rather than being announced as a button.

Examples

Variants

Five intents. primary carries the main action on a view; link reads as text so it keeps the button's hit area and focus ring.

Density
import { Button } from "@/components/ui/button";
<div data-density="comfortable">  <Button variant="primary">Primary</Button>  <Button variant="secondary">Secondary</Button>  <Button variant="danger">Danger</Button>  <Button variant="ghost">Ghost</Button>  <Button variant="link">Link</Button></div>

Sizes

Five sizes — and every one rescales again with the nearest data-density ancestor. Change the density above and watch the whole ramp shift: that is the Context system, not a Button prop.

Density
import { Button } from "@/components/ui/button";
<div data-density="comfortable">  <Button size="xs">Button xs</Button>  <Button size="sm">Button sm</Button>  <Button size="md">Button md</Button>  <Button size="lg">Button lg</Button>  <Button size="xl">Button xl</Button></div>

With icons

Icons are children, not props, so a leading or trailing glyph is just composition — nothing to configure.

Density
import { Button } from "@/components/ui/button";import { ArrowRight, Plus } from "@primitiv-ui/icons";
<Button>  <Plus />  Add item</Button>
<Button variant="secondary">  Continue  <ArrowRight /></Button>

asChild renders your own element instead of a <button> — here a Next <Link> — merging the button props and styles onto it. The a11y wiring follows, so it stays role="link".

Density
import Link from "next/link";import { Button } from "@/components/ui/button";import { ArrowRight } from "@primitiv-ui/icons";
<Button asChild>  <Link href="/components">    View the docs    <ArrowRight />  </Link></Button>

Disabled

disabled sets the native attribute and exposes data-disabled, so pointer events stop and the focus ring is suppressed — no ARIA needed, because the real attribute is doing the work.

Density
import { Button } from "@/components/ui/button";
<Button disabled>Save changes</Button><Button variant="secondary" disabled>Cancel</Button>