Advanced Development

Custom Component Development

Build reusable, accessible, and maintainable components following AEROSNAP design patterns

90 minutesAdvancedTypeScript

Progress Tracker

0 of 4 completed

Step-by-Step Implementation

Project Setup & Architecture

15 minutesSet up the foundation for your component library

Building the Button Component

20 minutesCreate a flexible, accessible button component

Form Input Components

25 minutesBuild form inputs with validation and error handling

Compound Component Patterns

30 minutesBuild flexible compound components like Card and Modal

Interactive Component Playgrounds

Test and experiment with the components you've built

Interactive Button Component

Experiment with different button variants, sizes, and states

Interactive Button ComponentTSX
import { forwardRef, ButtonHTMLAttributes } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/utils/cn';

const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background',
  {
    variants: {
      variant: {
        default: 'bg-blue-600 text-white hover:bg-blue-700',
        destructive: 'bg-red-600 text-white hover:bg-red-700',
        outline: 'border border-gray-300 hover:bg-gray-100 hover:text-gray-900',
        secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
        ghost: 'hover:bg-gray-100 hover:text-gray-900',
        link: 'underline-offset-4 hover:underline text-blue-600',
      },
      size: {
        default: 'h-10 py-2 px-4',
        sm: 'h-9 px-3 rounded-md',
        lg: 'h-11 px-8 rounded-md',
        icon: 'h-10 w-10',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  }
);

interface ButtonProps 
  extends ButtonHTMLAttributes<HTMLButtonElement>,
          VariantProps<typeof buttonVariants> {
  loading?: boolean;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
}

const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ 
    className, 
    variant, 
    size, 
    loading = false,
    leftIcon,
    rightIcon,
    children,
    disabled,
    ...props 
  }, ref) => {
    const isDisabled = disabled || loading;
    
    return (
      <button
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        disabled={isDisabled}
        aria-busy={loading}
        {...props}
      >
        {loading && (
          <svg 
            className="mr-2 h-4 w-4 animate-spin" 
            xmlns="http://www.w3.org/2000/svg" 
            fill="none" 
            viewBox="0 0 24 24"
          >
            <circle 
              className="opacity-25" 
              cx="12" 
              cy="12" 
              r="10" 
              stroke="currentColor" 
              strokeWidth="4"
            />
            <path 
              className="opacity-75" 
              fill="currentColor" 
              d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
            />
          </svg>
        )}
        {!loading && leftIcon && <span className="mr-2">{leftIcon}</span>}
        {children}
        {!loading && rightIcon && <span className="ml-2">{rightIcon}</span>}
      </button>
    );
  }
);

Button.displayName = 'Button';

// Usage Examples
export default function ButtonPlayground() {
  return (
    <div className="space-y-6 p-8 bg-gray-50 rounded-lg">
      <h3 className="text-lg font-semibold mb-4">Button Variants</h3>
      
      <div className="flex flex-wrap gap-3">
        <Button variant="default">Default</Button>
        <Button variant="destructive">Destructive</Button>
        <Button variant="outline">Outline</Button>
        <Button variant="secondary">Secondary</Button>
        <Button variant="ghost">Ghost</Button>
        <Button variant="link">Link</Button>
      </div>
      
      <h3 className="text-lg font-semibold mb-4">Button Sizes</h3>
      <div className="flex flex-wrap items-center gap-3">
        <Button size="sm">Small</Button>
        <Button size="default">Default</Button>
        <Button size="lg">Large</Button>
        <Button size="icon">
          <span>⚙️</span>
        </Button>
      </div>
      
      <h3 className="text-lg font-semibold mb-4">Button States</h3>
      <div className="flex flex-wrap gap-3">
        <Button loading>Loading</Button>
        <Button disabled>Disabled</Button>
        <Button leftIcon={<span>👍</span>}>With Left Icon</Button>
        <Button rightIcon={<span>→</span>}>With Right Icon</Button>
      </div>
    </div>
  );
}

Interactive Input Component

Build accessible form inputs with validation and error handling

Interactive Input ComponentTSX
import { InputHTMLAttributes, forwardRef } from 'react';
import { cn } from '@/utils/cn';

interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
  label?: string;
  error?: string;
  hint?: string;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
  isRequired?: boolean;
  variant?: 'default' | 'filled' | 'underlined';
}

