Button
A clickable action, in six variants and four sizes.
import {Button} from '@/components/ui/button'import {ArrowUpIcon} from 'lucide-react'export function ButtonDemo() { return ( <Button> Button <ArrowUpIcon /> </Button> )}Installation
Install with whichever CLI your project already has. The nat-ui CLI and manual routes assume @nat-ui/cli init has run, because the files below import cn from your utils alias and are styled against the theme tokens it writes. The shadcn route needs none of that — shadcn init has already written both.
pnpm dlx shadcn@latest add https://nat-ui-delta.vercel.app/s/button.jsonUsage
import {Button} from '@/components/ui/button'<Button variant='outline'>Button</Button>Default
import {Button} from '@/components/ui/button'export function ButtonDefault() { return <Button>Button</Button>}Secondary
import {Button} from '@/components/ui/button'export function ButtonSecondary() { return <Button variant='secondary'>Secondary</Button>}Destructive
import {Button} from '@/components/ui/button'export function ButtonDestructive() { return <Button variant='destructive'>Destructive</Button>}Outline
import {Button} from '@/components/ui/button'export function ButtonOutline() { return <Button variant='outline'>Outline</Button>}Ghost
import {Button} from '@/components/ui/button'export function ButtonGhost() { return <Button variant='ghost'>Ghost</Button>}Link
import {Button} from '@/components/ui/button'export function ButtonLink() { return <Button variant='link'>Link</Button>}Size
import {Button} from '@/components/ui/button'export function ButtonSize() { return ( <div className='flex flex-wrap items-center gap-3'> <Button size='sm'>Small</Button> <Button>Default</Button> <Button size='lg'>Large</Button> </div> )}Icon
An icon button is square. Give it an aria-label, because the icon carries the
meaning and there is no text for a screen reader to announce.
import {Button} from '@/components/ui/button'import {ChevronRightIcon} from 'lucide-react'export function ButtonIcon() { return ( <Button variant='outline' size='icon' aria-label='Next'> <ChevronRightIcon /> </Button> )}Disabled
import {Button} from '@/components/ui/button'export function ButtonDisabled() { return <Button disabled>Disabled</Button>}Animation
Every nat-ui component takes an animation prop. It is independent of
variant and size, so appearance and feel vary separately.
Each preset sets both how long a press takes and how deep it goes, because overshoot is a proportion of the distance travelled — a spring that bounces half as far again as a 4% press moves less than a pixel and reads as no bounce at all.
'use client'import {Button} from '@/components/ui/button'import { type AnimationPreset, DURATIONS_MS, PRESS_SCALES, transitionStyle,} from '@/lib/motion'import {usePrefersReducedMotion} from '@/lib/use-motion'import {useState} from 'react'const PRESETS = ['smooth', 'snappy', 'bouncy'] as const/** * One trigger drives all three, because comparing presets by pressing three * separate buttons means comparing the third against a memory of the first. */export function ButtonAnimation() { const [pressed, setPressed] = useState(false) const reduced = usePrefersReducedMotion() const release = () => { setPressed(false) } return ( <div className='flex flex-col items-center gap-6 py-4'> <div className='flex flex-wrap items-end justify-center gap-8'> {PRESETS.map((name) => { const preset: AnimationPreset = reduced ? 'none' : name return ( <div key={name} className='flex flex-col items-center gap-2'> <div className='bg-primary text-primary-foreground flex h-9 items-center rounded-md px-4 text-sm font-medium shadow' style={{ ...transitionStyle(preset), transform: `scale(${String(pressed ? PRESS_SCALES[preset] : 1)})`, }} > {name} </div> <span className='text-fd-muted-foreground font-mono text-xs'> {DURATIONS_MS[preset]}ms </span> </div> ) })} </div> {/* Keyboard handlers alongside pointer ones: a press you can only reach with a mouse leaves keyboard users with three static rectangles. */} <Button onBlur={release} onKeyDown={(event) => { if (event.key === ' ' || event.key === 'Enter') setPressed(true) }} onKeyUp={release} onPointerDown={() => { setPressed(true) }} onPointerLeave={release} onPointerUp={release} > Hold to press all three </Button> </div> )}API Reference
| Prop | Values | Default |
|---|---|---|
variant | default, destructive, outline, secondary, ghost, link | default |
size | sm, default, lg, icon | default |
animation | smooth, snappy, bouncy, none | snappy |
| Preset | Settles in | Presses to | Overshoot |
|---|---|---|---|
smooth | 387ms | 0.96 | none |
snappy | 200ms | 0.94 | slight |
bouncy | 960ms | 0.88 | pronounced |
none | instant | 1 | none |
Every other prop is forwarded to Base UI's Button. className is narrowed to
a string: Base UI also accepts a function of the component's state, but the
variants here decide their classes without it.
buttonVariants lives in its own button-variants.tsx, not button.tsx.
Button needs 'use client' for the animation prop's hooks, and that
directive makes every export of the module a client reference — so a plain
class-name function like buttonVariants has to sit somewhere a Server
Component can still call it directly, such as to style a <Link> as a
button.
import {buttonVariants} from '@/components/ui/button-variants'buttonVariants carries the press scale but not the timing that turns it into
a press rather than a jump, since that timing is what the animation prop
picks. pressStyle supplies both:
import {buttonVariants} from '@/components/ui/button-variants'
import {pressStyle} from '@/lib/motion'
;<Link href='/docs' className={buttonVariants()} style={pressStyle('snappy')}>
Get started
</Link>This works in a Server Component. lib/motion.ts holds the presets and carries
no directive; the reduced-motion hooks live in lib/use-motion.ts, which does.