Core Concepts
StyleSlots, CVA variants, slot overrides, and composition patterns in @mks2508/mks-ui.
Core Concepts
Understanding the architecture patterns used across all @mks2508/mks-ui components.
StyleSlots Pattern
Every component exposes a slots prop for fine-grained style customization. StyleSlots define the visual regions of a component:
import type { StyleSlots, SlotOverrides } from '@mks2508/mks-ui';
// Component defines its available slots
type ButtonSlot = 'root' | 'icon';
// StyleSlots is Record<SlotName, string>
type ButtonStyles = StyleSlots<ButtonSlot>;
// Consumers override slots via SlotOverrides
interface IButtonProps {
slots?: SlotOverrides<ButtonSlot>;
}Usage
<Button
slots={{
root: 'shadow-lg border-cyan-400/50',
icon: 'text-cyan-300',
}}
>
Custom styled
</Button>CVA Variants
Components use Class Variance Authority (CVA) for type-safe variant combinations:
import { cva, type VariantProps } from 'class-variance-authority';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md font-medium',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground',
outline: 'border border-input bg-background hover:bg-accent',
ghost: 'hover:bg-accent hover:text-accent-foreground',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: { variant: 'default', size: 'default' },
},
);className Merge Order
All components follow a consistent merge order using the cn() utility:
// base CVA → slot overrides → consumer className
className={cn(styles.root, slots?.root, className)}This means:
- Base styles (CVA variants) are applied first
- Slot overrides can extend or override base styles
- Consumer
classNamehas final priority
State-Based Styling
Many components accept className and style as functions that receive interaction state:
<DataCard
className={(state) =>
state.hovered ? 'scale-105 shadow-2xl' : 'scale-100'
}
style={(state) => ({
opacity: state.disabled ? 0.5 : 1,
transform: state.pressed ? 'scale(0.98)' : 'scale(1)',
})}
>
{/* Content */}
</DataCard>The state object typically includes:
| Property | Type | Description |
|---|---|---|
hovered | boolean | Mouse is over the element |
pressed | boolean | Element is being pressed |
focused | boolean | Element has focus |
disabled | boolean | Element is disabled |
asChild Composition
Components supporting asChild use the Slot primitive to merge props onto the child element:
import { Button } from '@mks2508/mks-ui/react';
{/* Renders as <a> with all Button styles */}
<Button asChild>
<a href="/docs">Read the docs</a>
</Button>The Slot primitive from @mks2508/mks-ui/react is motion-enhanced, supporting spring animations on composed elements.
3-File Component Structure
Every UI component follows a consistent file pattern:
ui/Button/
├── index.tsx # Component implementation + exports
├── Button.types.ts # IButtonProps, slot types, config interfaces
└── Button.styles.ts # CVA variants via StyleSlots pattern.types.ts— Interfaces withIprefix (e.g.,IButtonProps).styles.ts— CVA variant definitions and slot style mapsindex.tsx— Component implementation with JSDoc, forwardRef, displayName
IPrefix Convention
All interfaces use the I prefix, types do not:
// Interfaces → I prefix
export interface IButtonProps { /* ... */ }
export interface IBaseConfig { /* ... */ }
// Types → no prefix
export type ButtonSlot = 'root' | 'icon';
export type ButtonVariant = 'default' | 'destructive' | 'outline' | 'ghost';