Component Patterns
Consistent patterns used across Matrix UI components for predictable and maintainable code.
Variant Pattern
Components use class-variance-authority for managing visual variants:
import { cva } from 'class-variance-authority'
const componentVariants = cva(
'base-styles',
{
variants: {
variant: {
default: 'default-styles',
secondary: 'secondary-styles',
},
size: {
sm: 'small-styles',
md: 'medium-styles',
lg: 'large-styles',
},
},
defaultVariants: {
variant: 'default',
size: 'md',
},
}
)ForwardRef Pattern
Every component forwards refs to enable direct DOM access:
const Component = React.forwardRef<
HTMLElementType,
ComponentProps
>((props, ref) => {
return <element ref={ref} {...props} />
})
Component.displayName = 'Component'asChild Pattern
Components can render as different elements using Radix Slot:
import { Slot } from '@radix-ui/react-slot'
interface ComponentProps {
asChild?: boolean
}
const Component = ({ asChild, ...props }) => {
const Comp = asChild ? Slot : 'button'
return <Comp {...props} />
}
// Usage
<Component asChild>
<Link href="/home">Home</Link>
</Component>Loading State Pattern
Components handle loading states consistently:
interface ComponentProps {
loading?: boolean
disabled?: boolean
}
const Component = ({ loading, disabled, ...props }) => {
const isDisabled = disabled || loading
return (
<button
disabled={isDisabled}
aria-busy={loading}
{...props}
>
{loading && <Spinner className="mr-2" />}
{children}
</button>
)
}Icon Pattern
Components support left and right icon slots:
interface ButtonProps {
leftIcon?: React.ReactNode
rightIcon?: React.ReactNode
}
const Button = ({ leftIcon, rightIcon, children }) => (
<button>
{leftIcon && <span className="mr-2">{leftIcon}</span>}
{children}
{rightIcon && <span className="ml-2">{rightIcon}</span>}
</button>
)Controlled vs Uncontrolled Pattern
Form components support both controlled and uncontrolled usage:
// Controlled
const [value, setValue] = useState('')
<Input value={value} onChange={(e) => setValue(e.target.value)} />
// Uncontrolled
const inputRef = useRef<HTMLInputElement>(null)
<Input ref={inputRef} defaultValue="initial" />
// Component implementation
const Input = React.forwardRef(({ value, defaultValue, onChange, ...props }, ref) => {
// Internal state for uncontrolled mode
const [internalValue, setInternalValue] = useState(defaultValue)
const isControlled = value !== undefined
const currentValue = isControlled ? value : internalValue
const handleChange = (e) => {
if (!isControlled) {
setInternalValue(e.target.value)
}
onChange?.(e)
}
return <input value={currentValue} onChange={handleChange} ref={ref} {...props} />
})Class Name Merging Pattern
User classes always override default classes:
const Component = ({ className, ...props }) => {
return (
<div
className={cn(
'default-styles',
'base-styles',
className // User classes override defaults
)}
{...props}
/>
)
}Event Handler Composition
Internal handlers compose with user-provided handlers:
const Component = ({ onClick, onKeyDown, ...props }) => {
const handleClick = (e) => {
// Internal logic
console.log('Internal click')
// Call user handler
onClick?.(e)
}
const handleKeyDown = (e) => {
// Internal keyboard handling
if (e.key === 'Enter') {
e.preventDefault()
}
// Call user handler
onKeyDown?.(e)
}
return <button onClick={handleClick} onKeyDown={handleKeyDown} {...props} />
}Size Prop Pattern
Consistent size naming across components:
type Size = 'sm' | 'md' | 'lg'
interface ComponentProps {
size?: Size
}
// Consistent across all components
<Button size="sm" />
<Input size="sm" />
<Select size="sm" />
<Badge size="sm" />Full Width Pattern
Components support full width mode:
interface ComponentProps {
fullWidth?: boolean
}
const Component = ({ fullWidth, className, ...props }) => (
<div
className={cn(
'inline-flex',
fullWidth && 'w-full',
className
)}
{...props}
/>
)Data Attribute Pattern
Components expose state via data attributes for styling:
<button
data-state={open ? 'open' : 'closed'}
data-disabled={disabled}
data-loading={loading}
>
/* CSS styling based on data attributes */
[data-state="open"] { }
[data-disabled="true"] { }
[data-loading="true"] { }Error State Pattern
Form components handle error states:
interface InputProps {
error?: boolean | string
}
const Input = ({ error, className, ...props }) => (
<input
className={cn(
'base-styles',
error && 'border-destructive focus:ring-destructive',
className
)}
aria-invalid={!!error}
aria-describedby={error ? 'error-message' : undefined}
{...props}
/>
)Best Practices
- Always use the same prop names across components
- Provide sensible defaults for all optional props
- Support both controlled and uncontrolled modes for forms
- Allow className override for all styling
- Forward all HTML attributes to the underlying element
- Compose event handlers rather than replacing them
- Use TypeScript for prop validation
- Keep patterns consistent across the library