Skip to main content

Button

A versatile button component with 6 variants, 5 sizes, loading and disabled states, icon support, and spring-based micro-interactions. Built on Framer Motion for smooth, production-ready animations.

Live Preview

Interact with the button component in real time. Adjust variant, size, and state to see changes instantly.

tsx
1<AidashButton2variant="primary"3size="md"4>5Click Me6</AidashButton>

Installation

Install the Aidash Components package using your preferred package manager.

$ pnpm add @aidash/components

Import

Import the AidashButton component into your file.

tsx
import { AidashButton } from '@/components/aidash/button';

Usage

The simplest way to use AidashButton with default props.

tsx
1<AidashButton>Default Button</AidashButton>2<AidashButton variant="secondary">Secondary</AidashButton>

Variants

Eight visual variants for different contexts and emphasis levels.

tsx
1<AidashButton variant="primary">Primary</AidashButton>2<AidashButton variant="secondary">Secondary</AidashButton>3<AidashButton variant="outline">Outline</AidashButton>4<AidashButton variant="ghost">Ghost</AidashButton>5<AidashButton variant="destructive">Destructive</AidashButton>6<AidashButton variant="success">Success</AidashButton>7<AidashButton variant="warning">Warning</AidashButton>8<AidashButton variant="link">Link</AidashButton>
Tip
Use primary for the main call-to-action, secondary for supporting actions, destructive for irreversible actions, and reserve success / warning for state-changing actions with clear semantic meaning (e.g. approve, publish, block).

Shapes

Corner shape override — orthogonal to size, useful for pill filter chips or hard-edged toolbars.

tsx
1<AidashButton>Default</AidashButton>2<AidashButton shape="pill">Pill</AidashButton>3<AidashButton shape="square">Square</AidashButton>

Sizes

Five sizes from extra small to extra large to fit any layout context.

tsx
1<AidashButton size="xs">Extra Small</AidashButton>2<AidashButton size="sm">Small</AidashButton>3<AidashButton size="md">Medium</AidashButton>4<AidashButton size="lg">Large</AidashButton>5<AidashButton size="xl">Extra Large</AidashButton>

States

Loading, disabled, hover, and focus states with built-in visual feedback.

Loading

Disabled

Hover & Focus

Hover any button to see the scale-up micro-interaction. Use Tab to navigate and see the focus ring (2px brand-500 outline with 2px offset).

tsx
1{/* Loading state */}2<AidashButton loading>Saving...</AidashButton>3<AidashButton variant="secondary" loading>Processing</AidashButton>4 5{/* Disabled state */}6<AidashButton disabled>Disabled</AidashButton>7<AidashButton variant="outline" disabled>Disabled</AidashButton>8 9{/* Hover: scale(1.02), Tap: scale(0.97) — automatic */}10{/* Focus: outline-2 outline-(--ring) outline-offset-2 — automatic */}

Composition

Combine icons, button groups, and layouts for real-world use cases.

With Icons

tsx
1<AidashButton iconLeft={<PlusIcon />}>Add Item</AidashButton>2<AidashButton variant="secondary" iconRight={<ArrowRightIcon />}>Continue</AidashButton>3<AidashButton variant="outline" iconLeft={<DownloadIcon />}>Download</AidashButton>4<AidashButton variant="destructive" iconLeft={<TrashIcon />}>Delete</AidashButton>

Icon Only

tsx
1<AidashButton iconOnly size="sm" variant="ghost" aria-label="Search">2<SearchIcon />3</AidashButton>4<AidashButton iconOnly size="sm" variant="ghost" aria-label="Edit">5<EditIcon />6</AidashButton>

Toolbar

tsx
1<div className="flex items-center gap-1 p-1.5 rounded-xl border bg-surface-sunken">2<AidashButton iconOnly size="sm" variant="ghost" aria-label="Search">3  <SearchIcon />4</AidashButton>5<AidashButton iconOnly size="sm" variant="ghost" aria-label="Edit">6  <EditIcon />7</AidashButton>8<div className="w-px h-5 bg-border mx-1" />9<AidashButton iconOnly size="sm" variant="ghost" aria-label="Settings">10  <SettingsIcon />11</AidashButton>12</div>

Form Actions

tsx
1<div className="flex items-center justify-end gap-3">2<AidashButton variant="ghost">Cancel</AidashButton>3<AidashButton variant="outline">Save Draft</AidashButton>4<AidashButton>Publish</AidashButton>5</div>

Accessibility

Built-in accessibility features for keyboard navigation, ARIA attributes, and focus management.

Keyboard Navigation

Fully navigable with Tab key. Activate with Enter or Space. Focus ring is visible on all variants with a 2px brand-colored outline at 2px offset.

ARIA Attributes

