Button

Interactive buttons with different styles and sizes.

1import { Button } from '@/components/ui/button';
2
3export function Default() {
4 return <Button>Click me</Button>;
5}

Installation

Copy and paste the following code in your project.
'use client';
import { Loading02Icon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import { cva, type VariantProps } from 'class-variance-authority';
import { AnimatePresence, HTMLMotionProps, motion, useReducedMotion } from 'motion/react';
import React from 'react';
import { cn } from '../lib/cn';
// --- CVA ---
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl text-sm font-medium outline-none select-none relative overflow-hidden disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed aria-invalid:ring-2 aria-invalid:ring-destructive/50 aria-invalid:border-destructive',
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground shadow-md hover:shadow-lg hover:shadow-primary/20',
destructive:
'bg-destructive text-destructive-foreground shadow-md hover:shadow-lg hover:shadow-destructive/20',
outline: 'border-border border bg-background hover:bg-muted',
secondary:
'bg-secondary text-secondary-foreground shadow-md hover:shadow-lg hover:shadow-secondary/20',
ghost: 'hover:bg-muted',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 px-3 text-xs',
lg: 'h-11 px-8 text-base',
icon: 'size-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
// --- Animation variants (hoisted at module level) ---
const LOADER_VARIANTS = {
initial: { opacity: 0, scale: 0.8, filter: 'blur(4px)' },
animate: { opacity: 1, scale: 1, filter: 'blur(0px)' },
exit: { opacity: 0, scale: 0.8, filter: 'blur(4px)' },
} as const;
const CONTENT_VARIANTS = {
initial: { opacity: 0, y: 5, filter: 'blur(4px)' },
animate: { opacity: 1, y: 0, filter: 'blur(0px)' },
exit: { opacity: 0, y: -5, filter: 'blur(4px)' },
} as const;
const SWAP_TRANSITION = { duration: 0.2 } as const;
const BUTTON_SPRING = { type: 'spring', stiffness: 400, damping: 17 } as const;
const BUTTON_STYLE = { willChange: 'transform' } as const;
// --- Component ---
interface ButtonProps extends HTMLMotionProps<'button'>, VariantProps<typeof buttonVariants> {
loading?: boolean;
loader?: React.ReactNode;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
fullWidth?: boolean;
asChild?: boolean;
children?: React.ReactNode;
}
const ButtonRoot = ({
className,
variant = 'default',
size = 'default',
loading = false,
loader,
leftIcon,
rightIcon,
fullWidth = false,
disabled,
children,
type = 'button',
ref,
...props
}: ButtonProps) => {
const isDisabled = disabled || loading;
const shouldReduceMotion = useReducedMotion();
return (
<motion.button
ref={ref}
type={type}
disabled={isDisabled}
aria-busy={loading}
className={cn(buttonVariants({ variant, size }), fullWidth && 'w-full', className)}
whileTap={!isDisabled && !shouldReduceMotion ? { scale: 0.96 } : undefined}
transition={BUTTON_SPRING}
style={BUTTON_STYLE}
{...props}
>
<AnimatePresence mode="popLayout" initial={false}>
{loading ? (
<motion.span
key="loader"
variants={LOADER_VARIANTS}
initial="initial"
animate="animate"
exit="exit"
transition={SWAP_TRANSITION}
className="absolute flex items-center justify-center"
aria-hidden="true"
>
{loader ?? (
<HugeiconsIcon icon={Loading02Icon} className="size-4 animate-spin" size={16} />
)}
</motion.span>
) : (
<motion.div
key="content"
className="flex items-center gap-2"
variants={CONTENT_VARIANTS}
initial="initial"
animate="animate"
exit="exit"
transition={SWAP_TRANSITION}
>
{leftIcon ? <span className="shrink-0">{leftIcon}</span> : null}
<span>{children}</span>
{rightIcon ? <span className="shrink-0">{rightIcon}</span> : null}
</motion.div>
)}
</AnimatePresence>
</motion.button>
);
};
ButtonRoot.displayName = 'Button';
// --- ButtonGroup ---
interface ButtonGroupProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
orientation?: 'horizontal' | 'vertical';
attached?: boolean;
ref?: React.Ref<HTMLDivElement>;
}
const ButtonGroup = ({
className,
children,
orientation = 'horizontal',
attached = false,
ref,
...props
}: ButtonGroupProps) => {
return (
<div
ref={ref}
role="group"
className={cn(
'inline-flex',
orientation === 'horizontal' ? 'flex-row' : 'flex-col',
attached
? orientation === 'horizontal'
? '[&>button:not(:first-child)]:-ml-px [&>button:not(:first-child)]:rounded-l-none [&>button:not(:last-child)]:rounded-r-none'
: '[&>button:not(:first-child)]:-mt-px [&>button:not(:first-child)]:rounded-t-none [&>button:not(:last-child)]:rounded-b-none'
: 'gap-2',
className,
)}
{...props}
>
{children}
</div>
);
};
ButtonGroup.displayName = 'ButtonGroup';
const Button = Object.assign(ButtonRoot, {
Group: ButtonGroup,
});
export { Button, buttonVariants };
export type { ButtonGroupProps, ButtonProps };
Make sure to update the import paths according to your project structure.

