Button
The Button component provides a consistent way to trigger actions across the application.
import { HandPointingIcon } from '@phosphor-icons/react/dist/ssr';
import { Button } from '@/components/button';
export default function ButtonExample() {
return (
<Button>
<HandPointingIcon />
<span>Click me</span>
</Button>
);
} Dependencies
Source Code
'use client';
import type { VariantProps } from 'cva';
import { Slot, Slottable } from '@/components/slot';
import { Spinner } from '@/components/spinner';
import { cn, cva } from '@/lib/utils/classnames';
const buttonStyle = cva({
base: 'relative inline-flex h-(--button-height) shrink-0 items-center justify-center gap-1.5 whitespace-nowrap font-medium text-(--button-text-color) shadow-xs ring-ring transition [--button-text-color:var(--color-foreground)] focus-visible:outline-none focus-visible:ring-4 active:scale-98 enabled:cursor-pointer disabled:opacity-40',
variants: {
variant: {
primary: 'bg-accent [--button-text-color:var(--color-accent-foreground)]',
outline: 'border border-border bg-background focus-visible:border-accent',
ghost:
'border-none bg-transparent shadow-none ring-0 hover:bg-foreground/5',
destructive:
'bg-red-600 ring-red-600/50 [--button-text-color:var(--color-white)] hover:bg-red-700',
},
size: {
xs: 'rounded-lg px-2 text-sm [--button-height:--spacing(6)]',
sm: 'rounded-lg px-3 text-sm [--button-height:--spacing(8)]',
md: 'rounded-xl px-4 text-base [--button-height:--spacing(10)]',
lg: 'rounded-2xl px-5 text-base [--button-height:--spacing(12)]',
},
square: {
true: 'w-(--button-height) px-0',
false: '',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
});
export interface ButtonProps
extends React.ComponentPropsWithRef<'button'>,
VariantProps<typeof buttonStyle> {
asChild?: boolean;
isLoading?: boolean;
}
const Button = ({
children,
className,
variant,
asChild = false,
isLoading,
size = 'md',
square,
type = 'button',
ref,
...props
}: ButtonProps) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(
buttonStyle({
className,
variant,
size,
square,
}),
isLoading && 'text-transparent transition-none'
)}
ref={ref}
type={type}
{...props}
>
<Slottable asChild={asChild} child={children}>
{(child) => (
<>
{child}
{isLoading && (
<span
data-button-spinner
className={cn(
'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2',
'text-(--button-text-color)'
)}
>
<Spinner size={size} />
</span>
)}
</>
)}
</Slottable>
</Comp>
);
};
export { Button, buttonStyle }; API Reference
| Prop | Default | Type | Description |
|---|---|---|---|
variant | "primary" | "primary""outline""ghost""destructive" | |
size | "md" | "xs""sm""md""lg" | |
square | false | boolean | Makes the button a square. Helpful when you have a button with only an icon inside. |
isLoading | false | boolean | Replaces the button content with a spinner |
asChild | - | boolean |
Examples
Sizes
import { Button } from '@/components/button';
export default function ButtonSizesPreview() {
return (
<div className="flex flex-wrap items-center gap-2">
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
</div>
);
} Variants
import { Button } from '@/components/button';
export default function ButtonVariantsPreview() {
return (
<div className="flex flex-wrap items-center gap-2">
<Button variant="primary">Primary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
</div>
);
} Loading
'use client';
import { useState } from 'react';
import { Button } from '@/components/button';
export default function ButtonExample() {
const [isLoading, setIsLoading] = useState(false);
return (
<Button onClick={() => setIsLoading(!isLoading)} isLoading={isLoading}>
Click to toggle
</Button>
);
} Icons
import {
ArrowSquareOutIcon,
PackageIcon,
PencilIcon,
SunIcon,
} from '@phosphor-icons/react/dist/ssr';
import { Button } from '@/components/button';
export default function ButtonIconsPreview() {
return (
<div className="flex flex-wrap items-center gap-2">
<Button variant="outline" square aria-label="Switch theme">
<SunIcon />
</Button>
<Button variant="outline">
<PencilIcon />
<span>Edit</span>
</Button>
<Button variant="outline">
<PackageIcon />
<span>External link</span>
<ArrowSquareOutIcon />
</Button>
</div>
);
} disabled
import { Button } from '@/components/button';
export default function ButtonDisabledPreview() {
return (
<div className="flex flex-wrap items-center gap-2">
<Button disabled variant="primary">
Primary
</Button>
<Button disabled variant="outline">
Outline
</Button>
<Button disabled variant="ghost">
Ghost
</Button>
<Button disabled variant="destructive">
Destructive
</Button>
</div>
);
} As Link
import {
ArrowSquareOutIcon,
PackageIcon,
} from '@phosphor-icons/react/dist/ssr';
import { Button } from '@/components/button';
export default function ButtonLinkPreview() {
return (
<Button variant="outline" asChild>
<a href="https://significa.co" target="_blank" rel="noopener">
<PackageIcon />
<span>Significa website</span>
<ArrowSquareOutIcon />
</a>
</Button>
);
} Best Practices
-
Variants:
- Use primary for main actions
- Use destructive for dangerous actions
- Use ghost for subtle actions
-
Accessibility:
- Ensure clear button text
- Consider loading states
Previous
Badge
Next
Calendar