| name | ui-components-creation |
| description | Guide for creating reusable UI components. Use this when asked to create a new component, extract shared UI logic, or add to the component library. |
| license | MIT |
UI Components Creation Guide
This skill guides you through creating reusable UI components in the SuperTool application following React 19, TypeScript, and Panda CSS best practices.
When to Create a Reusable Component
✅ Create a Component When:
- Multiple Usage - The component will be used in 2+ tools or pages
- Complex Logic - State management or interaction logic can be abstracted
- Common Pattern - It's a standard UI pattern (buttons, inputs, modals, tabs)
- Consistency - You want to enforce consistent styling/behavior across the app
- Testing - Complex logic benefits from isolated unit tests
Examples:
- Tabs component (used in GraphQL Playground, API Tester, etc.)
- Label component (used in all forms)
- Button, Input, Card (used everywhere)
- Dialog, Toast, Dropdown (common UI patterns)
❌ Don't Create a Component When:
- One-Time Use - Only used in a single tool with no reuse plans
- Tool-Specific Logic - Contains business logic tied to specific tool functionality
- Trivial Elements - Simple divs with minimal styling (use inline Panda CSS)
- Premature Abstraction - Wait until you need it in 2+ places before extracting
Examples:
- GraphQL query executor logic (specific to GraphQL tool)
- PDF merger UI (specific to PDF tool)
- One-off badges or status indicators
- Tool-specific layout containers
Component Creation Checklist
- Component serves 2+ use cases or is a standard UI pattern
- TypeScript interface extends proper base types (HTMLAttributes, etc.)
- Panda CSS used for all styling (no Tailwind utilities)
- Component is composable with
classNameprop support - Accessibility requirements met (ARIA, keyboard navigation)
- Props are well-documented with JSDoc comments
- Unit tests created with >= 95% coverage
- Component added to index file if part of public API
Step-by-Step Component Creation
1. Determine Component Location
File: components/ui/component-name.tsx
Naming Convention:
- Use kebab-case for filenames:
button.tsx,text-area.tsx,tabs.tsx - Use PascalCase for component names:
Button,TextArea,Tabs
2. Create the Component File
Template Structure:
'use client'
import { type HTMLAttributes, forwardRef } from 'react'
import { css, cx } from '@/styled-system/css'
// Define props interface extending base HTML element
export interface ComponentNameProps extends HTMLAttributes<HTMLDivElement> {
variant?: 'default' | 'outlined' | 'ghost'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
// Add component-specific props
}
/**
* ComponentName - Brief description
*
* @example
* <ComponentName variant="outlined" size="md">
* Content
* </ComponentName>
*/
export const ComponentName = forwardRef<HTMLDivElement, ComponentNameProps>(
({ variant = 'default', size = 'md', disabled = false, className, children, ...props }, ref) => {
return (
<div
ref={ref}
className={cx(
css({
// Base styles
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
rounded: 'md',
transition: 'all 0.2s',
// Variant styles
...(variant === 'default' && {
bg: 'purple.600',
color: 'white',
_hover: { bg: 'purple.700' },
}),
...(variant === 'outlined' && {
border: '1px solid',
borderColor: 'purple.500',
color: 'purple.400',
_hover: { bg: 'purple.950' },
}),
// Size styles
...(size === 'sm' && { px: '3', py: '1.5', fontSize: 'sm' }),
...(size === 'md' && { px: '4', py: '2', fontSize: 'md' }),
...(size === 'lg' && { px: '6', py: '3', fontSize: 'lg' }),
// Disabled state
...(disabled && {
opacity: 0.5,
cursor: 'not-allowed',
pointerEvents: 'none',
}),
}),
className
)}
{...props}
>
{children}
</div>
)
}
)
ComponentName.displayName = 'ComponentName'
3. Real-World Example: Label Component
File: components/ui/label.tsx
'use client'
import type { LabelHTMLAttributes } from 'react'
import { css, cx } from '@/styled-system/css'
export interface LabelProps extends LabelHTMLAttributes<HTMLLabelElement> {
required?: boolean
}
/**
* Label component for form inputs
*
* @example
* <Label htmlFor="email">Email Address</Label>
* <Input id="email" type="email" />
*/
// biome-ignore lint/a11y/noLabelWithoutControl: Label component is used with htmlFor prop
export const Label = ({ required, className, children, ...props }: LabelProps) => {
return (
<label
className={cx(
css({
display: 'block',
fontSize: 'sm',
fontWeight: 'medium',
color: 'gray.100',
mb: 2,
}),
className
)}
{...props}
>
{children}
{required && <span className={css({ color: 'red.500', ml: 1 })}>*</span>}
</label>
)
}
Usage:
<Label htmlFor="username" required>Username</Label>
<Input id="username" type="text" />
4. Real-World Example: Tabs Component with Context
File: components/ui/tabs.tsx
For components with shared state, use React Context:
'use client'
import { type HTMLAttributes, type ReactNode, createContext, useContext, useState } from 'react'
import { css, cx } from '@/styled-system/css'
// Context for shared tab state
interface TabsContextValue {
activeTab: string
setActiveTab: (value: string) => void
}
const TabsContext = createContext<TabsContextValue | undefined>(undefined)
function useTabsContext() {
const context = useContext(TabsContext)
if (!context) {
throw new Error('Tabs components must be used within Tabs provider')
}
return context
}
// Main Tabs container (provider)
export interface TabsProps extends HTMLAttributes<HTMLDivElement> {
defaultValue: string
children: ReactNode
}
export function Tabs({ defaultValue, children, className, ...props }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultValue)
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className={cx(css({ w: 'full' }), className)} {...props}>
{children}
</div>
</TabsContext.Provider>
)
}
// Tabs list (container for triggers)
export interface TabsListProps extends HTMLAttributes<HTMLDivElement> {}
export function TabsList({ children, className, ...props }: TabsListProps) {
return (
<div
role="tablist"
className={cx(
css({
display: 'flex',
gap: 1,
borderBottom: '1px solid',
borderColor: 'gray.700',
mb: 4,
}),
className
)}
{...props}
>
{children}
</div>
)
}
// Individual tab trigger (button)
export interface TabsTriggerProps extends HTMLAttributes<HTMLButtonElement> {
value: string
}
export function TabsTrigger({ value, children, className, ...props }: TabsTriggerProps) {
const { activeTab, setActiveTab } = useTabsContext()
const isActive = activeTab === value
return (
<button
type="button"
role="tab"
aria-selected={isActive}
onClick={() => setActiveTab(value)}
className={cx(
css({
px: 4,
py: 2,
fontSize: 'sm',
fontWeight: 'medium',
color: isActive ? 'purple.400' : 'gray.400',
borderBottom: '2px solid',
borderColor: isActive ? 'purple.400' : 'transparent',
transition: 'all 0.2s',
cursor: 'pointer',
bg: 'transparent',
_hover: {
color: isActive ? 'purple.300' : 'gray.300',
},
}),
className
)}
{...props}
>
{children}
</button>
)
}
// Tab content panel
export interface TabsContentProps extends HTMLAttributes<HTMLDivElement> {
value: string
}
export function TabsContent({ value, children, className, ...props }: TabsContentProps) {
const { activeTab } = useTabsContext()
if (activeTab !== value) return null
return (
<div
role="tabpanel"
className={cx(css({ py: 4 }), className)}
{...props}
>
{children}
</div>
)
}
Usage:
<Tabs defaultValue="variables">
<TabsList>
<TabsTrigger value="variables">Variables</TabsTrigger>
<TabsTrigger value="headers">Headers</TabsTrigger>
<TabsTrigger value="samples">Samples</TabsTrigger>
</TabsList>
<TabsContent value="variables">
<Textarea placeholder="Enter variables..." />
</TabsContent>
<TabsContent value="headers">
<Textarea placeholder="Enter headers..." />
</TabsContent>
<TabsContent value="samples">
<div>Sample queries...</div>
</TabsContent>
</Tabs>
5. Accessibility Requirements
All interactive components MUST meet these requirements:
For Buttons and Clickable Elements:
<button
type="button" // Explicit type
onClick={handleClick} // Click handler
onKeyDown={handleKeyDown} // Keyboard support (Enter/Space)
aria-label="Descriptive label" // For icon-only buttons
disabled={isDisabled} // Disable state
>
For Form Elements:
<Label htmlFor="input-id">Label Text</Label>
<Input
id="input-id" // Match Label htmlFor
aria-required={isRequired} // Required state
aria-invalid={hasError} // Error state
aria-describedby="error-id" // Link to error message
/>
{hasError && <p id="error-id">Error message</p>}
For Custom Components:
<div
role="tablist" // Appropriate ARIA role
aria-label="Settings tabs" // Descriptive label
>
<button
role="tab"
aria-selected={isActive} // Selection state
aria-controls="panel-id" // Link to panel
tabIndex={isActive ? 0 : -1} // Keyboard navigation
>
Accessibility Checklist:
- Interactive elements use semantic HTML (
<button>,<a>,<input>) - Form inputs have associated labels (via
htmlForor wrapping) - Keyboard navigation works (Tab, Enter, Space, Arrow keys)
- Focus states are visible and styled
- ARIA roles and attributes used correctly
- Color contrast meets WCAG AA standards (4.5:1 for text)
- Screen reader announces states and changes
6. Panda CSS Styling Patterns
Base Styles:
css({
// Layout
display: 'flex',
flexDirection: 'column',
gap: 4,
w: 'full',
maxW: '7xl',
// Spacing
px: { base: '4', sm: '6', md: '8' },
py: { base: '6', sm: '8', md: '10' },
// Colors
bg: 'gray.900',
color: 'gray.100',
borderColor: 'gray.700',
// Glassmorphism
bg: 'rgba(255, 255, 255, 0.05)',
backdropFilter: 'blur(10px)',
border: '1px solid',
borderColor: 'rgba(255, 255, 255, 0.1)',
// Gradients
bgGradient: 'to-r',
gradientFrom: 'purple.400',
gradientTo: 'pink.600',
bgClip: 'text',
// States
_hover: { bg: 'purple.700' },
_focus: { outline: '2px solid', outlineColor: 'purple.500' },
_disabled: { opacity: 0.5, cursor: 'not-allowed' },
// Responsive
fontSize: { base: 'sm', md: 'md', lg: 'lg' },
gridTemplateColumns: { base: '1fr', sm: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' },
})
Combining Styles:
import { cx } from '@/styled-system/css'
<div className={cx(baseStyles, conditionalStyles, className)} />
7. TypeScript Interface Patterns
Extending HTML Elements:
// For div elements
export interface CardProps extends HTMLAttributes<HTMLDivElement> {
variant?: 'default' | 'outlined'
}
// For button elements
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
loading?: boolean
}
// For input elements
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
error?: string
}
// For form elements
export interface LabelProps extends LabelHTMLAttributes<HTMLLabelElement> {
required?: boolean
}
Using forwardRef for Refs:
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ children, className, ...props }, ref) => {
return (
<button ref={ref} className={className} {...props}>
{children}
</button>
)
}
)
Button.displayName = 'Button'
8. Create Component Tests
File: components/ui/__tests__/component-name.test.tsx
import { describe, expect, it } from 'vitest'
import { render, screen } from '@testing-library/react'
import { userEvent } from '@testing-library/user-event'
import { ComponentName } from '../component-name'
describe('ComponentName', () => {
it('renders with default props', () => {
render(<ComponentName>Content</ComponentName>)
expect(screen.getByText('Content')).toBeInTheDocument()
})
it('applies variant styles correctly', () => {
const { container } = render(
<ComponentName variant="outlined">Content</ComponentName>
)
const element = container.firstChild as HTMLElement
expect(element).toHaveClass('outlined')
})
it('handles click events', async () => {
const handleClick = vi.fn()
render(<ComponentName onClick={handleClick}>Click Me</ComponentName>)
await userEvent.click(screen.getByText('Click Me'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('respects disabled state', async () => {
const handleClick = vi.fn()
render(
<ComponentName disabled onClick={handleClick}>
Disabled
</ComponentName>
)
await userEvent.click(screen.getByText('Disabled'))
expect(handleClick).not.toHaveBeenCalled()
})
it('forwards ref correctly', () => {
const ref = createRef<HTMLDivElement>()
render(<ComponentName ref={ref}>Content</ComponentName>)
expect(ref.current).toBeInstanceOf(HTMLDivElement)
})
it('merges custom className with base styles', () => {
const { container } = render(
<ComponentName className="custom-class">Content</ComponentName>
)
expect(container.firstChild).toHaveClass('custom-class')
})
it('supports keyboard navigation', async () => {
const handleClick = vi.fn()
render(<ComponentName onClick={handleClick}>Press Enter</ComponentName>)
const element = screen.getByText('Press Enter')
element.focus()
await userEvent.keyboard('{Enter}')
expect(handleClick).toHaveBeenCalled()
})
it('meets accessibility requirements', () => {
render(
<ComponentName role="button" aria-label="Accessible button">
Content
</ComponentName>
)
const element = screen.getByRole('button')
expect(element).toHaveAccessibleName('Accessible button')
})
})
Test Coverage Checklist:
- Default rendering with no props
- All prop variants (size, variant, state)
- Event handlers (onClick, onChange, onFocus, etc.)
- Disabled state behavior
- Ref forwarding
- Custom className merging
- Keyboard interactions
- Accessibility attributes
- Edge cases (empty content, long text, etc.)
Run Tests:
pnpm test components/ui/__tests__/component-name.test.tsx
pnpm test -- --coverage
9. Document the Component
Add JSDoc comments:
/**
* Button component for user actions
*
* @example
* // Primary button
* <Button variant="primary" size="md" onClick={handleClick}>
* Click Me
* </Button>
*
* @example
* // Disabled button with icon
* <Button disabled>
* <Icon /> Loading...
* </Button>
*
* @param variant - Visual style: 'primary' | 'secondary' | 'ghost'
* @param size - Button size: 'sm' | 'md' | 'lg'
* @param disabled - Disable button interactions
*/
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** Visual style variant */
variant?: 'primary' | 'secondary' | 'ghost'
/** Button size */
size?: 'sm' | 'md' | 'lg'
/** Loading state - shows spinner */
loading?: boolean
}
Common Component Patterns
1. Button Component
- Variants: primary, secondary, ghost, danger
- Sizes: sm, md, lg
- States: default, hover, active, disabled, loading
- Icon support (left/right)
2. Input Component
- Types: text, email, password, number, tel, url
- States: default, focus, error, disabled
- Optional label, error message, helper text
- Icon support (left/right)
3. Card Component
- Composable: Card > CardHeader > CardTitle/CardDescription > CardContent > CardFooter
- Variants: default, outlined, ghost
- Optional hover effect
4. Dialog/Modal Component
- Open/close state management
- Backdrop with blur
- Keyboard handling (Escape to close)
- Focus trap
- Portal rendering
5. Tabs Component
- Context-based state sharing
- Keyboard navigation (Arrow keys)
- Accessibility (role="tablist", role="tab", aria-selected)
- Composable: Tabs > TabsList > TabsTrigger + TabsContent
6. Dropdown Component
- Click/hover trigger
- Portal rendering
- Keyboard navigation
- Accessible (aria-expanded, aria-haspopup)
Biome Lint Exceptions
When and how to use biome-ignore comments:
✅ Valid Use Cases:
// 1. Label component with htmlFor (not an actual error)
// biome-ignore lint/a11y/noLabelWithoutControl: Label component is used with htmlFor prop
<label htmlFor="input-id">...</label>
// 2. Intentional any type with justification
// biome-ignore lint/suspicious/noExplicitAny: JSON.parse returns unknown type
const parsed: any = JSON.parse(jsonString)
// 3. Validated HTML from trusted source
// biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD structured data is validated
<script dangerouslySetInnerHTML={{ __html: jsonLd }} />
❌ Invalid Use Cases:
// Don't ignore legitimate issues
// biome-ignore lint/a11y/useKeyWithClickEvents
<div onClick={handleClick} /> // Use <button> instead!
// Don't ignore without explanation
// biome-ignore
const result = doSomething()
// Don't ignore multiple rules at once
// biome-ignore lint/suspicious/noExplicitAny lint/complexity/noBannedTypes
Anti-Patterns to Avoid
❌ Don't Use Tailwind in Component Files
// WRONG
<button className="bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded">
Click Me
</button>
// CORRECT
<button className={css({
bg: 'purple.600',
_hover: { bg: 'purple.700' },
px: 4,
py: 2,
rounded: 'md'
})}>
Click Me
</button>
❌ Don't Use Div for Interactive Elements
// WRONG
<div onClick={handleClick} className={css({ cursor: 'pointer' })}>
Click Me
</div>
// CORRECT
<button type="button" onClick={handleClick}>
Click Me
</button>
❌ Don't Forget Keyboard Support
// WRONG
<div onClick={handleClick}>Click</div>
// CORRECT
<button
type="button"
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleClick()
}
}}
>
Click
</button>
❌ Don't Use Index as Key
// WRONG
{items.map((item, index) => <Item key={index} {...item} />)}
// CORRECT
{items.map(item => <Item key={item.id} {...item} />)}
Reference Implementations
Study these existing components:
- Button (
components/ui/button.tsx) - Variants, sizes, loading state - Input (
components/ui/input.tsx) - Form integration, error states - Card (
components/ui/card.tsx) - Composable pattern with subcomponents - Label (
components/ui/label.tsx) - Simple, accessible form label - Tabs (
components/ui/tabs.tsx) - Context API for state sharing - Textarea (
components/ui/textarea.tsx) - Multi-line input with auto-resize
Testing Strategy
Unit Tests (Required)
- Test each component in isolation
- Mock external dependencies
- Test all prop variations
- Test user interactions
- Test accessibility features
- Achieve >= 95% coverage
Integration Tests (Optional)
- Test component compositions
- Test with real form libraries
- Test with routing
Performance Considerations
- Use
memo()sparingly - Only for expensive components - Avoid inline object/array creation - Define outside render
- Use
useCallbackfor handlers - Only if passed to memoized children - Lazy load heavy components - Use
lazy()for dialogs, modals - Optimize re-renders - Use React DevTools Profiler
Component Library Structure
components/
├── ui/ # Reusable UI components
│ ├── button.tsx
│ ├── input.tsx
│ ├── card.tsx
│ ├── label.tsx # NEW
│ ├── tabs.tsx # NEW
│ ├── dialog.tsx
│ ├── dropdown.tsx
│ └── __tests__/ # Component tests
│ ├── button.test.tsx
│ ├── input.test.tsx
│ ├── label.test.tsx # Required for new components
│ └── tabs.test.tsx # Required for new components
├── features/ # Feature-specific components
├── layout/ # Layout components
└── providers/ # Context providers
Next Steps After Creating Component
Add to exports (if needed):
// components/ui/index.ts export { Button } from './button' export { Label } from './label' export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs'Document in Storybook (if available):
- Create
component-name.stories.tsx - Show all variants and states
- Provide usage examples
- Create
Update design system docs:
- Add component to design system
- Include do's and don'ts
- Show accessibility guidelines
Create usage examples:
- Add to example page
- Show common patterns
- Document edge cases
Related Resources
- Panda CSS Skill (
.github/skills/panda-css-styling/SKILL.md) - Styling patterns - Frontend Specialist Agent (
.github/agents/frontend-panda-css-specialist.agent.md) - Component architecture - Testing Coverage Skill (
.github/skills/testing-coverage/SKILL.md) - Testing patterns - New Tool Development Skill (
.github/skills/new-tool-development/SKILL.md) - Tool creation
Summary
Creating reusable components:
- ✅ Determine if extraction is necessary (2+ uses, common pattern)
- ✅ Create in
components/ui/with proper naming - ✅ Use TypeScript interfaces extending HTML element types
- ✅ Style with Panda CSS (never Tailwind in components)
- ✅ Meet accessibility requirements (ARIA, keyboard, semantics)
- ✅ Write comprehensive tests (>= 95% coverage)
- ✅ Document with JSDoc comments and examples
- ✅ Use
forwardReffor ref support - ✅ Support composition with
classNameprop - ✅ Follow existing component patterns
Remember: Only create components when you have a clear reuse case or it's a standard UI pattern. When in doubt, keep logic in the tool page and extract later when the pattern emerges.