const Input = forwardRef<HTMLInputElement, InputProps>(
  ({
    className,
    type = 'text',
    label,
    error,
    hint,
    leftIcon,
    rightIcon,
    isRequired = false,
    variant = 'default',
    id,
    ...props
  }, ref) => {
    const inputId = id || `input-${Math.random().toString(36).substr(2, 9)}`;
    
    const baseStyles = 'flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50';
    
    const variantStyles = {
      default: 'border-gray-300',
      filled: 'border-transparent bg-gray-100',
      underlined: 'border-0 border-b-2 border-gray-300 rounded-none px-0'
    };
    
    const errorStyles = error 
      ? 'border-red-500 focus-visible:ring-red-500' 
      : '';

    return (
      <div className="grid w-full gap-1.5">
        {label && (
          <label
            htmlFor={inputId}
            className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
          >
            {label}
            {isRequired && <span className="text-red-500 ml-1">*</span>}
          </label>
        )}
        
        <div className="relative">
          {leftIcon && (
            <div className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-500">
              {leftIcon}
            </div>
          )}
          
          <input
            type={type}
            className={cn(
              baseStyles,
              variantStyles[variant],
              errorStyles,
              leftIcon && 'pl-9',
              rightIcon && 'pr-9',
              className
            )}
            ref={ref}
            id={inputId}
            aria-invalid={error ? 'true' : 'false'}
            aria-describedby={
              error ? `${inputId}-error` : 
              hint ? `${inputId}-hint` : 
              undefined
            }
            {...props}
          />
          
          {rightIcon && (
            <div className="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-500">
              {rightIcon}
            </div>
          )}
        </div>
        
        {error && (
          <p
            id={`${inputId}-error`}
            className="text-sm font-medium text-red-500"
          >
            {error}
          </p>
        )}
        
        {hint && !error && (
          <p
            id={`${inputId}-hint`}
            className="text-sm text-gray-500"
          >
            {hint}
          </p>
        )}
      </div>
    );
  }
);

Input.displayName = 'Input';

// Usage Examples
export default function InputPlayground() {
  return (
    <div className="space-y-6 p-8 bg-gray-50 rounded-lg max-w-md">
      <h3 className="text-lg font-semibold mb-4">Input Variants</h3>
      
      <Input
        label="Default Input"
        placeholder="Enter your text..."
        hint="This is a helpful hint"
      />
      
      <Input
        label="Filled Input"
        variant="filled"
        placeholder="Enter your email..."
        type="email"
      />
      
      <Input
        label="Underlined Input"
        variant="underlined"
        placeholder="Your name"
        isRequired
      />
      
      <Input
        label="Input with Error"
        placeholder="Enter password..."
        type="password"
        error="Password must be at least 8 characters"
      />
      
      <Input
        label="Input with Icons"
        placeholder="Search..."
        leftIcon={<span>🔍</span>}
        rightIcon={<span>❌</span>}
      />
      
      <Input
        label="Disabled Input"
        placeholder="Cannot edit"
        disabled
        value="Read only value"
      />
    </div>
  );
}

Compound Card Component

Create flexible compound components with composition patterns

Compound Card ComponentTSX
import { HTMLAttributes, forwardRef } from 'react';
import { cn } from '@/utils/cn';

interface CardProps extends HTMLAttributes<HTMLDivElement> {
  variant?: 'default' | 'outlined' | 'elevated';
}

const Card = forwardRef<HTMLDivElement, CardProps>(
  ({ className, variant = 'default', ...props }, ref) => {
    const variants = {
      default: 'rounded-lg border bg-white text-gray-900 shadow-sm',
      outlined: 'rounded-lg border-2 bg-white text-gray-900',
      elevated: 'rounded-lg bg-white text-gray-900 shadow-lg'
    };
    
    return (
      <div
        ref={ref}
        className={cn(variants[variant], className)}
        {...props}
      />
    );
  }
);

const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div
      ref={ref}
      className={cn('flex flex-col space-y-1.5 p-6', className)}
      {...props}
    />
  )
);

const CardTitle = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHeadingElement>>(
  ({ className, ...props }, ref) => (
    <h3
      ref={ref}
      className={cn('text-2xl font-semibold leading-none tracking-tight', className)}
      {...props}
    />
  )
);

const CardDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
  ({ className, ...props }, ref) => (
    <p
      ref={ref}
      className={cn('text-sm text-gray-500', className)}
      {...props}
    />
  )
);

const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div 
      ref={ref} 
      className={cn('p-6 pt-0', className)} 
      {...props} 
    />
  )
);

const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div
      ref={ref}
      className={cn('flex items-center p-6 pt-0', className)}
      {...props}
    />
  )
);

