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.
1<AidashButton2variant="primary"3size="md"4>5Click Me6</AidashButton>Installation
Install the Aidash Components package using your preferred package manager.
$ pnpm add @aidash/componentsImport
Import the AidashButton component into your file.
import { AidashButton } from '@/components/aidash/button';Usage
The simplest way to use AidashButton with default props.
1<AidashButton>Default Button</AidashButton>2<AidashButton variant="secondary">Secondary</AidashButton>Variants
Eight visual variants for different contexts and emphasis levels.
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>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.
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.
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).
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
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
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
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
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.
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.
| Prop | Type | Default | Description |
|---|---|---|---|
| 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) |
| loading | boolean | false | Show a loading spinner and disable interaction |
| disabled | boolean | false | Disable the button |
| iconLeft | ReactNode | — | Icon rendered before the label |
| iconRight | ReactNode | — | Icon rendered after the label |
| iconOnly | boolean | false | Render as a square icon button |
| fullWidth | boolean | false | Stretch button to fill container width |
| children | ReactNode | — | Button label content |
| className | string | '' | Additional CSS classes |
| onClick | (e: MouseEvent) => void | — | Click handler |
| type | 'button' | 'submit' | 'reset' | 'button' | HTML button type attribute |
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
1<AidashButton fullWidth iconLeft={<MailIcon />}>2Sign In with Email3</AidashButton>4<AidashButton variant="link" size="sm">5Forgot password?6</AidashButton>Content Toolbar
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.
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.
- 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
- 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.
'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>
);
}
);Related
Other components that work well alongside Button.