When loading is true, the button is automatically disabled (aria-disabled). Use aria-label on icon-only buttons to provide accessible names.

Focus Management

Uses focus-visible to only show focus ring during keyboard navigation, not on mouse click. Disabled and loading states prevent interaction via pointer-events-none.

Ref Forwarding

Supports forwardRef for programmatic focus and integration with form libraries.

tsx
1{/* Always add aria-label for icon-only buttons */}2<AidashButton iconOnly aria-label="Delete item">3<TrashIcon />4</AidashButton>5 6{/* Loading state automatically disables the button */}7<AidashButton loading>Saving...</AidashButton>8 9{/* Use forwardRef for programmatic focus */}10const buttonRef = useRef<HTMLButtonElement>(null);11<AidashButton ref={buttonRef}>Focus Me</AidashButton>

API Reference

Complete list of props accepted by AidashButton.

PropTypeDefaultDescription
variant'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive' | 'success' | 'warning' | 'link''primary'Visual style of the button
size'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Size of the button
shape'default' | 'pill' | 'square''default'Corner shape override (default follows size rounding)
loadingbooleanfalseShow a loading spinner and disable interaction
disabledbooleanfalseDisable the button
iconLeftReactNodeIcon rendered before the label
iconRightReactNodeIcon rendered after the label
iconOnlybooleanfalseRender as a square icon button
fullWidthbooleanfalseStretch button to fill container width
childrenReactNodeButton label content
classNamestring''Additional CSS classes
onClick(e: MouseEvent) => voidClick handler
type'button' | 'submit' | 'reset''button'HTML button type attribute
Note
AidashButton extends React.ButtonHTMLAttributes, so it also accepts all standard HTML button attributes such as onClick, type, form, and aria-* props.

Examples

Realistic production examples showing the button in context.

Login Form

tsx
1<AidashButton fullWidth iconLeft={<MailIcon />}>2Sign In with Email3</AidashButton>4<AidashButton variant="link" size="sm">5Forgot password?6</AidashButton>

Content Toolbar

tsx
1<div className="flex items-center justify-between">2<div className="flex items-center gap-2">3  <AidashButton size="sm" variant="ghost" iconLeft={<EditIcon />}>Edit</AidashButton>4  <AidashButton size="sm" variant="ghost" iconLeft={<ShareIcon />}>Share</AidashButton>5  <AidashButton size="sm" variant="ghost" iconLeft={<DownloadIcon />}>Export</AidashButton>6</div>7<div className="flex items-center gap-2">8  <AidashButton size="sm" variant="outline" iconLeft={<HeartIcon />}>Favorite</AidashButton>9  <AidashButton size="sm" variant="destructive" iconLeft={<TrashIcon />}>Delete</AidashButton>10</div>11</div>

Dialog Actions

Delete Project?

This action cannot be undone. All data associated with this project will be permanently removed.

tsx
1<div className="flex items-center justify-end gap-3">2<AidashButton variant="outline">Cancel</AidashButton>3<AidashButton variant="destructive" iconLeft={<TrashIcon />}>4  Delete Project5</AidashButton>6</div>

Best Practices

Guidelines for using buttons effectively in your interface.

Do
  • Use one primary button per section for the main call-to-action
  • Use descriptive labels that indicate the action (e.g., "Save Changes")
  • Add aria-label to icon-only buttons for screen readers
  • Show loading state during async operations to prevent double-clicks
  • Use consistent sizing within the same context
  • Use destructive variant for irreversible actions with a confirmation step
Don't
  • Use multiple primary buttons in the same view competing for attention
  • Use vague labels like "Click Here" or "Submit" without context
  • Forget to disable or show loading during form submission
  • Use icon-only buttons without an aria-label
  • Mix different button sizes within the same button group
  • Use destructive variant for non-destructive actions

Source Code

Full source code for the AidashButton component.

src/components/aidash/button.tsx
tsx
'use client';

import { forwardRef } from 'react';
import { motion } from 'framer-motion';

const FILLED_SHADOW =
  'shadow-[0_1px_3px_rgba(0,0,0,0.12),inset_0_1px_0_rgba(255,255,255,0.12)]';

const variantStyles = {
  primary:
    `bg-linear-to-b from-brand-400 to-brand-600 text-white hover:from-brand-300 hover:to-brand-500 ${FILLED_SHADOW}`,
  secondary:
    'bg-(--surface-sunken) text-(--text) hover:bg-(--border) border border-(--border) shadow-(--shadow-card)',
  outline:
    'bg-transparent text-(--text) border border-(--border) hover:bg-(--surface-sunken) hover:border-brand-500/30 hover:shadow-(--shadow-card)',
  ghost:
    'bg-transparent text-(--text) hover:bg-(--surface-sunken)',
  destructive:
    `bg-linear-to-b from-danger-400 to-danger-600 text-white hover:from-danger-300 hover:to-danger-500 ${FILLED_SHADOW}`,
  success:
    `bg-linear-to-b from-success-400 to-success-600 text-white hover:from-success-300 hover:to-success-500 ${FILLED_SHADOW}`,
  warning:
    `bg-linear-to-b from-warn-400 to-warn-600 text-white hover:from-warn-300 hover:to-warn-500 ${FILLED_SHADOW}`,
  link:
    'bg-transparent text-brand-500 hover:underline underline-offset-4 p-0 h-auto shadow-none',
} as const;

