← Dashboard
Zubeyr··Componentsbuttons

Button

A tactile dashboard button with layered inset shadows. Primary, secondary, ghost and danger variants, icon sizes and a loading state.

Live demo ↗

Installation

Run the shadcn CLI in a project that already has components.json. Using the @tomei tab? Register the namespace once first.

pnpm dlx shadcn@latest add https://tomei.zubeyr.dev/ui/r/button.json
  • →Files land in components/dashboard.

Usage

example.tsx
import { Download, Plus, Settings2, Trash2 } from 'lucide-react'

import { Button } from '@/components/dashboard/button'

export default function Demo() {
  return (
    <div className="flex flex-col items-center gap-6">
      <div className="flex flex-wrap items-center justify-center gap-3">
        <Button variant="primary">
          <Plus /> New report
        </Button>
        <Button>
          <Download /> Export
        </Button>
        <Button variant="ghost">Cancel</Button>
        <Button variant="danger">
          <Trash2 /> Delete
        </Button>
      </div>
      <div className="flex flex-wrap items-center justify-center gap-3">
        <Button size="sm">Small</Button>
        <Button size="lg" variant="primary">
          Large
        </Button>
        <Button size="icon" aria-label="Settings">
          <Settings2 />
        </Button>
        <Button variant="primary" loading>
          Saving
        </Button>
      </div>
    </div>
  )
}

Source code

button.tsx
import { cn } from '@/lib/utils'

const variants = {
  primary:
    'bg-primary text-primary-foreground shadow-(--shadow-button-primary) hover:bg-primary/90',
  secondary: 'bg-card text-foreground shadow-(--shadow-button) hover:bg-muted',
  ghost: 'text-muted-foreground hover:bg-muted hover:text-foreground',
  danger: 'bg-card text-destructive shadow-(--shadow-button) hover:bg-destructive/10',
}

const sizes = {
  sm: 'h-7 px-2.5 text-xs',
  md: 'h-8 px-3 text-sm',
  lg: 'h-10 px-4 text-sm',
  icon: 'size-8',
  'icon-sm': 'size-7',
}

export type ButtonProps = {
  as?: React.ElementType
  href?: string
  variant?: keyof typeof variants
  size?: keyof typeof sizes
  loading?: boolean
} & React.ButtonHTMLAttributes<HTMLButtonElement>

export function Button({
  as,
  href,
  variant = 'secondary',
  size = 'md',
  loading = false,
  disabled,
  className,
  children,
  ...props
}: ButtonProps) {
  const Tag = as ?? (href ? 'a' : 'button')

  return (
    <Tag
      href={href}
      type={Tag === 'button' ? (props.type ?? 'button') : undefined}
      disabled={Tag === 'button' ? disabled || loading : undefined}
      aria-busy={loading || undefined}
      className={cn(
        'inline-flex shrink-0 cursor-pointer items-center justify-center gap-1.5 rounded-lg leading-none font-medium whitespace-nowrap transition-[background-color,color,box-shadow] duration-150 outline-none select-none focus-visible:ring-2 focus-visible:ring-ring/60 disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-3.5 [&_svg]:shrink-0',
        variants[variant],
        sizes[size],
        className,
      )}
      {...props}
    >
      {loading && (
        <svg viewBox="0 0 24 24" fill="none" aria-hidden className="animate-spin">
          <circle
            cx="12"
            cy="12"
            r="9"
            stroke="currentColor"
            strokeOpacity="0.25"
            strokeWidth="3"
          />
          <path
            d="M21 12a9 9 0 0 0-9-9"
            stroke="currentColor"
            strokeWidth="3"
            strokeLinecap="round"
          />
        </svg>
      )}
      {children}
    </Tag>
  )
}