Octocat

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>
  );
}
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

  1. Variants:

    • Use primary for main actions
    • Use destructive for dangerous actions
    • Use ghost for subtle actions
  2. Accessibility:

    • Ensure clear button text
    • Consider loading states

Previous

Badge

Next

Calendar