ButtonDesign system
ComponentSite and product

Button

Product buttons and site pill buttons, with variants for primary, secondary, and contextual actions.

npx context-ds add button
On the site
In the product

Usage

Use it for

Every action. In the product one brand (blue) per screen for the main action, outline (the default) for the rest, primary (black) to save, ghost for quiet actions, on-brand on a blue panel; md (40px, the default) for a form's or page's main action, sm (32px) in toolbars and cards. On the site one pill (black) per section, pill-outline for the rest, pill-on-brand on blue.

Avoid

  • Two blue main actions on one product screen
  • Pills inside the product
  • Bold labels

Dependencies: @radix-ui/react-slot, class-variance-authority

Example

examples/button.tsx
"use client";import { useState } from "react";import { ArrowRight, MoreVertical, Plus } from "lucide-react";import { Button } from "@/components/ui/button";export function Product() {  const [message, setMessage] = useState("");  return (    <div className="flex flex-col gap-4">      <div className="flex flex-wrap items-center gap-2">        <Button variant="brand" onClick={() => setMessage("New monitor selected.")}>          <Plus /> New monitor        </Button>        <Button variant="primary" onClick={() => setMessage("Changes saved.")}>Save</Button>        <Button onClick={() => setMessage("Changes cancelled.")}>Cancel</Button>        <Button variant="outline-brand" onClick={() => setMessage("Upgrade selected.")}>Upgrade</Button>        <Button variant="ghost" onClick={() => setMessage("Step skipped.")}>Skip</Button>        <Button variant="ghost" size="icon" aria-label="Row actions" onClick={() => setMessage("Row actions selected.")}>          <MoreVertical strokeWidth={1.5} />        </Button>      </div>      <div className="flex flex-wrap items-center gap-2">        <Button variant="brand" size="sm" onClick={() => setMessage("Create key selected.")}>          <Plus /> Create key        </Button>        <Button size="sm" onClick={() => { const link = document.createElement("a"); link.href = URL.createObjectURL(new Blob(["name,status\nProduction,active\n"], { type: "text/csv" })); link.download = "example.csv"; link.click(); URL.revokeObjectURL(link.href); setMessage("Example exported."); }}>Export</Button>        <Button variant="ghost" size="sm" onClick={() => setMessage("")}>          Clear        </Button>      </div>      <div className="flex flex-wrap items-center gap-2 rounded-card bg-brand p-4">        <Button variant="on-brand" asChild><a href="mailto:hello@context.dev">Talk to us</a></Button>      </div>      <p role="status" className="text-body-sm text-fg-muted empty:hidden">{message}</p>    </div>  );}export function Site() {  return (    <div className="flex flex-wrap items-center gap-3">      <Button variant="pill" asChild><a href="https://context.dev/signup" target="_blank" rel="noreferrer">Start for free</a></Button>      <Button variant="pill-outline" asChild><a href="https://docs.context.dev" target="_blank" rel="noreferrer">Read the docs <ArrowRight /></a></Button>      <Button variant="pill-outline" size="sm" asChild><a href="https://context.dev" target="_blank" rel="noreferrer">Explore</a></Button>      <div className="rounded-card bg-brand p-4">        <Button variant="pill-on-brand" asChild><a href="https://cal.com/context-dev/30min" target="_blank" rel="noreferrer">Book a demo</a></Button>      </div>    </div>  );}

Source

components/ui/button.tsx
import * as React from "react";import { Slot } from "@radix-ui/react-slot";import { cva, type VariantProps } from "class-variance-authority";import { cn } from "@/lib/utils";// One button for both surfaces. The product's variants and sizes are the dashboard's (button-class-name.ts): black// `primary`, brand blue `brand` for the one main action, `outline` (the default), `outline-brand`, `ghost` and// `on-brand`, all with control corners and a small press. The site's pills (brand-outline-pill.ts) are the other three:// the black pill, the blue outline pill and the white outline pill on blue.const buttonVariants = cva(  "inline-flex shrink-0 items-center justify-center gap-1.5 whitespace-nowrap font-normal outline-none select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-focus-ring active:scale-[0.97] disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none motion-reduce:active:scale-100 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",  {    variants: {      variant: {        primary: "rounded-control bg-primary text-on-primary hover:bg-neutral-90",        brand: "rounded-control bg-brand text-on-brand hover:bg-blue-90",        outline: "rounded-control border border-line-strong text-fg hover:border-fg",        "outline-brand": "rounded-control border border-brand text-fg-brand hover:bg-blue-10",        ghost: "rounded-control text-fg-muted hover:bg-wash-hover hover:text-fg data-[state=open]:bg-wash-hover data-[state=open]:text-fg",        "on-brand": "rounded-control border border-white text-white hover:bg-white/10",        pill: "rounded-pill border border-black bg-black text-white hover:bg-neutral-80",        "pill-outline": "rounded-pill border border-brand bg-white text-fg-brand hover:bg-blue-10",        "pill-on-brand": "rounded-pill border border-white text-white hover:bg-white/12",      },      size: {        sm: "h-8 px-3.5 text-body-sm",        // The main action on a form or page (the dashboard's Support button).        md: "h-10 px-4.5 text-body-sm",        // A square icon button, the size of the dashboard's row menu and dialog close buttons.        icon: "size-8 px-0 [&_svg:not([class*='size-'])]:size-4",      },    },    compoundVariants: [      { variant: ["primary", "brand", "outline", "outline-brand", "ghost", "on-brand"], className: "transition-[color,background-color,border-color,scale] duration-150 ease-out" },      { variant: "ghost", size: "icon", className: "text-fg-subtle" },      // The site's pills are sized by their padding, at body size, as on the brand's pages; `sm` is the smaller pill      // the brand's forms and lists use.      { variant: ["pill", "pill-outline", "pill-on-brand"], className: "transition-[color,background-color,border-color,scale] duration-200 ease-out" },      { variant: ["pill", "pill-outline", "pill-on-brand"], size: "md", className: "h-auto px-5 py-3 text-body leading-snug" },      { variant: ["pill", "pill-outline", "pill-on-brand"], size: "sm", className: "h-auto px-4 py-2 text-body-sm leading-snug" },    ],    defaultVariants: { variant: "outline", size: "md" },  },);type ButtonProps = React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean };function Button({ className, variant, size, asChild = false, type, ...props }: ButtonProps) {  const Comp = asChild ? Slot : "button";  return <Comp data-slot="button" type={asChild ? type : (type ?? "button")} className={cn(buttonVariants({ variant, size, className }))} {...props} />;}export { Button, buttonVariants };

Reference: context-dashboard c0ddf7a src/components/ui/button.tsx; website src/components/sections/shared/brand-outline-pill.ts.