Standards for creating custom reusable React components with TypeScript, Tailwind CSS, and Radix Primitives. Use when building new UI components for web applications.
Guidelines for creating custom reusable React components following consistent patterns and best practices.
components/ui/.tsx extension (e.g., Button.tsx, TextField.tsx)Use default export with function declaration:
export default function ComponentName(props: ComponentNameProps) {
// Component implementation
}
Define props using interface extending React.ComponentProps:
interface ButtonProps extends React.ComponentProps<'button'> {
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
// Additional custom props
}
Key points:
interface keywordReact.ComponentProps<'element'> for HTML elementsUse string literal types for variants:
interface ComponentProps extends React.ComponentProps<'element'> {
variant?: 'primary' | 'secondary' | 'outline';
}
export default function Component({ variant = 'primary', ...props }: ComponentProps) {
const variantStyles = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
outline: 'border-2 border-blue-600 text-blue-600 hover:bg-blue-50'
};
return <element className={cn(variantStyles[variant], props.className)} {...props} />;
}
Always use the cn() utility from @/lib/cn.ts to merge Tailwind classes:
import { cn } from '@/lib/cn';
export default function Component({ className, ...props }: ComponentProps) {
return (
<element
className={cn(
'base-classes',
'hover:state-classes',
className
)}
{...props}
/>
);
}
Key points:
cn from @/lib/cn.tsclassName prop for composabilityclassName last to allow overridesUse Radix Primitives as the base for components when available:
import * as Slider from '@radix-ui/react-slider';
import { cn } from '@/lib/cn';
interface PriceSliderProps extends React.ComponentProps<typeof Slider.Root> {
// Custom props
}
export default function PriceSlider({ className, ...props }: PriceSliderProps) {
return (
<Slider.Root
className={cn(
'relative flex h-5 w-[200px] touch-none select-none items-center',
className
)}
{...props}
>
<Slider.Track className="relative h-[3px] grow rounded-full bg-gray-300">
<Slider.Range className="absolute h-full rounded-full bg-blue-600" />
</Slider.Track>
<Slider.Thumb
className="block size-5 rounded-full bg-white shadow-md hover:bg-gray-50"
aria-label="Price range"
/>
</Slider.Root>
);
}
Key points:
import * as ComponentName from '@radix-ui/react-component-name'React.ComponentProps<typeof Radix.Root>Forward refs only when needed (interactive elements, forms, animations):
import { forwardRef } from 'react';
interface InputProps extends React.ComponentProps<'input'> {
label?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, label, ...props }, ref) => {
return (
<div>
{label && <label className="text-sm font-medium">{label}</label>}
<input
ref={ref}
className={cn('border rounded px-3 py-2', className)}
{...props}
/>
</div>
);
}
);
Input.displayName = 'Input';
export default Input;
When to forward refs:
When NOT to forward refs: