Button
Action button with solid, outlined, ghost, and minimal variants.
Solid
default<Button variant="solid" color="root">Root</Button>
<Button variant="solid" color="primary">Primary</Button>
<Button variant="solid" color="secondary">Secondary</Button>
<Button variant="solid" color="accent">Accent</Button>
<Button variant="solid" color="danger">Danger</Button>Outlined
<Button variant="outlined" color="root">Root</Button>
<Button variant="outlined" color="primary">Primary</Button>
<Button variant="outlined" color="secondary">Secondary</Button>
<Button variant="outlined" color="accent">Accent</Button>
<Button variant="outlined" color="danger">Danger</Button>Ghost
<Button variant="ghost" color="root">Root</Button>
<Button variant="ghost" color="primary">Primary</Button>
<Button variant="ghost" color="secondary">Secondary</Button>
<Button variant="ghost" color="accent">Accent</Button>
<Button variant="ghost" color="danger">Danger</Button>Minimal
<Button variant="minimal" color="root">Root</Button>
<Button variant="minimal" color="primary">Primary</Button>
<Button variant="minimal" color="secondary">Secondary</Button>
<Button variant="minimal" color="accent">Accent</Button>
<Button variant="minimal" color="danger">Danger</Button><Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>End
default<Button icon={<ArrowForwardIcon />}>Next</Button>
<Button variant="outlined" icon={<ArrowForwardIcon />}>Learn More</Button>Start
<Button icon={<ArrowBackIcon />} iconPosition="start">Back</Button>
<Button variant="outlined" icon={<ArrowBackIcon />} iconPosition="start">Previous</Button><Button loading>Saving</Button>
<Button variant="outlined" loading>Loading</Button>
<Button variant="ghost" loading>Processing</Button><div className="max-w-[400px]">
<Button fullWidth>Full Width</Button>
</div>
<div className="max-w-[400px]">
<Button fullWidth variant="outlined" icon={<ArrowForwardIcon />}>Continue</Button>
</div><Button href="/">Home</Button>
<Button variant="outlined" href="/" icon={<ArrowForwardIcon />}>Learn More</Button>import { Button } from "@/ui";
<Button>Click me</Button>src/ui/Button.tsx
import { cva, type VariantProps } from "class-variance-authority";
import NextLink from "next/link";
import type { ComponentProps } from "react";
import { Loader } from "./Loader";
import React from "react";
import { cn } from "@/utils";
export const buttonVariants = cva(
"inline-flex cursor-pointer items-center justify-center gap-2 font-semibold transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
solid: "rounded-md border border-transparent",
outlined: "rounded-md border bg-transparent",
ghost: "rounded-md border border-transparent",
minimal: "group rounded-md border border-transparent",
},
color: {
root: "",
primary: "",
secondary: "",
accent: "",
danger: "",
},
size: {
sm: "h-9 px-3 text-sm",
md: "h-10 px-4 text-sm",
lg: "h-11 px-8 text-sm",
},
},
compoundVariants: [
// solid
{ variant: "solid", color: "root", className: "bg-root-contrast text-root hover:bg-root-hover" },
{ variant: "solid", color: "primary", className: "bg-primary text-primary-contrast hover:bg-primary-hover" },
{ variant: "solid", color: "secondary", className: "bg-secondary text-secondary-contrast hover:bg-secondary-hover" },
{ variant: "solid", color: "accent", className: "bg-accent text-accent-contrast hover:bg-accent-hover" },
{ variant: "solid", color: "danger", className: "bg-danger text-danger-contrast hover:bg-danger-hover" },
// outlined
{ variant: "outlined", color: "root", className: "border-root-contrast text-root-contrast hover:bg-root-contrast hover:text-root" },
{ variant: "outlined", color: "primary", className: "border-primary text-primary hover:bg-primary hover:text-primary-contrast" },
{ variant: "outlined", color: "secondary", className: "border-secondary text-secondary hover:bg-secondary hover:text-secondary-contrast" },
{ variant: "outlined", color: "accent", className: "border-accent text-accent hover:bg-accent hover:text-accent-contrast" },
{ variant: "outlined", color: "danger", className: "border-danger-contrast text-danger-contrast hover:bg-danger hover:text-danger-contrast" },
// ghost
{ variant: "ghost", color: "root", className: "bg-root-ghost text-root-contrast hover:bg-root-ghost-hover" },
{ variant: "ghost", color: "primary", className: "bg-primary-ghost text-primary hover:bg-primary-ghost-hover" },
{ variant: "ghost", color: "secondary", className: "bg-secondary-ghost text-secondary hover:bg-secondary-ghost-hover" },
{ variant: "ghost", color: "accent", className: "bg-accent-ghost text-accent hover:bg-accent-ghost-hover" },
{ variant: "ghost", color: "danger", className: "bg-danger-ghost text-danger-contrast hover:bg-danger-ghost-hover" },
// minimal
{ variant: "minimal", color: "root", className: "text-root-contrast hover:bg-root-ghost" },
{ variant: "minimal", color: "primary", className: "text-primary hover:bg-primary-ghost" },
{ variant: "minimal", color: "secondary", className: "text-secondary hover:bg-secondary-ghost" },
{ variant: "minimal", color: "accent", className: "text-accent hover:bg-accent-ghost" },
{ variant: "minimal", color: "danger", className: "text-danger-contrast hover:bg-danger-ghost" },
],
defaultVariants: { variant: "solid", color: "root", size: "md" },
},
);
interface ButtonSharedProps extends VariantProps<typeof buttonVariants> {
children: React.ReactNode;
className?: string;
icon?: React.ReactNode;
iconPosition?: "start" | "end";
loading?: boolean;
fullWidth?: boolean;
}
type ButtonAsLinkProps = ButtonSharedProps &
Omit<ComponentProps<typeof NextLink>, "color" | "href" | "children"> & {
href: string;
};
type ButtonAsButtonProps = ButtonSharedProps &
Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "color" | "children"> & {
href?: undefined;
};
export type ButtonProps = ButtonAsLinkProps | ButtonAsButtonProps;
function buildContent({
icon,
iconPosition,
loading,
variant,
}: {
icon?: React.ReactNode;
iconPosition: "start" | "end";
loading: boolean;
variant: ButtonSharedProps["variant"];
}) {
const iconEl = icon ? (
<span
className={cn(
"inline-flex shrink-0",
variant === "minimal" && iconPosition === "end" && "transition-transform group-hover:translate-x-1",
)}
>
{icon}
</span>
) : null;
const startEl = loading ? (
<Loader className="size-[1em] text-current!" aria-hidden="true" />
) : iconPosition === "start" ? (
iconEl
) : null;
const endEl = !loading && iconPosition === "end" ? iconEl : null;
return { startEl, endEl };
}
export function Button(props: ButtonProps) {
if (props.href !== undefined) {
const {
variant,
color,
size,
children,
className,
icon,
iconPosition = "end",
loading = false,
fullWidth = false,
href,
...linkProps
} = props;
const { startEl, endEl } = buildContent({ icon, iconPosition, loading, variant });
const classes = cn(buttonVariants({ variant, color, size }), fullWidth && "w-full", className);
const isDisabled = loading;
return (
<NextLink
href={isDisabled ? "#" : href}
className={cn(classes, isDisabled && "pointer-events-none opacity-50")}
aria-disabled={isDisabled || undefined}
tabIndex={isDisabled ? -1 : undefined}
{...linkProps}
>
{startEl}
{children}
{endEl}
</NextLink>
);
}
const {
variant,
color,
size,
children,
className,
icon,
iconPosition = "end",
loading = false,
fullWidth = false,
disabled = false,
...buttonProps
} = props;
const { startEl, endEl } = buildContent({ icon, iconPosition, loading, variant });
const classes = cn(buttonVariants({ variant, color, size }), fullWidth && "w-full", className);
const isDisabled = disabled || loading;
return (
<button className={classes} disabled={isDisabled} aria-busy={loading} {...buttonProps}>
{startEl}
{children}
{endEl}
</button>
);
}