Anatomy

import { Button } from '@/components/ui/button';
<Button variant="outline">Button</Button>

Examples

Sizes

1import { Plus, Settings } from 'lucide-react';
2import { Button } from '@/components/ui/button';
3
4export function Sizes() {
5 return (
6 <div className="flex flex-col gap-6">
7 <div className="flex flex-wrap items-center gap-4">
8 <Button size="sm">Small</Button>
9 <Button size="default">Default</Button>
10 <Button size="lg">Large</Button>
11 <Button size="icon" aria-label="Settings">
12 <Settings className="size-4" />
13 </Button>
14 </div>
15 <div className="flex flex-wrap items-center gap-4">
16 <Button size="sm" leftIcon={<Plus className="size-4" />}>
17 Add small
18 </Button>
19 <Button size="default" leftIcon={<Plus className="size-4" />}>
20 Add default
21 </Button>
22 <Button size="lg" leftIcon={<Plus className="size-5" />}>
23 Add large
24 </Button>
25 </div>
26 </div>
27 );
28}

Secondary

1import { Button } from '@/components/ui/button';
2
3export function Secondary() {
4 return <Button variant="secondary">Secondary</Button>;
5}

Destructive

1import { Button } from '@/components/ui/button';
2
3export function Destructive() {
4 return <Button variant="destructive">Destructive</Button>;
5}

Outline

1import { Button } from '@/components/ui/button';
2
3export function Outline() {
4 return <Button variant="outline">Outline</Button>;
5}

Ghost

1import { Button } from '@/components/ui/button';
2
3export function Ghost() {
4 return <Button variant="ghost">Ghost</Button>;
5}
1import { Button } from '@/components/ui/button';
2
3export function Link() {
4 return <Button variant="link">Link</Button>;
5}

API Reference

Button

PropTypeDefaultDescription
variant"default" | "destructive" | "outline" | "secondary" | "ghost" | "link""default"The button style.
size"default" | "sm" | "lg" | "icon""default"The button size.
loadingbooleanfalseWhether the button is in a loading state.
loaderReact.ReactNode<Loader2 />Custom loader component to be displayed when loading.
leftIconReact.ReactNodeIcon to display on the left side of the button.
rightIconReact.ReactNodeIcon to display on the right side of the button.
fullWidthbooleanfalseWhether the button should take up the full width of its container.
disabledbooleanDisables the button.
classNamestringAdditional CSS class names.
type"button" | "submit" | "reset""button"The type of the button element.
childrenReact.ReactNodeThe button content (text or other elements).

ButtonGroup

PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"The layout of the button group, either horizontal or vertical.
attachedbooleanfalseWhether the buttons in the group are attached to each other (i.e., no border-radius between buttons).
classNamestringAdditional CSS class names.
childrenReact.ReactNodeThe buttons inside the group.