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.json

Usage

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>}
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.

smooth
387ms
snappy
200ms
bouncy
960ms
'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

PropValuesDefault
variantdefault, destructive, outline, secondary, ghost, linkdefault
sizesm, default, lg, icondefault
animationsmooth, snappy, bouncy, nonesnappy
PresetSettles inPresses toOvershoot
smooth387ms0.96none
snappy200ms0.94slight
bouncy960ms0.88pronounced
noneinstant1none

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.

On this page