Advanced Development
Custom Component Development
Build reusable, accessible, and maintainable components following AEROSNAP design patterns
90 minutesAdvancedTypeScript
Progress Tracker
0 of 4 completedStep-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.