@cf/button
Button
The clickable action primitive, in six variants and seven sizes, that every other control here builds its buttons from.
Button is Base UI's Button with shadcn's variants on top. Every size carries touch-manipulation, which stops a double-tap zooming the page and costs nothing elsewhere.
What it doesn't do is grow small buttons to 44px. I tried that and it made dense rows worse, so isolated icon buttons opt in (the chat header uses size-11 md:size-8) and dense rows get real height below md from their own layout. It still uses forwardRef, which I haven't moved to ref-as-a-prop yet.
import { Button } from "@/components/ui/button";
import { TrashIcon } from "@phosphor-icons/react";
<Button aria-label="Delete note" size="icon-sm" variant="ghost">
<TrashIcon data-icon />
</Button>Props
| Prop | Type | Default |
|---|---|---|
| variant | default | secondary | outline | ghost | link | destructive | default |
| size | xs | sm | default | lg | icon | icon-sm | icon-xs | default |
Rules
The same rules ship in DESIGN.md and the MCP server, for the agents building with this.
- must
Use
variant="destructive"for an action that deletes or can't be undone, rather than restylingdefaultwith a red class.The destructive variant is the one signal the notes UI, menus and alert dialogs all share for danger.
destructive-variant
- must
Give every
size="icon",size="icon-sm"orsize="icon-xs"button an accessible name, eitheraria-labelor a<span className="sr-only">child.icon-only-needs-name
- should
Put
data-icon(ordata-icon="inline-start") on a child icon so the button sizes it tosize-4, instead of sizing the icon with its own classes.The button styles icons through
[&_[data-icon]], so an unmarked icon keeps Phosphor's default 1em and drifts from its neighbours.mark-icons-with-data-icon
- should
Give an isolated small button a 44px tap target below
mdyourself, withtouch-targetorclassName="size-11 md:size-8"; don't expect the button to grow on its own.Hit-area expansion is left out on purpose, because in tight rows like the
gap-0.5editor menus neighbours would steal each other's edges.opt-in-tap-target
Install
bunx shadcn@latest add @cf/buttonNeeds the @cf registry in your components.json once. The theme and any sibling components come along automatically.
Source
button.tsxShowHide
"use client";
import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "cn";
import * as React from "react";
/**
* `touch-manipulation` on every button: it is what stops a double-tap zooming
* the page, and it costs nothing anywhere else.
*
* Hit-area expansion is deliberately *not* here. Growing every button to 44px
* with an overlay would have neighbours stealing each other's edges wherever
* buttons sit close together (the editor menus are `gap-0.5`), which trades one
* mis-tap for another. Isolated small controls opt in with `touch-target`
* instead; dense rows get real height below `md`.
*/
const buttonVariants = cva(
"inline-flex touch-manipulation items-center justify-center whitespace-nowrap rounded-md font-medium text-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_[data-icon]]:size-4 [&_[data-icon]]:shrink-0",
{
defaultVariants: { size: "default", variant: "default" },
variants: {
size: {
default: "h-9 px-4 py-2",
icon: "h-9 w-9",
"icon-sm": "size-8",
"icon-xs": "size-6",
lg: "h-10 rounded-md px-8",
sm: "h-8 rounded-md px-3 text-xs",
xs: "h-6 rounded-md px-2 text-xs",
},
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
},
},
}
);
type ButtonProps = React.ComponentProps<typeof ButtonPrimitive> &
VariantProps<typeof buttonVariants>;
const Button = React.forwardRef<HTMLElement, ButtonProps>(
({ className, size, variant, ...props }, ref) => (
<ButtonPrimitive
className={cn(buttonVariants({ className, size, variant }))}
ref={ref}
{...props}
/>
)
);
Button.displayName = "Button";
export { Button, type ButtonProps, buttonVariants };