Create a new React UI component following the project's established patterns. Use when the user asks to create, add, or build a new component, UI element, or widget. Covers component file, types, styling with Tailwind + CSS variables, and ref forwarding.
Use this skill when the user asks to create a new UI component for the project. This includes buttons, cards, modals, form elements, data displays, or any reusable React component.
src/components/ui/{ComponentName}.tsxsrc/components/{feature}/{ComponentName}.tsxsrc/app/{route}/_components/{ComponentName}.tsxEvery component must follow this structure:
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
import { cn } from '@/lib/utils/cn';
// 1. Export types first
export type ComponentVariant = 'primary' | 'secondary';
export type ComponentSize = 'sm' | 'md' | 'lg';
export interface ComponentProps extends HTMLAttributes<HTMLDivElement> {
/** JSDoc for every prop */
variant?: ComponentVariant;
size?: ComponentSize;
}
// 2. Define style maps as Record<Variant, string>
const variantStyles: Record<ComponentVariant, string> = {
primary: cn('bg-(--color-primary) text-(--color-primary-fg)', 'hover:bg-(--color-primary-hover)'),
secondary: cn('bg-(--color-bg) text-(--color-fg)', 'border border-(--color-border)'),
};
const sizeStyles: Record<ComponentSize, string> = {
sm: 'h-9 px-4 text-sm',
md: 'h-11 px-5 text-base',
lg: 'h-13 px-7 text-lg',
};
// 3. Component with forwardRef
export const Component = forwardRef<HTMLDivElement, ComponentProps>(
({ className, variant = 'primary', size = 'md', children, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
// Base styles
'inline-flex items-center justify-center',
'rounded-xl',
'transition-all duration-200',
// Variant and size
variantStyles[variant],
sizeStyles[size],
// Custom classes (always last)
className
)}
{...props}
>
{children}
</div>
);
}
);
Component.displayName = 'Component';
// 4. Default export
export default Component;
Use when a component has sub-sections (like Card with Header/Content/Footer):
const ComponentRoot = forwardRef<HTMLDivElement, RootProps>(
({ className, children, ...props }, ref) => (
<div ref={ref} className={cn('base-styles', className)} {...props}>
{children}
</div>
)
);
ComponentRoot.displayName = 'Component';
const ComponentHeader = forwardRef<HTMLDivElement, HeaderProps>(/* ... */);
ComponentHeader.displayName = 'Component.Header';
const ComponentContent = forwardRef<HTMLDivElement, ContentProps>(/* ... */);
ComponentContent.displayName = 'Component.Content';
// Compose with Object.assign
export const Component = Object.assign(ComponentRoot, {
Header: ComponentHeader,
Content: ComponentContent,
});
bg-(--color-bg), text-(--color-fg), etc.bg-blue-500, text-gray-700)cn() from @/lib/utils/cn for class merging (combines clsx + tailwind-merge)bg-(--color-primary) not bg-[var(--color-primary)]transition-all duration-200 for animationsrounded-xl or rounded-2xl for border radius (modern rounded look)--color-bg, --color-bg-subtle, --color-bg-muted, --color-bg-elevated, --color-bg-hover, --color-bg-active--color-fg, --color-fg-muted, --color-fg-subtle--color-border, --color-border-muted--color-primary, --color-primary-hover, --color-primary-muted, --color-primary-fg--color-success, --color-warning, --color-error, --color-info (each with -hover, -muted, -fg)--shadow-xs, --shadow-sm, --shadow-md, --shadow-lg, --shadow-xlaria-* attributes where relevant (aria-disabled, aria-busy, aria-label)displayName on every forwardRef componentUse lucide-react for icons. Import individual icons:
import { ChevronDown, X, Check } from 'lucide-react';
tests/unit/components/ui/{Component}.test.tsxsrc/app/components/page.tsx if it's a reusable UI component