Button
A pill-shaped action in four tones: primary, secondary, accent and ghost.
Installation
$ pnpm dlx shadcn@latest add @ssntl/buttonUsage
import { Button } from "@/components/ui/button"
<Button>Book a call</Button>
<Button variant="secondary">Send email</Button>
<Button variant="accent">Publish</Button>
<Button asChild variant="ghost">
<a href="/pricing">See pricing</a>
</Button>- primary is the default call to action: one per view.
- accent is only for the commit action (Publish, Send, Pay). Never two on one screen.
- ghost for Cancel, Skip and tertiary actions.
- Sizes: md 48px (same as inputs), sm 36px for dense rows, lg 56px for heroes.
- Verb first, sentence case, two or three words. No all caps and no icons on both sides.
- Icon-only buttons need an aria-label. Mark a text button's icon with data-icon for optical padding.
Code
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
// Essential Design `es-btn`: a pill, 48px by default, that scales to 0.96 on
// press. Hover only exists on pointer devices (Tailwind v4 `hover:`).
const buttonVariants = cva(
[
"inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-full border border-transparent font-sans font-medium whitespace-nowrap select-none",
"transition-[scale,background-color,color] duration-(--duration-fast) ease-out active:scale-(--es-press) motion-reduce:active:scale-100",
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
"disabled:pointer-events-none disabled:opacity-40 aria-disabled:pointer-events-none aria-disabled:opacity-40",
"[&_svg]:pointer-events-none [&_svg]:shrink-0",
],
{
variants: {
variant: {
primary: "bg-primary text-primary-foreground hover:bg-primary/85",
secondary:
"bg-secondary text-secondary-foreground hover:bg-neutral-200 dark:hover:bg-neutral-600",
// The one commit action of a tool (Publish, Send, Pay). Never two per screen.
accent: "bg-blue-600 text-white shadow-brand hover:bg-blue-600/90",
ghost:
"bg-transparent px-4 text-muted-foreground hover:bg-secondary hover:text-foreground",
},
size: {
// Optical alignment: the side with an icon (data-icon) takes 4px less
// padding, so text and icon read as centered.
sm: "h-9 px-4 text-caption font-medium has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3 [&_svg]:size-4",
md: "h-12 px-6 text-label has-data-[icon=inline-end]:pr-5 has-data-[icon=inline-start]:pl-5 [&_svg]:size-5",
lg: "h-14 px-8 text-body font-medium has-data-[icon=inline-end]:pr-7 has-data-[icon=inline-start]:pl-7 [&_svg]:size-5",
"icon-sm": "size-9 [&_svg]:size-4",
icon: "size-12 [&_svg]:size-5",
"icon-lg": "size-14 [&_svg]:size-5",
},
},
defaultVariants: {
variant: "primary",
size: "md",
},
}
)
type ButtonProps = React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
/** Render the child element (a link, for example) with the button's styles. */
asChild?: boolean
}
function Button({
className,
variant,
size,
asChild = false,
type,
...props
}: ButtonProps) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="button"
data-variant={variant ?? "primary"}
type={asChild ? type : (type ?? "button")}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants, type ButtonProps }