For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to any URL or sending an Accept: text/markdown header. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Button

A composed action primitive with a loading state, icon slots, semantic variants, and compact sizes.

Installation

$ pnpm dlx shadcn@latest add https://lab.pratikthapw.dev/r/button.json

The composed API (loading, swap, leftIcon, rightIcon, and children with data-icon) is complete as installed; no extra pieces are required.

Usage

import { Button } from "@/components/ui/button";
 
export function Actions() {
  return (
    <div className="flex flex-wrap gap-3">
      <Button>Save changes</Button>
      <Button variant="secondary">Preview</Button>
      <Button variant="outline">Cancel</Button>
      <Button variant="destructive">Delete</Button>
    </div>
  );
}

Examples

Variants

Use semantic variants to communicate action hierarchy. Avoid choosing a variant only for its color.

import { Button } from "@/components/ui/button";

const variants = [
  "default",
  "secondary",
  "outline",
  "ghost",
  "destructive",
  "link",
] as const;

export const ButtonVariantsExample = () => (
  <div className="border-border bg-card flex flex-wrap items-center gap-3 rounded-xl border p-6">
    {variants.map((variant) => (
      <Button key={variant} variant={variant}>
        {variant[0]?.toUpperCase()}
        {variant.slice(1)}
      </Button>
    ))}
  </div>
);

Left icon

leftIcon accepts any node and renders it in a dedicated data-icon="inline-start" slot, so spacing stays predictable. When loading is true, the spinner replaces the left icon in place, keeping width stable.

import { ArrowRightIcon, CheckIcon } from "@/components/ui/icons";
import { RenderIcon } from "@/components/ui/render-icon";
import { Button } from "@/components/ui/button";

export const ButtonLeftIconExample = () => (
  <div className="border-border bg-card flex flex-wrap items-center gap-3 rounded-xl border p-6">
    <Button leftIcon={<RenderIcon icon={ArrowRightIcon} />}>Continue</Button>
    <Button leftIcon={<RenderIcon icon={CheckIcon} />} variant="outline">
      Approve
    </Button>
    <Button
      leftIcon={<RenderIcon icon={ArrowRightIcon} />}
      loading
      variant="secondary"
    >
      Saving changes
    </Button>
  </div>
);
<Button leftIcon={<ArrowRightIcon />}>Continue</Button>
<Button leftIcon={<CheckIcon />} variant="outline">Approve</Button>

Right icon

rightIcon mirrors the left slot at data-icon="inline-end". Use it for onward actions and dismissive affordances; the right slot never swaps for the loading spinner, so trailing icons stay visible while loading.

import { ArrowRightIcon, BellIcon } from "@/components/ui/icons";
import { RenderIcon } from "@/components/ui/render-icon";
import { Button } from "@/components/ui/button";

export const ButtonRightIconExample = () => (
  <div className="border-border bg-card flex flex-wrap items-center gap-3 rounded-xl border p-6">
    <Button rightIcon={<RenderIcon icon={ArrowRightIcon} />}>Next step</Button>
    <Button rightIcon={<RenderIcon icon={BellIcon} />} variant="outline">
      Notify me
    </Button>
    <Button rightIcon={<RenderIcon icon={ArrowRightIcon} />} variant="ghost">
      Skip
    </Button>
  </div>
);
<Button rightIcon={<ArrowRightIcon />}>Next step</Button>
<Button rightIcon={<BellIcon />} variant="outline">Notify me</Button>

Loading

loading disables the button and shows a spinner. When leftIcon is set, the spinner replaces it in place; otherwise it renders at inline-start.

"use client";

import { Button } from "@/components/ui/button";

export const ButtonLoadingExample = () => (
  <div className="border-border bg-card flex flex-wrap items-center gap-3 rounded-xl border p-6">
    <Button loading>Saving changes</Button>
    <Button loading variant="outline">
      Cancel
    </Button>
    <Button aria-label="Loading" loading size="icon" variant="secondary" />
  </div>
);
<Button loading>Saving changes</Button>
<Button leftIcon={<ArrowRightIcon />} loading>Continue</Button>
<Button aria-label="Loading" loading size="icon" variant="secondary" />

Swap

With no icons set, pass swap to stack the label and spinner in the same cell: the label becomes invisible and the spinner takes its place, so the button width never shifts mid-flight.

"use client";

import { useState } from "react";

import { Button } from "@/components/ui/button";

export const ButtonSwapExample = () => {
  const [loading, setLoading] = useState(true);

  return (
    <div className="border-border bg-card flex flex-wrap items-center gap-3 rounded-xl border p-6">
      <Button loading={loading} swap>
        Save changes
      </Button>
      <Button
        onClick={() => setLoading((value) => !value)}
        size="sm"
        variant="outline"
      >
        {loading ? "Set idle" : "Set loading"}
      </Button>
    </div>
  );
};
<Button loading={isPending} swap>
  Save changes
</Button>

swap only applies when neither leftIcon nor rightIcon is set; with icons present the in-place replacement already keeps width stable.

You can still place icons as ordinary children and tag them with data-icon="inline-start" or data-icon="inline-end"; the props above are sugar over that same contract.

API

Props

PropTypeDefault
variant"default" | "secondary" | "outline" | "ghost" | "destructive" | "link""default"
size"default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg""default"
loadingbooleanfalse
leftIconReact.ReactNodeNone
rightIconReact.ReactNodeNone
swapbooleanfalse

When loading is true, the button is disabled and a private spinner renders: in place of leftIcon when set, inside the swap cell when swap is on, or at inline-start otherwise. Icon-only buttons still need an accessible name via aria-label or visually hidden text.

Polymorphism

Button uses Base UI's render prop and nativeButton. Pass render={<a />} only when the rendered element is not a native button, and set nativeButton={false} accordingly.

For links that look like buttons, prefer the exported buttonVariants helper with a plain <a> instead of rendering a link through the button. This keeps the semantic link role intact.

import { buttonVariants } from "@/components/ui/button";
 
<a className={buttonVariants({ variant: "outline" })} href="/pricing">
  Pricing
</a>;