Create New Component
This skill creates React components following Lab's established architecture patterns.
Technology Stack
- •React: v19 (using forwardRef, JSX.Element)
- •TypeScript: v5 (strict typing)
- •Tailwind CSS: v4 (semantic color tokens)
- •Storybook: v9 (autodocs with decorators)
- •Icons: Heroicons v2 (
@heroicons/react/20/solid,@heroicons/react/16/solid) - •UI Library: Headless UI v2 (for interactive components)
- •Utilities:
clsxfor conditional classes - •Forms: react-hook-form v7 (when applicable)
Before You Begin
STOP AND ASK QUESTIONS unless the user has provided complete specifications.
Ask the user to clarify:
- •Component scope: Is this reusable across pages (core) or page-specific?
- •Category: Which category fits best? (see Component Categories below)
- •Functionality: What should this component do? What props are needed?
- •Composition: Should it use existing components internally?
- •Variants: Does it need size/variant/color options?
Only proceed after understanding the requirements.
Component Architecture
Core Components (src/components/)
When: Reusable across multiple pages/sections Requirements:
- •Generic and configurable via props
- •No page-specific business logic
- •No direct API calls (accept data via props)
- •Semantic Tailwind tokens (
bg-surface,text-foreground) - •MUST include Storybook stories
- •Compose from other core components when logical
Page-Scoped Components (src/pages/[section]/components/)
When: Used within specific page/section only Requirements:
- •Page-specific business logic allowed
- •Can use hooks (useNetwork, API hooks, etc.)
- •Compose/extend core components
- •May contain specialized state management
- •NO Storybook stories required
Component Categories
Core Component Categories (src/components/)
- •Use
lsto list the components in thesrc/components/directory.
Page Sections (src/pages/)
- •Use
lsto list the pages in thesrc/pages/directory.
Standard Component Structure
ComponentName/ ComponentName.tsx # Implementation ComponentName.types.ts # TypeScript types (when needed) ComponentName.stories.tsx # Storybook stories (core components only) index.ts # Barrel export
Implementation Steps
1. Research Existing Components
Before implementing, examine similar existing components:
- •Check category for existing patterns
- •Review how props are structured
- •Observe Tailwind class patterns
- •Note composability approach
2. Create Component Structure
For Core Components:
src/components/[Category]/[ComponentName]/
For Page-Scoped Components:
src/pages/[section]/components/[ComponentName]/
3. Implement Component File
Key Patterns:
import { forwardRef } from 'react';
import type { ComponentNameProps } from './ComponentName.types';
// Inline Tailwind class definitions (not objects)
const baseClasses = 'inline-flex items-center font-semibold';
// Variant classes when needed
const variantClasses = {
primary: 'bg-primary text-white hover:bg-primary/90',
secondary: 'bg-white text-gray-900 hover:bg-gray-50',
};
export const ComponentName = forwardRef<HTMLElement, ComponentNameProps>(
({ variant = 'primary', className = '', children, ...props }, ref) => {
const classes = [baseClasses, variantClasses[variant], className]
.filter(Boolean)
.join(' ');
return (
<element ref={ref} className={classes} {...props}>
{children}
</element>
);
}
);
ComponentName.displayName = 'ComponentName';
Semantic Colors:
- •Use tokens:
bg-surface,text-foreground,text-muted,border-border - •Brand:
bg-primary,text-primary,bg-secondary,bg-accent - •States:
text-success,text-warning,text-error - •Avoid hard-coded colors (no
text-gray-500)
Conditional Classes:
import { clsx } from 'clsx';
className={clsx(
'base-classes',
variant === 'primary' && 'primary-classes',
disabled && 'opacity-50 cursor-not-allowed',
className
)}
4. Create Types File (when needed)
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
export type ComponentVariant = 'primary' | 'secondary';
export type ComponentSize = 'sm' | 'md' | 'lg';
export interface ComponentNameProps extends ComponentPropsWithoutRef<'element'> {
/**
* The visual style variant
* @default 'primary'
*/
variant?: ComponentVariant;
/**
* Component content
*/
children?: ReactNode;
}
5. Create Storybook Stories (Core Components Only)
Required Template:
import type { Meta, StoryObj } from '@storybook/react-vite';
import { ComponentName } from './ComponentName';
const meta = {
title: 'Components/[Category]/ComponentName',
component: ComponentName,
parameters: {
layout: 'centered',
},
decorators: [
Story => (
<div className="min-w-[600px] rounded-sm bg-surface p-6">
<Story />
</div>
),
],
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary'],
},
},
} satisfies Meta<typeof ComponentName>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
children: 'Example',
variant: 'primary',
},
};
export const AllVariants: Story = {
render: () => (
<div className="flex flex-col gap-4">
<ComponentName variant="primary">Primary</ComponentName>
<ComponentName variant="secondary">Secondary</ComponentName>
</div>
),
};
Story Best Practices:
- •Use full nested path:
Components/[Category]/ComponentName - •Include
decoratorswithbg-surfacewrapper - •Show all variants/states
- •Use realistic content
- •Include complex examples
6. Create Barrel Export
export { ComponentName } from './ComponentName';
export type {
ComponentNameProps,
ComponentVariant,
ComponentSize
} from './ComponentName.types';
Composition Patterns
Core Composing Core
// NetworkSelect composes SelectMenu
import { SelectMenu } from '@/components/Forms/SelectMenu';
import { NetworkIcon } from '@/components/Ethereum/NetworkIcon';
export function NetworkSelect({ showLabel = true }: Props) {
const options = networks.map(network => ({
value: network,
label: network.display_name,
icon: <NetworkIcon networkName={network.name} />,
}));
return <SelectMenu options={options} showLabel={showLabel} />;
}
Page-Scoped Composing Core
// UserCard composes Card, Badge, ClientLogo
import { Card } from '@/components/Layout/Card';
import { Badge } from '@/components/Elements/Badge';
import { ClientLogo } from '@/components/Ethereum/ClientLogo';
export function UserCard({ username, clients }: Props) {
return (
<Card>
<h3>{username}</h3>
<Badge color="gray">{classification}</Badge>
{clients.map(c => <ClientLogo key={c} client={c} />)}
</Card>
);
}
Common Component Patterns
Icon Handling (Heroicons)
import { cloneElement, isValidElement } from 'react';
// Clone icon element to apply size classes
{leadingIcon && isValidElement(leadingIcon) &&
cloneElement(leadingIcon, {
'aria-hidden': 'true',
className: 'size-5',
} as Record<string, unknown>)
}
Size Variants
const sizeClasses = {
sm: 'px-2 py-1 text-sm',
md: 'px-2.5 py-1.5 text-sm',
lg: 'px-3 py-2 text-sm',
};
Interactive States
const baseClasses = ` inline-flex items-center hover:bg-primary/90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-primary/80 `;
Headless UI Integration
import { Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/react';
<Listbox value={value} onChange={onChange}>
<ListboxButton className="relative cursor-pointer rounded-lg">
{/* Button content */}
</ListboxButton>
<ListboxOptions className="absolute z-[9999] mt-1 rounded-lg border">
{options.map((option, index) => (
<ListboxOption
key={index}
value={option.value}
className="data-focus:bg-primary/10 data-selected:text-primary"
>
{option.label}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
Validation Checklist
Before completing:
- • Matches existing component patterns in category
- • Uses semantic Tailwind tokens (not hard-coded colors)
- • TypeScript types properly defined
- • Props have JSDoc comments with defaults
- • Barrel export created
- • Core components have Storybook stories
- • Stories include all variants/states
- • Component composes existing components when appropriate
- • No page-specific logic in core components
- • Accessibility attributes included (aria-*, role)
- • Dark mode handled via semantic tokens
- • Icon handling uses cloneElement pattern
- •
pnpm lintpasses - •
pnpm buildpasses
Testing in Storybook
For core components, use Storybook for iteration:
pnpm storybook
Use Playwright MCP tools to:
- •Navigate to component story
- •Take screenshots of variants
- •Test interactions
- •Verify responsive behavior
Common Mistakes to Avoid
❌ DON'T:
- •Create new component without asking scope questions
- •Hard-code colors (
text-gray-500instead oftext-muted) - •Skip Storybook stories for core components
- •Add API calls in core components
- •Duplicate existing component functionality
- •Use relative imports (use
@/path aliases) - •Create standalone files (use folder structure)
✅ DO:
- •Research existing patterns first
- •Ask clarifying questions
- •Use semantic color tokens
- •Compose from existing components
- •Follow established naming conventions
- •Create comprehensive Storybook stories
- •Document props with JSDoc
- •Test with
pnpm lintandpnpm build
Example Workflow
- •
User Request: "Create a tooltip component"
- •
Ask Questions:
- •"Should this be reusable across pages (core) or page-specific?"
- •"Which category fits best? I'm thinking Overlays."
- •"What trigger should it support? Hover, click, or both?"
- •"Should it have different positions (top, bottom, left, right)?"
- •
Research:
- •Examine
src/components/Overlays/for patterns - •Check if similar component exists
- •Review Headless UI tooltip docs
- •Examine
- •
Implement:
- •Create
src/components/Overlays/Tooltip/ - •Implement
Tooltip.tsxwith Headless UI - •Create
Tooltip.types.tswith variants - •Create comprehensive
Tooltip.stories.tsx - •Create barrel export
index.ts
- •Create
- •
Validate:
- •Test in Storybook
- •Run
pnpm lint - •Run
pnpm build - •Verify all checklist items
- •
Report: "Created core Tooltip component in src/components/Overlays/Tooltip with 4 position variants and comprehensive Storybook stories. Ready to use across all pages."