Actions
Button
Triggers an action. Eight variants, including an AI variant for actions that run a model.
npx prodular-ui add buttonVariants
One primary button per view. Secondary and outline for everything else.
import { Button } from "@/components/ui/button"
export default function ButtonVariants() { return ( <div className="flex flex-wrap items-center justify-center gap-3"> <Button>Save changes</Button> <Button variant="secondary">Duplicate</Button> <Button variant="outline">Export</Button> <Button variant="ghost">Cancel</Button> <Button variant="destructive">Delete project</Button> <Button variant="link">Learn more</Button> </div> )}AI
For actions that call a model. The animated gradient tells people the result is generated.
import { SparklesIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
export default function ButtonAi() { return ( <div className="flex flex-wrap items-center justify-center gap-3"> <Button variant="ai"> <SparklesIcon data-icon="inline-start" /> Generate summary </Button> <Button variant="ai" size="icon" aria-label="Ask AI"> <SparklesIcon /> </Button> </div> )}Sizes
import { PlusIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
export default function ButtonSizes() { return ( <div className="flex flex-wrap items-center justify-center gap-3"> <Button size="xs">Extra small</Button> <Button size="sm">Small</Button> <Button>Default</Button> <Button size="lg">Large</Button> <Button size="xl">Extra large</Button> <Button size="icon" variant="outline" aria-label="Add"> <PlusIcon /> </Button> </div> )}With icons
Mark the icon with data-icon so the padding balances.
import { ArrowRightIcon, DownloadIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
export default function ButtonIcons() { return ( <div className="flex flex-wrap items-center justify-center gap-3"> <Button variant="outline"> <DownloadIcon data-icon="inline-start" /> Download CSV </Button> <Button> Continue <ArrowRightIcon data-icon="inline-end" /> </Button> </div> )}Loading
The label stays in place, so the button keeps its width.
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
export default function ButtonLoading() { const [loading, setLoading] = React.useState(false) return ( <div className="flex items-center justify-center gap-3"> <Button loading={loading} onClick={() => { setLoading(true) setTimeout(() => setLoading(false), 1600) }} > Publish release </Button> <Button variant="outline" loading> Syncing </Button> </div> )}Anatomy
<Button variant="outline" size="sm"> <DownloadIcon data-icon="inline-start" /> Export</Button>Guidance
Use it when
- Use default for the single most important action in a view.
- Use ai when pressing the button runs a model and the output is generated, not retrieved.
- Use destructive only for the final confirmation of an irreversible action.
Avoid
- Two default buttons side by side. Make one of them outline or secondary.
- The ai variant for ordinary actions that only mention AI in the label.
Rules for AI agents
Published in llms-full.txt and the registry, so coding agents follow them when they build with this component.
- Label buttons with a verb and an object: Save changes, Export CSV. Never OK or Submit alone.
- Pass loading while a request is pending instead of swapping in a spinner yourself.
- Icon-only buttons need aria-label.
API
Button
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "default" | "secondary" | "outline" | "ghost" | "destructive" | "destructive-subtle" | "ai" | "link" | "default" | Visual style. |
| size | "xs" | "sm" | "default" | "lg" | "xl" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | "default" | Height and padding. Icon sizes are square. |
| loading | boolean | false | Shows a spinner, sets aria-busy and blocks clicks. |
| render | ReactElement | Renders the button as another element, for example a link. |