Button
A flexible button component that supports multiple variants, sizes, and states. Built with Ark UI factory for accessibility and styled using Ocean UI design tokens.
Example
import { Button } from "@/components/ui/button";
export default function ButtonDemo() {
return (
<div className="flex flex-wrap gap-4">
<Button>Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="link">Link</Button>
</div>
);
}
How to install and use
Complete the manual installation setup
If you haven't already completed the first 4 steps of the manual installation guide, please do so before continuing to install these components.
Create a button.tsx file and paste the following code into it.
"use client";
import { cva, type VariantProps } from "class-variance-authority";
import { ark } from "@ark-ui/react/factory";
import { Loader2 } from "lucide-react";
import type { ComponentProps, ReactNode } from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-semibold transition-all outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-5 [&_svg]:shrink-0",
{
variants: {
variant: {
primary:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
tertiary: "text-muted-foreground hover:bg-muted hover:text-foreground",
outline:
"border border-border bg-transparent text-foreground hover:bg-muted",
ghost: "text-foreground hover:bg-muted",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
sm: "h-8 px-3 py-2 has-[>svg]:px-2.5",
md: "h-9 px-3.5 py-2.5 has-[>svg]:px-3",
lg: "h-10 px-4 py-2.5 has-[>svg]:px-3.5",
xl: "h-11 px-4.5 py-3 has-[>svg]:px-4",
},
},
defaultVariants: {
variant: "primary",
size: "md",
},
}
);
export interface ButtonProps
extends ComponentProps<typeof ark.button>,
VariantProps<typeof buttonVariants> {
/**
* Icon or element to display before the button text
*/
iconLeading?: ReactNode;
/**
* Icon or element to display after the button text
*/
iconTrailing?: ReactNode;
/**
* Show loading spinner and disable button
* @default false
*/
loading?: boolean;
/**
* The text to show while loading
*/
loadingText?: ReactNode;
/**
* Keep text visible during loading state
* @default false
*/
showTextWhileLoading?: boolean;
}
function Button({
variant,
size,
className,
children,
iconLeading,
iconTrailing,
loading = false,
loadingText,
showTextWhileLoading = false,
disabled,
...props
}: ButtonProps) {
const isDisabled = disabled || loading;
const isIconOnly = (iconLeading || iconTrailing) && !children;
return (
<ark.button
type="button"
className={cn(
buttonVariants({ variant, size }),
loading && !showTextWhileLoading && "relative",
isIconOnly && size === "sm" && "p-2",
isIconOnly && size === "md" && "p-2.5",
isIconOnly && size === "lg" && "p-3",
isIconOnly && size === "xl" && "p-3.5",
className
)}
disabled={isDisabled}
aria-busy={loading}
{...props}
>
{loading && (
<Loader2
className={cn(
"animate-spin",
!showTextWhileLoading &&
"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
)}
aria-hidden="true"
/>
)}
{iconLeading && !loading && iconLeading}
{children && (
<span className={loading && !showTextWhileLoading ? "invisible" : ""}>
{loading && loadingText ? loadingText : children}
</span>
)}
{iconTrailing && !loading && iconTrailing}
</ark.button>
);
}
export { Button, buttonVariants };
"use client";
import { cva, type VariantProps } from "class-variance-authority";
import { ark } from "@ark-ui/react/factory";
import { Loader2 } from "lucide-react";
import type { ComponentProps, ReactNode } from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-semibold transition-all outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-5 [&_svg]:shrink-0",
{
variants: {
variant: {
primary:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
tertiary: "text-muted-foreground hover:bg-muted hover:text-foreground",
outline:
"border border-border bg-transparent text-foreground hover:bg-muted",
ghost: "text-foreground hover:bg-muted",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
sm: "h-8 px-3 py-2 has-[>svg]:px-2.5",
md: "h-9 px-3.5 py-2.5 has-[>svg]:px-3",
lg: "h-10 px-4 py-2.5 has-[>svg]:px-3.5",
xl: "h-11 px-4.5 py-3 has-[>svg]:px-4",
},
},
defaultVariants: {
variant: "primary",
size: "md",
},
}
);
export interface ButtonProps
extends ComponentProps<typeof ark.button>,
VariantProps<typeof buttonVariants> {
/**
* Icon or element to display before the button text
*/
iconLeading?: ReactNode;
/**
* Icon or element to display after the button text
*/
iconTrailing?: ReactNode;
/**
* Show loading spinner and disable button
* @default false
*/
loading?: boolean;
/**
* The text to show while loading
*/
loadingText?: ReactNode;
/**
* Keep text visible during loading state
* @default false
*/
showTextWhileLoading?: boolean;
}
function Button({
variant,
size,
className,
children,
iconLeading,
iconTrailing,
loading = false,
loadingText,
showTextWhileLoading = false,
disabled,
...props
}: ButtonProps) {
const isDisabled = disabled || loading;
const isIconOnly = (iconLeading || iconTrailing) && !children;
return (
<ark.button
type="button"
className={cn(
buttonVariants({ variant, size }),
loading && !showTextWhileLoading && "relative",
isIconOnly && size === "sm" && "p-2",
isIconOnly && size === "md" && "p-2.5",
isIconOnly && size === "lg" && "p-3",
isIconOnly && size === "xl" && "p-3.5",
className
)}
disabled={isDisabled}
aria-busy={loading}
{...props}
>
{loading && (
<Loader2
className={cn(
"animate-spin",
!showTextWhileLoading &&
"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
)}
aria-hidden="true"
/>
)}
{iconLeading && !loading && iconLeading}
{children && (
<span className={loading && !showTextWhileLoading ? "invisible" : ""}>
{loading && loadingText ? loadingText : children}
</span>
)}
{iconTrailing && !loading && iconTrailing}
</ark.button>
);
}
export { Button, buttonVariants };
"use client";
import { cva, type VariantProps } from "class-variance-authority";
import { ark } from "@ark-ui/react/factory";
import { Loader2 } from "lucide-react";
import type { ComponentProps, ReactNode } from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-semibold transition-all outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-5 [&_svg]:shrink-0",
{
variants: {
variant: {
primary:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
tertiary: "text-muted-foreground hover:bg-muted hover:text-foreground",
outline:
"border border-border bg-transparent text-foreground hover:bg-muted",
ghost: "text-foreground hover:bg-muted",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
sm: "h-8 px-3 py-2 has-[>svg]:px-2.5",
md: "h-9 px-3.5 py-2.5 has-[>svg]:px-3",
lg: "h-10 px-4 py-2.5 has-[>svg]:px-3.5",
xl: "h-11 px-4.5 py-3 has-[>svg]:px-4",
},
},
defaultVariants: {
variant: "primary",
size: "md",
},
}
);
export interface ButtonProps
extends ComponentProps<typeof ark.button>,
VariantProps<typeof buttonVariants> {
/**
* Icon or element to display before the button text
*/
iconLeading?: ReactNode;
/**
* Icon or element to display after the button text
*/
iconTrailing?: ReactNode;
/**
* Show loading spinner and disable button
* @default false
*/
loading?: boolean;
/**
* The text to show while loading
*/
loadingText?: ReactNode;
/**
* Keep text visible during loading state
* @default false
*/
showTextWhileLoading?: boolean;
}
function Button({
variant,
size,
className,
children,
iconLeading,
iconTrailing,
loading = false,
loadingText,
showTextWhileLoading = false,
disabled,
...props
}: ButtonProps) {
const isDisabled = disabled || loading;
const isIconOnly = (iconLeading || iconTrailing) && !children;
return (
<ark.button
type="button"
className={cn(
buttonVariants({ variant, size }),
loading && !showTextWhileLoading && "relative",
isIconOnly && size === "sm" && "p-2",
isIconOnly && size === "md" && "p-2.5",
isIconOnly && size === "lg" && "p-3",
isIconOnly && size === "xl" && "p-3.5",
className
)}
disabled={isDisabled}
aria-busy={loading}
{...props}
>
{loading && (
<Loader2
className={cn(
"animate-spin",
!showTextWhileLoading &&
"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
)}
aria-hidden="true"
/>
)}
{iconLeading && !loading && iconLeading}
{children && (
<span className={loading && !showTextWhileLoading ? "invisible" : ""}>
{loading && loadingText ? loadingText : children}
</span>
)}
{iconTrailing && !loading && iconTrailing}
</ark.button>
);
}
export { Button, buttonVariants };
"use client";
import { cva, type VariantProps } from "class-variance-authority";
import { ark } from "@ark-ui/react/factory";
import { Loader2 } from "lucide-react";
import type { ComponentProps, ReactNode } from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-semibold transition-all outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-5 [&_svg]:shrink-0",
{
variants: {
variant: {
primary:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
tertiary: "text-muted-foreground hover:bg-muted hover:text-foreground",
outline:
"border border-border bg-transparent text-foreground hover:bg-muted",
ghost: "text-foreground hover:bg-muted",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
sm: "h-8 px-3 py-2 has-[>svg]:px-2.5",
md: "h-9 px-3.5 py-2.5 has-[>svg]:px-3",
lg: "h-10 px-4 py-2.5 has-[>svg]:px-3.5",
xl: "h-11 px-4.5 py-3 has-[>svg]:px-4",
},
},
defaultVariants: {
variant: "primary",
size: "md",
},
}
);
export interface ButtonProps
extends ComponentProps<typeof ark.button>,
VariantProps<typeof buttonVariants> {
/**
* Icon or element to display before the button text
*/
iconLeading?: ReactNode;
/**
* Icon or element to display after the button text
*/
iconTrailing?: ReactNode;
/**
* Show loading spinner and disable button
* @default false
*/
loading?: boolean;
/**
* The text to show while loading
*/
loadingText?: ReactNode;
/**
* Keep text visible during loading state
* @default false
*/
showTextWhileLoading?: boolean;
}
function Button({
variant,
size,
className,
children,
iconLeading,
iconTrailing,
loading = false,
loadingText,
showTextWhileLoading = false,
disabled,
...props
}: ButtonProps) {
const isDisabled = disabled || loading;
const isIconOnly = (iconLeading || iconTrailing) && !children;
return (
<ark.button
type="button"
className={cn(
buttonVariants({ variant, size }),
loading && !showTextWhileLoading && "relative",
isIconOnly && size === "sm" && "p-2",
isIconOnly && size === "md" && "p-2.5",
isIconOnly && size === "lg" && "p-3",
isIconOnly && size === "xl" && "p-3.5",
className
)}
disabled={isDisabled}
aria-busy={loading}
{...props}
>
{loading && (
<Loader2
className={cn(
"animate-spin",
!showTextWhileLoading &&
"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
)}
aria-hidden="true"
/>
)}
{iconLeading && !loading && iconLeading}
{children && (
<span className={loading && !showTextWhileLoading ? "invisible" : ""}>
{loading && loadingText ? loadingText : children}
</span>
)}
{iconTrailing && !loading && iconTrailing}
</ark.button>
);
}
export { Button, buttonVariants };
Finally, Choose any example you like and add it to your project.
For instance, create a new file at components/shared/{example-component-name}.tsx, paste the example code into that file, and then import and use the component wherever you need it in your application.
The Button component uses Ark UI's factory function (ark.button) for
built-in accessibility. It's styled with Tailwind CSS using Ocean UI design
tokens from tokens.css.
Overview
The Button component is a fundamental UI element for triggering actions. It supports multiple visual styles, sizes, loading states, and icon placement. Built on top of Ark UI's factory function, it provides native button accessibility out of the box.
Variants
The Button component supports seven visual variants:
- primary: Main action button with brand color background
- secondary: Secondary action with subtle background and border
- tertiary: Tertiary action with tertiary text color and hover background
- outline: Outlined button with transparent background
- ghost: Minimal button with no background, only text
- destructive: For destructive actions (delete, remove)
- link: Text button styled as a link
Sizes
The Button component supports four sizes:
- sm: Small button (height: 32px)
- md: Medium button (height: 36px) - default
- lg: Large button (height: 40px)
- xl: Extra large button (height: 44px)
Features
- Icons: Support for leading and trailing icons
- Loading State: Built-in loading spinner with optional loading text
- Disabled State: Proper disabled styling and behavior
- Accessibility: Full keyboard navigation and ARIA support via Ark UI
- Customizable: All styles can be overridden via
classNameprop
Select Examples
Primary
Primary variant buttons are used for main actions. They feature a brand color background and are the most prominent buttons in the interface.
import { Button } from "@/components/ui/button";
const Circle = () => (
<span className="size-5 rounded-full border-2 border-current" />
);
export default function ButtonPrimary() {
return (
<div className="flex gap-4">
<Button size="md">Button CTA</Button>
<Button size="md" disabled>
Button CTA
</Button>
<Button loading showTextWhileLoading size="md">
Submitting...
</Button>
<Button size="md" iconLeading={<Circle />} iconTrailing={<Circle />}>
Button CTA
</Button>
</div>
);
}
Secondary
Secondary variant buttons are used for secondary actions. They have a subtle background and border, providing a less prominent alternative to primary buttons.
import { Button } from "@/components/ui/button";
const Circle = () => (
<span className="size-5 rounded-full border-2 border-current" />
);
export default function ButtonSecondary() {
return (
<div>
<div className="mb-4 flex gap-8">
<Button variant="secondary" size="sm">
Button CTA
</Button>
<Button variant="secondary" size="md">
Button CTA
</Button>
<Button variant="secondary" size="lg">
Button CTA
</Button>
<Button variant="secondary" size="xl">
Button CTA
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button variant="secondary" size="sm" disabled>
Button CTA
</Button>
<Button variant="secondary" size="md" disabled>
Button CTA
</Button>
<Button variant="secondary" size="lg" disabled>
Button CTA
</Button>
<Button variant="secondary" size="xl" disabled>
Button CTA
</Button>
</div>
<div className="mb-16 flex gap-8">
<Button variant="secondary" loading showTextWhileLoading size="sm">
Submitting...
</Button>
<Button variant="secondary" loading showTextWhileLoading size="md">
Submitting...
</Button>
<Button variant="secondary" loading showTextWhileLoading size="lg">
Submitting...
</Button>
<Button variant="secondary" loading showTextWhileLoading size="xl">
Submitting...
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button
variant="secondary"
size="sm"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="secondary"
size="md"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="secondary"
size="lg"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="secondary"
size="xl"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button
variant="secondary"
size="sm"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="secondary"
size="md"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="secondary"
size="lg"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="secondary"
size="xl"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
</div>
<div className="mb-16 flex gap-8">
<Button
variant="secondary"
loading
showTextWhileLoading
size="sm"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Submitting...
</Button>
<Button
variant="secondary"
loading
showTextWhileLoading
size="md"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Submitting...
</Button>
<Button
variant="secondary"
loading
showTextWhileLoading
size="lg"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Submitting...
</Button>
<Button
variant="secondary"
loading
showTextWhileLoading
size="xl"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Submitting...
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button variant="secondary" size="sm" iconLeading={<Circle />} />
<Button variant="secondary" size="md" iconLeading={<Circle />} />
<Button variant="secondary" size="lg" iconLeading={<Circle />} />
<Button variant="secondary" size="xl" iconLeading={<Circle />} />
</div>
<div className="mb-4 flex gap-8">
<Button variant="secondary" size="sm" disabled iconLeading={<Circle />} />
<Button variant="secondary" size="md" disabled iconLeading={<Circle />} />
<Button variant="secondary" size="lg" disabled iconLeading={<Circle />} />
<Button variant="secondary" size="xl" disabled iconLeading={<Circle />} />
</div>
<div className="flex gap-8">
<Button
variant="secondary"
loading
size="sm"
iconLeading={<Circle />}
aria-label="Loading"
/>
<Button
variant="secondary"
loading
size="md"
iconLeading={<Circle />}
aria-label="Loading"
/>
<Button
variant="secondary"
loading
size="lg"
iconLeading={<Circle />}
aria-label="Loading"
/>
<Button
variant="secondary"
loading
size="xl"
iconLeading={<Circle />}
aria-label="Loading"
/>
</div>
</div>
);
}
Tertiary
Tertiary variant buttons use tertiary text colors and show a background on hover. They're useful for less prominent actions that still need to be visible.
import { Button } from "@/components/ui/button";
const Circle = () => (
<span className="size-5 rounded-full border-2 border-current" />
);
export default function ButtonTertiary() {
return (
<div>
<div className="mb-4 flex gap-8">
<Button variant="tertiary" size="sm">
Button CTA
</Button>
<Button variant="tertiary" size="md">
Button CTA
</Button>
<Button variant="tertiary" size="lg">
Button CTA
</Button>
<Button variant="tertiary" size="xl">
Button CTA
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button variant="tertiary" size="sm" disabled>
Button CTA
</Button>
<Button variant="tertiary" size="md" disabled>
Button CTA
</Button>
<Button variant="tertiary" size="lg" disabled>
Button CTA
</Button>
<Button variant="tertiary" size="xl" disabled>
Button CTA
</Button>
</div>
<div className="mb-16 flex gap-8">
<Button variant="tertiary" loading showTextWhileLoading size="sm">
Submitting...
</Button>
<Button variant="tertiary" loading showTextWhileLoading size="md">
Submitting...
</Button>
<Button variant="tertiary" loading showTextWhileLoading size="lg">
Submitting...
</Button>
<Button variant="tertiary" loading showTextWhileLoading size="xl">
Submitting...
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button
variant="tertiary"
size="sm"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="tertiary"
size="md"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="tertiary"
size="lg"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="tertiary"
size="xl"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button
variant="tertiary"
size="sm"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="tertiary"
size="md"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="tertiary"
size="lg"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="tertiary"
size="xl"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
</div>
<div className="mb-16 flex gap-8">
<Button
variant="tertiary"
loading
showTextWhileLoading
size="sm"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Submitting...
</Button>
<Button
variant="tertiary"
loading
showTextWhileLoading
size="md"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Submitting...
</Button>
<Button
variant="tertiary"
loading
showTextWhileLoading
size="lg"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Submitting...
</Button>
<Button
variant="tertiary"
loading
showTextWhileLoading
size="xl"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Submitting...
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button variant="tertiary" size="sm" iconLeading={<Circle />} />
<Button variant="tertiary" size="md" iconLeading={<Circle />} />
<Button variant="tertiary" size="lg" iconLeading={<Circle />} />
<Button variant="tertiary" size="xl" iconLeading={<Circle />} />
</div>
<div className="flex gap-8">
<Button variant="tertiary" size="sm" disabled iconLeading={<Circle />} />
<Button variant="tertiary" size="md" disabled iconLeading={<Circle />} />
<Button variant="tertiary" size="lg" disabled iconLeading={<Circle />} />
<Button variant="tertiary" size="xl" disabled iconLeading={<Circle />} />
</div>
<div className="mb-4 flex gap-8">
<Button
variant="tertiary"
loading
size="sm"
iconLeading={<Circle />}
aria-label="Loading"
/>
<Button
variant="tertiary"
loading
size="md"
iconLeading={<Circle />}
aria-label="Loading"
/>
<Button
variant="tertiary"
loading
size="lg"
iconLeading={<Circle />}
aria-label="Loading"
/>
<Button
variant="tertiary"
loading
size="xl"
iconLeading={<Circle />}
aria-label="Loading"
/>
</div>
</div>
);
}
Outline
Outline variant buttons have a transparent background with a border. They're useful for actions that need to be visible but less prominent than primary or secondary buttons.
import { Button } from "@/components/ui/button";
const Circle = () => (
<span className="size-5 rounded-full border-2 border-current" />
);
export default function ButtonOutline() {
return (
<div>
<div className="mb-4 flex gap-8">
<Button variant="outline" size="sm">
Button CTA
</Button>
<Button variant="outline" size="md">
Button CTA
</Button>
<Button variant="outline" size="lg">
Button CTA
</Button>
<Button variant="outline" size="xl">
Button CTA
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button variant="outline" size="sm" disabled>
Button CTA
</Button>
<Button variant="outline" size="md" disabled>
Button CTA
</Button>
<Button variant="outline" size="lg" disabled>
Button CTA
</Button>
<Button variant="outline" size="xl" disabled>
Button CTA
</Button>
</div>
<div className="mb-16 flex gap-8">
<Button variant="outline" loading showTextWhileLoading size="sm">
Submitting...
</Button>
<Button variant="outline" loading showTextWhileLoading size="md">
Submitting...
</Button>
<Button variant="outline" loading showTextWhileLoading size="lg">
Submitting...
</Button>
<Button variant="outline" loading showTextWhileLoading size="xl">
Submitting...
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button
variant="outline"
size="sm"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="outline"
size="md"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="outline"
size="lg"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="outline"
size="xl"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button
variant="outline"
size="sm"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="outline"
size="md"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="outline"
size="lg"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="outline"
size="xl"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button variant="outline" size="sm" iconLeading={<Circle />} />
<Button variant="outline" size="md" iconLeading={<Circle />} />
<Button variant="outline" size="lg" iconLeading={<Circle />} />
<Button variant="outline" size="xl" iconLeading={<Circle />} />
</div>
<div className="flex gap-8">
<Button variant="outline" size="sm" disabled iconLeading={<Circle />} />
<Button variant="outline" size="md" disabled iconLeading={<Circle />} />
<Button variant="outline" size="lg" disabled iconLeading={<Circle />} />
<Button variant="outline" size="xl" disabled iconLeading={<Circle />} />
</div>
</div>
);
}
Ghost
Ghost variant buttons have no background and minimal styling. They're ideal for subtle actions or when you want the button to blend into the interface.
import { Button } from "@/components/ui/button";
const Circle = () => (
<span className="size-5 rounded-full border-2 border-current" />
);
export default function ButtonGhost() {
return (
<div>
<div className="mb-4 flex gap-8">
<Button variant="ghost" size="sm">
Button CTA
</Button>
<Button variant="ghost" size="md">
Button CTA
</Button>
<Button variant="ghost" size="lg">
Button CTA
</Button>
<Button variant="ghost" size="xl">
Button CTA
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button variant="ghost" size="sm" disabled>
Button CTA
</Button>
<Button variant="ghost" size="md" disabled>
Button CTA
</Button>
<Button variant="ghost" size="lg" disabled>
Button CTA
</Button>
<Button variant="ghost" size="xl" disabled>
Button CTA
</Button>
</div>
<div className="mb-16 flex gap-8">
<Button variant="ghost" loading showTextWhileLoading size="sm">
Submitting...
</Button>
<Button variant="ghost" loading showTextWhileLoading size="md">
Submitting...
</Button>
<Button variant="ghost" loading showTextWhileLoading size="lg">
Submitting...
</Button>
<Button variant="ghost" loading showTextWhileLoading size="xl">
Submitting...
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button
variant="ghost"
size="sm"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="ghost"
size="md"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="ghost"
size="lg"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="ghost"
size="xl"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button
variant="ghost"
size="sm"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="ghost"
size="md"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="ghost"
size="lg"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
<Button
variant="ghost"
size="xl"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Button CTA
</Button>
</div>
<div className="mb-16 flex gap-8">
<Button
variant="ghost"
loading
showTextWhileLoading
size="sm"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Submitting...
</Button>
<Button
variant="ghost"
loading
showTextWhileLoading
size="md"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Submitting...
</Button>
<Button
variant="ghost"
loading
showTextWhileLoading
size="lg"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Submitting...
</Button>
<Button
variant="ghost"
loading
showTextWhileLoading
size="xl"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Submitting...
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button variant="ghost" size="sm" iconLeading={<Circle />} />
<Button variant="ghost" size="md" iconLeading={<Circle />} />
<Button variant="ghost" size="lg" iconLeading={<Circle />} />
<Button variant="ghost" size="xl" iconLeading={<Circle />} />
</div>
<div className="mb-4 flex gap-8">
<Button variant="ghost" size="sm" disabled iconLeading={<Circle />} />
<Button variant="ghost" size="md" disabled iconLeading={<Circle />} />
<Button variant="ghost" size="lg" disabled iconLeading={<Circle />} />
<Button variant="ghost" size="xl" disabled iconLeading={<Circle />} />
</div>
<div className="flex gap-8">
<Button
variant="ghost"
loading
size="sm"
iconLeading={<Circle />}
aria-label="Loading"
/>
<Button
variant="ghost"
loading
size="md"
iconLeading={<Circle />}
aria-label="Loading"
/>
<Button
variant="ghost"
loading
size="lg"
iconLeading={<Circle />}
aria-label="Loading"
/>
<Button
variant="ghost"
loading
size="xl"
iconLeading={<Circle />}
aria-label="Loading"
/>
</div>
</div>
);
}
Destructive
Destructive variant buttons are used for dangerous or irreversible actions like deleting or removing content. They use error colors to indicate the severity of the action.
import { Button } from "@/components/ui/button";
const Circle = () => (
<span className="size-5 rounded-full border-2 border-current" />
);
export default function ButtonDestructive() {
return (
<div>
<div className="mb-4 flex gap-8">
<Button variant="destructive" size="sm">
Delete project
</Button>
<Button variant="destructive" size="md">
Delete project
</Button>
<Button variant="destructive" size="lg">
Delete project
</Button>
<Button variant="destructive" size="xl">
Delete project
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button variant="destructive" size="sm" disabled>
Delete project
</Button>
<Button variant="destructive" size="md" disabled>
Delete project
</Button>
<Button variant="destructive" size="lg" disabled>
Delete project
</Button>
<Button variant="destructive" size="xl" disabled>
Delete project
</Button>
</div>
<div className="mb-16 flex gap-8">
<Button variant="destructive" loading showTextWhileLoading size="sm">
Deleting...
</Button>
<Button variant="destructive" loading showTextWhileLoading size="md">
Deleting...
</Button>
<Button variant="destructive" loading showTextWhileLoading size="lg">
Deleting...
</Button>
<Button variant="destructive" loading showTextWhileLoading size="xl">
Deleting...
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button
variant="destructive"
size="sm"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Delete project
</Button>
<Button
variant="destructive"
size="md"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Delete project
</Button>
<Button
variant="destructive"
size="lg"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Delete project
</Button>
<Button
variant="destructive"
size="xl"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Delete project
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button
variant="destructive"
size="sm"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Delete project
</Button>
<Button
variant="destructive"
size="md"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Delete project
</Button>
<Button
variant="destructive"
size="lg"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Delete project
</Button>
<Button
variant="destructive"
size="xl"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Delete project
</Button>
</div>
<div className="mb-16 flex gap-8">
<Button
variant="destructive"
loading
showTextWhileLoading
size="sm"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Deleting...
</Button>
<Button
variant="destructive"
loading
showTextWhileLoading
size="md"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Deleting...
</Button>
<Button
variant="destructive"
loading
showTextWhileLoading
size="lg"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Deleting...
</Button>
<Button
variant="destructive"
loading
showTextWhileLoading
size="xl"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Deleting...
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button variant="destructive" size="sm" iconLeading={<Circle />} />
<Button variant="destructive" size="md" iconLeading={<Circle />} />
<Button variant="destructive" size="lg" iconLeading={<Circle />} />
<Button variant="destructive" size="xl" iconLeading={<Circle />} />
</div>
<div className="mb-4 flex gap-8">
<Button variant="destructive" size="sm" disabled iconLeading={<Circle />} />
<Button variant="destructive" size="md" disabled iconLeading={<Circle />} />
<Button variant="destructive" size="lg" disabled iconLeading={<Circle />} />
<Button variant="destructive" size="xl" disabled iconLeading={<Circle />} />
</div>
<div className="flex gap-8">
<Button
variant="destructive"
loading
size="sm"
iconLeading={<Circle />}
aria-label="Loading"
/>
<Button
variant="destructive"
loading
size="md"
iconLeading={<Circle />}
aria-label="Loading"
/>
<Button
variant="destructive"
loading
size="lg"
iconLeading={<Circle />}
aria-label="Loading"
/>
<Button
variant="destructive"
loading
size="xl"
iconLeading={<Circle />}
aria-label="Loading"
/>
</div>
</div>
);
}
Link
Link variant buttons are styled as text links with underline on hover. They're useful for navigation or less prominent actions.
import { Button } from "@/components/ui/button";
const Circle = () => (
<span className="size-5 rounded-full border-2 border-current" />
);
export default function ButtonLink() {
return (
<div>
<div className="mb-4 flex gap-8">
<Button variant="link" size="sm">
Learn more
</Button>
<Button variant="link" size="md">
Learn more
</Button>
<Button variant="link" size="lg">
Learn more
</Button>
<Button variant="link" size="xl">
Learn more
</Button>
</div>
<div className="mb-16 flex gap-8">
<Button variant="link" size="sm" disabled>
Learn more
</Button>
<Button variant="link" size="md" disabled>
Learn more
</Button>
<Button variant="link" size="lg" disabled>
Learn more
</Button>
<Button variant="link" size="xl" disabled>
Learn more
</Button>
</div>
<div className="mb-4 flex gap-8">
<Button
variant="link"
size="sm"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Learn more
</Button>
<Button
variant="link"
size="md"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Learn more
</Button>
<Button
variant="link"
size="lg"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Learn more
</Button>
<Button
variant="link"
size="xl"
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Learn more
</Button>
</div>
<div className="mb-16 flex gap-8">
<Button
variant="link"
size="sm"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Learn more
</Button>
<Button
variant="link"
size="md"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Learn more
</Button>
<Button
variant="link"
size="lg"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Learn more
</Button>
<Button
variant="link"
size="xl"
disabled
iconLeading={<Circle />}
iconTrailing={<Circle />}
>
Learn more
</Button>
</div>
</div>
);
}
With Icons
Buttons can include icons before or after the text, or both. Icons help communicate the action more clearly and improve visual hierarchy.
Leading Icon
Trailing Icon
Leading & Trailing Icons
Icon Only
Different Sizes with Icons
import { Button } from "@/components/ui/button";
import { Check, Edit, Trash2, ArrowRight, Download } from "lucide-react";
export default function ButtonWithIcons() {
return (
<div className="flex flex-col gap-8">
{/* Leading Icon */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">Leading Icon</h3>
<div className="flex flex-wrap gap-4">
<Button iconLeading={<Check className="size-5" />} size="md">
Save
</Button>
<Button variant="secondary" iconLeading={<Edit className="size-5" />} size="md">
Edit
</Button>
<Button
variant="destructive"
iconLeading={<Trash2 className="size-5" />}
size="md"
>
Delete
</Button>
</div>
</div>
{/* Trailing Icon */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">Trailing Icon</h3>
<div className="flex flex-wrap gap-4">
<Button iconTrailing={<Check className="size-5" />} size="md">
Save
</Button>
<Button
variant="secondary"
iconTrailing={<Edit className="size-5" />}
size="md"
>
Edit
</Button>
<Button
variant="destructive"
iconTrailing={<Trash2 className="size-5" />}
size="md"
>
Delete
</Button>
</div>
</div>
{/* Both Icons */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">
Leading & Trailing Icons
</h3>
<div className="flex flex-wrap gap-4">
<Button
iconLeading={<Download className="size-5" />}
iconTrailing={<ArrowRight className="size-5" />}
size="md"
>
Download
</Button>
<Button
variant="secondary"
iconLeading={<Download className="size-5" />}
iconTrailing={<ArrowRight className="size-5" />}
size="md"
>
Download
</Button>
<Button
variant="outline"
iconLeading={<Download className="size-5" />}
iconTrailing={<ArrowRight className="size-5" />}
size="md"
>
Download
</Button>
</div>
</div>
{/* Icon Only */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">Icon Only</h3>
<div className="flex flex-wrap gap-4">
<Button iconLeading={<Check className="size-5" />} size="md" />
<Button
variant="secondary"
iconLeading={<Edit className="size-5" />}
size="md"
/>
<Button
variant="destructive"
iconLeading={<Trash2 className="size-5" />}
size="md"
/>
</div>
</div>
{/* Different Sizes with Icons */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">
Different Sizes with Icons
</h3>
<div className="flex flex-wrap gap-4 items-center">
<Button iconLeading={<Check className="size-5" />} size="sm">
Small
</Button>
<Button iconLeading={<Check className="size-5" />} size="md">
Medium
</Button>
<Button iconLeading={<Check className="size-5" />} size="lg">
Large
</Button>
<Button iconLeading={<Check className="size-5" />} size="xl">
Extra Large
</Button>
</div>
</div>
</div>
);
}
Loading
Buttons support loading states with a spinner. You can show the loading spinner alone or keep the text visible while loading.
Basic Loading
Loading with Text Visible
Loading with Custom Text
Loading with Icons
Loading Icon Only
Different Sizes Loading
import { Button } from "@/components/ui/button";
import { Check, ArrowRight } from "lucide-react";
export default function ButtonLoading() {
return (
<div className="flex flex-col gap-8">
{/* Basic Loading */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">Basic Loading</h3>
<div className="flex flex-wrap gap-4">
<Button loading size="md">
Loading...
</Button>
<Button variant="secondary" loading size="md">
Loading...
</Button>
<Button variant="outline" loading size="md">
Loading...
</Button>
<Button variant="ghost" loading size="md">
Loading...
</Button>
</div>
</div>
{/* Loading with Text Visible */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">
Loading with Text Visible
</h3>
<div className="flex flex-wrap gap-4">
<Button loading showTextWhileLoading size="md">
Submitting...
</Button>
<Button variant="secondary" loading showTextWhileLoading size="md">
Submitting...
</Button>
<Button variant="outline" loading showTextWhileLoading size="md">
Submitting...
</Button>
<Button variant="ghost" loading showTextWhileLoading size="md">
Submitting...
</Button>
</div>
</div>
{/* Loading with Custom Text */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">
Loading with Custom Text
</h3>
<div className="flex flex-wrap gap-4">
<Button loading loadingText="Processing..." size="md">
Save
</Button>
<Button
variant="secondary"
loading
loadingText="Saving..."
size="md"
>
Save
</Button>
<Button variant="destructive" loading loadingText="Deleting..." size="md">
Delete
</Button>
</div>
</div>
{/* Loading with Icons */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">
Loading with Icons
</h3>
<div className="flex flex-wrap gap-4">
<Button
loading
showTextWhileLoading
iconLeading={<Check className="size-5" />}
size="md"
>
Saving...
</Button>
<Button
loading
showTextWhileLoading
iconTrailing={<ArrowRight className="size-5" />}
size="md"
>
Submitting...
</Button>
</div>
</div>
{/* Loading Icon Only */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">Loading Icon Only</h3>
<div className="flex flex-wrap gap-4">
<Button loading size="sm" iconLeading={<Check className="size-5" />} aria-label="Loading" />
<Button loading size="md" iconLeading={<Check className="size-5" />} aria-label="Loading" />
<Button loading size="lg" iconLeading={<Check className="size-5" />} aria-label="Loading" />
<Button loading size="xl" iconLeading={<Check className="size-5" />} aria-label="Loading" />
</div>
</div>
{/* Different Sizes Loading */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">
Different Sizes Loading
</h3>
<div className="flex flex-wrap gap-4">
<Button loading showTextWhileLoading size="sm">
Loading...
</Button>
<Button loading showTextWhileLoading size="md">
Loading...
</Button>
<Button loading showTextWhileLoading size="lg">
Loading...
</Button>
<Button loading showTextWhileLoading size="xl">
Loading...
</Button>
</div>
</div>
</div>
);
}
Sizes
Buttons come in four sizes: small (sm), medium (md), large (lg), and extra large (xl). Choose the size that best fits your interface and action importance.
Primary
Secondary
Outline
Ghost
Destructive
Link
import { Button } from "@/components/ui/button";
export default function ButtonSizes() {
return (
<div className="flex flex-col gap-8">
{/* Primary Variant Sizes */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">Primary</h3>
<div className="flex flex-wrap gap-4 items-center">
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<Button size="xl">Extra Large</Button>
</div>
</div>
{/* Secondary Variant Sizes */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">Secondary</h3>
<div className="flex flex-wrap gap-4 items-center">
<Button variant="secondary" size="sm">
Small
</Button>
<Button variant="secondary" size="md">
Medium
</Button>
<Button variant="secondary" size="lg">
Large
</Button>
<Button variant="secondary" size="xl">
Extra Large
</Button>
</div>
</div>
{/* Outline Variant Sizes */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">Outline</h3>
<div className="flex flex-wrap gap-4 items-center">
<Button variant="outline" size="sm">
Small
</Button>
<Button variant="outline" size="md">
Medium
</Button>
<Button variant="outline" size="lg">
Large
</Button>
<Button variant="outline" size="xl">
Extra Large
</Button>
</div>
</div>
{/* Ghost Variant Sizes */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">Ghost</h3>
<div className="flex flex-wrap gap-4 items-center">
<Button variant="ghost" size="sm">
Small
</Button>
<Button variant="ghost" size="md">
Medium
</Button>
<Button variant="ghost" size="lg">
Large
</Button>
<Button variant="ghost" size="xl">
Extra Large
</Button>
</div>
</div>
{/* Destructive Variant Sizes */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">Destructive</h3>
<div className="flex flex-wrap gap-4 items-center">
<Button variant="destructive" size="sm">
Small
</Button>
<Button variant="destructive" size="md">
Medium
</Button>
<Button variant="destructive" size="lg">
Large
</Button>
<Button variant="destructive" size="xl">
Extra Large
</Button>
</div>
</div>
{/* Link Variant Sizes */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">Link</h3>
<div className="flex flex-wrap gap-4 items-center">
<Button variant="link" size="sm">
Small
</Button>
<Button variant="link" size="md">
Medium
</Button>
<Button variant="link" size="lg">
Large
</Button>
<Button variant="link" size="xl">
Extra Large
</Button>
</div>
</div>
</div>
);
}