Card.displayName = 'Card';
CardHeader.displayName = 'CardHeader';
CardTitle.displayName = 'CardTitle';
CardDescription.displayName = 'CardDescription';
CardContent.displayName = 'CardContent';
CardFooter.displayName = 'CardFooter';

// Simple Button component for the example
const Button = ({ variant = 'default', size = 'default', className, ...props }) => {
  const baseStyles = 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:opacity-50';
  const variants = {
    default: 'bg-blue-600 text-white hover:bg-blue-700',
    outline: 'border border-gray-300 hover:bg-gray-100',
  };
  const sizes = {
    default: 'h-10 px-4 py-2',
    sm: 'h-9 px-3',
  };
  
  return (
    <button
      className={cn(baseStyles, variants[variant], sizes[size], className)}
      {...props}
    />
  );
};

// Usage Examples
export default function CardPlayground() {
  return (
    <div className="grid gap-6 p-8 bg-gray-50">
      <h3 className="text-lg font-semibold mb-4">Card Variants</h3>
      
      <div className="grid md:grid-cols-3 gap-6">
        <Card variant="default">
          <CardHeader>
            <CardTitle>Default Card</CardTitle>
            <CardDescription>
              This card uses the default styling with subtle shadow.
            </CardDescription>
          </CardHeader>
          <CardContent>
            <p className="text-sm">
              Perfect for most content sections and information displays.
            </p>
          </CardContent>
          <CardFooter>
            <Button size="sm">Action</Button>
          </CardFooter>
        </Card>

        <Card variant="outlined">
          <CardHeader>
            <CardTitle>Outlined Card</CardTitle>
            <CardDescription>
              This card has a prominent border with no shadow.
            </CardDescription>
          </CardHeader>
          <CardContent>
            <p className="text-sm">
              Great for forms and interactive content that needs clear boundaries.
            </p>
          </CardContent>
          <CardFooter>
            <Button variant="outline" size="sm">Action</Button>
          </CardFooter>
        </Card>

        <Card variant="elevated">
          <CardHeader>
            <CardTitle>Elevated Card</CardTitle>
            <CardDescription>
              This card has a prominent shadow for emphasis.
            </CardDescription>
          </CardHeader>
          <CardContent>
            <p className="text-sm">
              Ideal for important announcements or featured content.
            </p>
          </CardContent>
          <CardFooter className="gap-2">
            <Button size="sm">Primary</Button>
            <Button variant="outline" size="sm">Secondary</Button>
          </CardFooter>
        </Card>
      </div>
      
      <Card className="max-w-md">
        <CardHeader>
          <CardTitle>User Profile</CardTitle>
          <CardDescription>
            Manage your account settings and preferences.
          </CardDescription>
        </CardHeader>
        <CardContent className="space-y-4">
          <div className="flex items-center space-x-3">
            <div className="w-10 h-10 bg-blue-500 rounded-full flex items-center justify-center text-white font-semibold">
              JD
            </div>
            <div>
              <p className="text-sm font-medium">John Doe</p>
              <p className="text-xs text-gray-500">john.doe@example.com</p>
            </div>
          </div>
          <div className="border-t pt-4">
            <p className="text-xs text-gray-500 mb-2">Account Status</p>
            <div className="flex items-center space-x-2">
              <div className="w-2 h-2 bg-green-500 rounded-full"></div>
              <span className="text-sm">Active</span>
            </div>
          </div>
        </CardContent>
        <CardFooter className="gap-2">
          <Button size="sm">Edit Profile</Button>
          <Button variant="outline" size="sm">Settings</Button>
        </CardFooter>
      </Card>
    </div>
  );
}

Best Practices

TypeScript First

Always define comprehensive TypeScript interfaces for your component props

  • Extend native HTML element props when appropriate
  • Use generic types for reusable components
  • Define strict prop types to catch errors early

Accessibility by Default

Build accessibility into your components from the ground up

  • Use semantic HTML elements as the foundation
  • Include proper ARIA attributes
  • Ensure keyboard navigation works correctly
  • Test with screen readers during development

Composition over Configuration

Design components that can be easily composed together

  • Keep components focused on a single responsibility
  • Use compound component patterns for complex UI
  • Allow for flexible styling and behaviour customisation

Common Pitfalls to Avoid

  • • Over-engineering components with unnecessary complexity
  • • Forgetting to handle edge cases and error states
  • • Not considering mobile and touch interactions
  • • Skipping accessibility testing with screen readers
  • • Building components in isolation without considering the design system

Ready to Build Your Component Library?

Take your skills further with our advanced tutorials and real-world projects.