@mks2508/mks-ui

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:

  1. Base styles (CVA variants) are applied first
  2. Slot overrides can extend or override base styles
  3. Consumer className has 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:

PropertyTypeDescription
hoveredbooleanMouse is over the element
pressedbooleanElement is being pressed
focusedbooleanElement has focus
disabledbooleanElement 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 with I prefix (e.g., IButtonProps)
  • .styles.ts — CVA variant definitions and slot style maps
  • index.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';