const sizeStyles = {
  xs: 'text-xs px-2.5 py-1 rounded-md gap-1',
  sm: 'text-xs px-3 py-1.5 rounded-lg gap-1.5',
  md: 'text-sm px-4 py-2 rounded-lg gap-2',
  lg: 'text-sm px-5 py-2.5 rounded-lg gap-2',
  xl: 'text-base px-6 py-3 rounded-xl gap-2.5',
} as const;

const iconOnlySizeStyles = {
  xs: 'w-7 h-7 rounded-md',
  sm: 'w-8 h-8 rounded-lg',
  md: 'w-9 h-9 rounded-lg',
  lg: 'w-10 h-10 rounded-lg',
  xl: 'w-12 h-12 rounded-xl',
} as const;

const shapeOverrides = {
  default: '',
  pill: 'rounded-full',
  square: 'rounded-none',
} as const;

export type ButtonVariant = keyof typeof variantStyles;
export type ButtonSize = keyof typeof sizeStyles;
export type ButtonShape = keyof typeof shapeOverrides;

type NativeButtonProps = Omit<
  React.ButtonHTMLAttributes<HTMLButtonElement>,
  'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration'
>;

export interface ButtonProps extends NativeButtonProps {
  variant?: ButtonVariant;
  size?: ButtonSize;
  shape?: ButtonShape;
  loading?: boolean;
  iconLeft?: React.ReactNode;
  iconRight?: React.ReactNode;
  iconOnly?: boolean;
  fullWidth?: boolean;
}

export const AidashButton = forwardRef<HTMLButtonElement, ButtonProps>(
  function AidashButton(
    {
      variant = 'primary',
      size = 'md',
      shape = 'default',
      loading = false,
      disabled = false,
      iconLeft,
      iconRight,
      iconOnly = false,
      fullWidth = false,
      children,
      className = '',
      ...props
    },
    ref
  ) {
    const isDisabled = disabled || loading;
    const isLink = variant === 'link';

    const baseClasses =
      'inline-flex items-center justify-center font-medium transition-all focus-visible:outline-2 focus-visible:outline-(--ring) focus-visible:outline-offset-2';

    const stateClasses = isDisabled
      ? 'opacity-50 cursor-not-allowed pointer-events-none'
      : 'cursor-pointer';

    const widthClass = fullWidth ? 'w-full' : '';

    const sizeClass = iconOnly
      ? iconOnlySizeStyles[size]
      : sizeStyles[size];

    const shapeClass = isLink ? '' : shapeOverrides[shape];

    const combinedClasses = [
      baseClasses,
      variantStyles[variant],
      sizeClass,
      shapeClass,
      stateClasses,
      widthClass,
      className,
    ]
      .filter(Boolean)
      .join(' ');

    return (
      <motion.button
        ref={ref}
        className={combinedClasses}
        disabled={isDisabled}
        aria-busy={loading || undefined}
        whileHover={!isDisabled && !isLink ? { scale: 1.02 } : undefined}
        whileTap={!isDisabled && !isLink ? { scale: 0.97 } : undefined}
        transition={{ type: 'spring', stiffness: 400, damping: 25 }}
        {...props}
      >
        {loading && (
          <svg
            className="animate-spin shrink-0"
            width={size === 'xs' || size === 'sm' ? 14 : 16}
            height={size === 'xs' || size === 'sm' ? 14 : 16}
            viewBox="0 0 24 24"
            fill="none"
            aria-hidden="true"
          >
            <circle
              className="opacity-25"
              cx="12"
              cy="12"
              r="10"
              stroke="currentColor"
              strokeWidth="3"
            />
            <path
              className="opacity-75"
              fill="currentColor"
              d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
            />
          </svg>
        )}
        {!loading && iconLeft && (
          <span className="shrink-0 flex items-center" aria-hidden="true">{iconLeft}</span>
        )}
        {!iconOnly && children && (
          <span className={loading ? 'opacity-0' : ''}>{children}</span>
        )}
        {iconOnly && !loading && children}
        {!loading && iconRight && (
          <span className="shrink-0 flex items-center" aria-hidden="true">{iconRight}</span>
        )}
      </motion.button>
    );
  }
);

Other components that work well alongside Button.