...
// Better:
+
+// Document structure roles
+...
+...
+```
+
+### States and Properties
+
+States indicate current conditions; properties describe relationships.
+
+```tsx
+// States (can change)
+aria-checked="true|false|mixed"
+aria-disabled="true|false"
+aria-expanded="true|false"
+aria-hidden="true|false"
+aria-pressed="true|false"
+aria-selected="true|false"
+
+// Properties (usually static)
+aria-label="Accessible name"
+aria-labelledby="id-of-label"
+aria-describedby="id-of-description"
+aria-controls="id-of-controlled-element"
+aria-owns="id-of-owned-element"
+aria-live="polite|assertive|off"
+```
+
+## Common ARIA Patterns
+
+### Accordion
+
+```tsx
+function Accordion({ items }) {
+ const [openIndex, setOpenIndex] = useState(-1);
+
+ return (
+
+ {items.map((item, index) => {
+ const isOpen = openIndex === index;
+ const headingId = `accordion-heading-${index}`;
+ const panelId = `accordion-panel-${index}`;
+
+ return (
+
+
+
+
+
+ {item.content}
+
+
+ );
+ })}
+
+ );
+}
+```
+
+### Tabs
+
+```tsx
+function Tabs({ tabs }) {
+ const [activeIndex, setActiveIndex] = useState(0);
+ const tabListRef = useRef(null);
+
+ const handleKeyDown = (e, index) => {
+ let newIndex = index;
+
+ switch (e.key) {
+ case 'ArrowRight':
+ newIndex = (index + 1) % tabs.length;
+ break;
+ case 'ArrowLeft':
+ newIndex = (index - 1 + tabs.length) % tabs.length;
+ break;
+ case 'Home':
+ newIndex = 0;
+ break;
+ case 'End':
+ newIndex = tabs.length - 1;
+ break;
+ default:
+ return;
+ }
+
+ e.preventDefault();
+ setActiveIndex(newIndex);
+ tabListRef.current?.children[newIndex]?.focus();
+ };
+
+ return (
+
+
+ {tabs.map((tab, index) => (
+
+ ))}
+
+
+ {tabs.map((tab, index) => (
+
+ {tab.content}
+
+ ))}
+
+ );
+}
+```
+
+### Menu Button
+
+```tsx
+function MenuButton({ label, items }) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [activeIndex, setActiveIndex] = useState(-1);
+ const buttonRef = useRef(null);
+ const menuRef = useRef(null);
+ const menuId = useId();
+
+ const handleKeyDown = (e) => {
+ switch (e.key) {
+ case 'ArrowDown':
+ e.preventDefault();
+ if (!isOpen) {
+ setIsOpen(true);
+ setActiveIndex(0);
+ } else {
+ setActiveIndex((prev) => Math.min(prev + 1, items.length - 1));
+ }
+ break;
+ case 'ArrowUp':
+ e.preventDefault();
+ setActiveIndex((prev) => Math.max(prev - 1, 0));
+ break;
+ case 'Escape':
+ setIsOpen(false);
+ buttonRef.current?.focus();
+ break;
+ case 'Enter':
+ case ' ':
+ if (isOpen && activeIndex >= 0) {
+ e.preventDefault();
+ items[activeIndex].onClick();
+ setIsOpen(false);
+ }
+ break;
+ }
+ };
+
+ // Focus management
+ useEffect(() => {
+ if (isOpen && activeIndex >= 0) {
+ menuRef.current?.children[activeIndex]?.focus();
+ }
+ }, [isOpen, activeIndex]);
+
+ return (
+
+
+
+ {isOpen && (
+
+ )}
+
+ );
+}
+```
+
+### Combobox (Autocomplete)
+
+```tsx
+function Combobox({ options, onSelect, placeholder }) {
+ const [inputValue, setInputValue] = useState('');
+ const [isOpen, setIsOpen] = useState(false);
+ const [activeIndex, setActiveIndex] = useState(-1);
+ const inputRef = useRef(null);
+ const listboxId = useId();
+
+ const filteredOptions = options.filter((opt) =>
+ opt.toLowerCase().includes(inputValue.toLowerCase())
+ );
+
+ const handleKeyDown = (e) => {
+ switch (e.key) {
+ case 'ArrowDown':
+ e.preventDefault();
+ setIsOpen(true);
+ setActiveIndex((prev) =>
+ Math.min(prev + 1, filteredOptions.length - 1)
+ );
+ break;
+ case 'ArrowUp':
+ e.preventDefault();
+ setActiveIndex((prev) => Math.max(prev - 1, 0));
+ break;
+ case 'Enter':
+ if (activeIndex >= 0) {
+ e.preventDefault();
+ selectOption(filteredOptions[activeIndex]);
+ }
+ break;
+ case 'Escape':
+ setIsOpen(false);
+ setActiveIndex(-1);
+ break;
+ }
+ };
+
+ const selectOption = (option) => {
+ setInputValue(option);
+ onSelect(option);
+ setIsOpen(false);
+ setActiveIndex(-1);
+ };
+
+ return (
+
+
= 0 ? `option-${activeIndex}` : undefined
+ }
+ aria-autocomplete="list"
+ value={inputValue}
+ placeholder={placeholder}
+ onChange={(e) => {
+ setInputValue(e.target.value);
+ setIsOpen(true);
+ setActiveIndex(-1);
+ }}
+ onKeyDown={handleKeyDown}
+ onFocus={() => setIsOpen(true)}
+ onBlur={() => setTimeout(() => setIsOpen(false), 200)}
+ />
+
+ {isOpen && filteredOptions.length > 0 && (
+
+ {filteredOptions.map((option, index) => (
+ - selectOption(option)}
+ onMouseEnter={() => setActiveIndex(index)}
+ >
+ {option}
+
+ ))}
+
+ )}
+
+ );
+}
+```
+
+### Alert Dialog
+
+```tsx
+function AlertDialog({ isOpen, onConfirm, onCancel, title, message }) {
+ const confirmRef = useRef(null);
+ const dialogId = useId();
+ const titleId = `${dialogId}-title`;
+ const descId = `${dialogId}-desc`;
+
+ useEffect(() => {
+ if (isOpen) {
+ confirmRef.current?.focus();
+ }
+ }, [isOpen]);
+
+ if (!isOpen) return null;
+
+ return (
+
+
+
+
+
+
{title}
+
{message}
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+### Toolbar
+
+```tsx
+function Toolbar({ items }) {
+ const [activeIndex, setActiveIndex] = useState(0);
+ const toolbarRef = useRef(null);
+
+ const handleKeyDown = (e) => {
+ let newIndex = activeIndex;
+
+ switch (e.key) {
+ case 'ArrowRight':
+ newIndex = (activeIndex + 1) % items.length;
+ break;
+ case 'ArrowLeft':
+ newIndex = (activeIndex - 1 + items.length) % items.length;
+ break;
+ case 'Home':
+ newIndex = 0;
+ break;
+ case 'End':
+ newIndex = items.length - 1;
+ break;
+ default:
+ return;
+ }
+
+ e.preventDefault();
+ setActiveIndex(newIndex);
+ toolbarRef.current?.querySelectorAll('button')[newIndex]?.focus();
+ };
+
+ return (
+
+ {items.map((item, index) => (
+
+ ))}
+
+ );
+}
+```
+
+## Live Regions
+
+### Polite Announcements
+
+```tsx
+// Status messages that don't interrupt
+function SearchStatus({ count, query }) {
+ return (
+
+ {count} results found for "{query}"
+
+ );
+}
+
+// Progress indicator
+function LoadingStatus({ progress }) {
+ return (
+
+ Loading: {progress}% complete
+
+ );
+}
+```
+
+### Assertive Announcements
+
+```tsx
+// Important errors that should interrupt
+function ErrorAlert({ message }) {
+ return (
+
+ Error: {message}
+
+ );
+}
+
+// Form validation summary
+function ValidationSummary({ errors }) {
+ if (errors.length === 0) return null;
+
+ return (
+
+
Please fix the following errors:
+
+ {errors.map((error, index) => (
+ - {error}
+ ))}
+
+
+ );
+}
+```
+
+### Log Region
+
+```tsx
+// Chat messages or activity log
+function ChatLog({ messages }) {
+ return (
+
+ {messages.map((msg) => (
+
+ {msg.author}:
+ {msg.text}
+
+ ))}
+
+ );
+}
+```
+
+## Common Mistakes to Avoid
+
+### 1. Redundant ARIA
+
+```tsx
+// Bad: role="button" on a button
+
+
+// Good: just use button
+
+
+// Bad: aria-label duplicating visible text
+
+
+// Good: just use visible text
+
+```
+
+### 2. Invalid ARIA
+
+```tsx
+// Bad: aria-selected on non-selectable element
+Item
+
+// Good: use with proper role
+Item
+
+// Bad: aria-expanded without control relationship
+
+Menu content
+
+// Good: with aria-controls
+
+
+```
+
+### 3. Hidden Content Still Announced
+
+```tsx
+// Bad: visually hidden but still in accessibility tree
+Hidden content
+
+// Good: properly hidden
+Hidden content
+
+// Or just use display: none (implicitly hidden)
+Hidden content
+```
+
+## Resources
+
+- [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
+- [ARIA in HTML](https://www.w3.org/TR/html-aria/)
+- [Using ARIA](https://www.w3.org/TR/using-aria/)
diff --git a/plugins/ui-design/skills/accessibility-compliance/references/mobile-accessibility.md b/plugins/ui-design/skills/accessibility-compliance/references/mobile-accessibility.md
new file mode 100644
index 0000000..909fba4
--- /dev/null
+++ b/plugins/ui-design/skills/accessibility-compliance/references/mobile-accessibility.md
@@ -0,0 +1,539 @@
+# Mobile Accessibility
+
+## Overview
+
+Mobile accessibility ensures apps work for users with disabilities on iOS and Android devices. This includes support for screen readers (VoiceOver, TalkBack), motor impairments, and various visual disabilities.
+
+## Touch Target Sizing
+
+### Minimum Sizes
+
+```css
+/* WCAG 2.2 Level AA: 24x24px minimum */
+.interactive-element {
+ min-width: 24px;
+ min-height: 24px;
+}
+
+/* WCAG 2.2 Level AAA / Apple HIG / Material Design: 44x44dp */
+.touch-target {
+ min-width: 44px;
+ min-height: 44px;
+}
+
+/* Android Material Design: 48x48dp recommended */
+.android-touch-target {
+ min-width: 48px;
+ min-height: 48px;
+}
+```
+
+### Touch Target Spacing
+
+```tsx
+// Ensure adequate spacing between touch targets
+function ButtonGroup({ buttons }) {
+ return (
+ {/* 12px minimum gap */}
+ {buttons.map((btn) => (
+
+ ))}
+
+ );
+}
+
+// Expanding hit area without changing visual size
+function IconButton({ icon, label, onClick }) {
+ return (
+
+ );
+}
+```
+
+## iOS VoiceOver
+
+### React Native Accessibility Props
+
+```tsx
+import { View, Text, TouchableOpacity, AccessibilityInfo } from 'react-native';
+
+// Basic accessible button
+function AccessibleButton({ onPress, title, hint }) {
+ return (
+
+ {title}
+
+ );
+}
+
+// Complex component with grouped content
+function ProductCard({ product }) {
+ return (
+ {
+ switch (event.nativeEvent.actionName) {
+ case 'addToCart':
+ addToCart(product);
+ break;
+ case 'activate':
+ viewDetails(product);
+ break;
+ }
+ }}
+ >
+
+ {product.name}
+ {product.price}
+
+ );
+}
+
+// Announcing dynamic changes
+function Counter() {
+ const [count, setCount] = useState(0);
+
+ const increment = () => {
+ setCount((prev) => prev + 1);
+ AccessibilityInfo.announceForAccessibility(`Count is now ${count + 1}`);
+ };
+
+ return (
+
+
+ Count: {count}
+
+
+ +
+
+
+ );
+}
+```
+
+### SwiftUI Accessibility
+
+```swift
+import SwiftUI
+
+struct AccessibleButton: View {
+ let title: String
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ Text(title)
+ }
+ .accessibilityLabel(title)
+ .accessibilityHint("Double tap to activate")
+ .accessibilityAddTraits(.isButton)
+ }
+}
+
+struct ProductCard: View {
+ let product: Product
+
+ var body: some View {
+ VStack {
+ AsyncImage(url: product.imageURL)
+ .accessibilityHidden(true) // Image is decorative
+
+ Text(product.name)
+ Text(product.price.formatted(.currency(code: "USD")))
+ }
+ .accessibilityElement(children: .combine)
+ .accessibilityLabel("\(product.name), \(product.price.formatted(.currency(code: "USD")))")
+ .accessibilityHint("Double tap to view details")
+ .accessibilityAction(named: "Add to cart") {
+ addToCart(product)
+ }
+ }
+}
+
+// Custom accessibility rotor
+struct DocumentView: View {
+ let sections: [Section]
+
+ var body: some View {
+ ScrollView {
+ ForEach(sections) { section in
+ Text(section.title)
+ .font(.headline)
+ .accessibilityAddTraits(.isHeader)
+ Text(section.content)
+ }
+ }
+ .accessibilityRotor("Headings") {
+ ForEach(sections) { section in
+ AccessibilityRotorEntry(section.title, id: section.id)
+ }
+ }
+ }
+}
+```
+
+## Android TalkBack
+
+### Jetpack Compose Accessibility
+
+```kotlin
+import androidx.compose.ui.semantics.*
+
+@Composable
+fun AccessibleButton(
+ onClick: () -> Unit,
+ text: String,
+ enabled: Boolean = true
+) {
+ Button(
+ onClick = onClick,
+ enabled = enabled,
+ modifier = Modifier.semantics {
+ contentDescription = text
+ role = Role.Button
+ if (!enabled) {
+ disabled()
+ }
+ }
+ ) {
+ Text(text)
+ }
+}
+
+@Composable
+fun ProductCard(product: Product) {
+ Card(
+ modifier = Modifier
+ .semantics(mergeDescendants = true) {
+ contentDescription = "${product.name}, ${product.formattedPrice}"
+ customActions = listOf(
+ CustomAccessibilityAction("Add to cart") {
+ addToCart(product)
+ true
+ }
+ )
+ }
+ .clickable { navigateToDetails(product) }
+ ) {
+ Image(
+ painter = painterResource(product.imageRes),
+ contentDescription = null, // Decorative
+ modifier = Modifier.semantics { invisibleToUser() }
+ )
+ Text(product.name)
+ Text(product.formattedPrice)
+ }
+}
+
+// Live region for dynamic content
+@Composable
+fun Counter() {
+ var count by remember { mutableStateOf(0) }
+
+ Column {
+ Text(
+ text = "Count: $count",
+ modifier = Modifier.semantics {
+ liveRegion = LiveRegionMode.Polite
+ }
+ )
+ Button(onClick = { count++ }) {
+ Text("Increment")
+ }
+ }
+}
+
+// Heading levels
+@Composable
+fun SectionHeader(title: String, level: Int) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.headlineMedium,
+ modifier = Modifier.semantics {
+ heading()
+ // Custom heading level (not built-in)
+ testTag = "heading-$level"
+ }
+ )
+}
+```
+
+### Android XML Views
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```kotlin
+// Kotlin accessibility
+binding.submitButton.apply {
+ contentDescription = getString(R.string.submit_form)
+ accessibilityDelegate = object : View.AccessibilityDelegate() {
+ override fun onInitializeAccessibilityNodeInfo(
+ host: View,
+ info: AccessibilityNodeInfo
+ ) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ info.addAction(
+ AccessibilityNodeInfo.AccessibilityAction(
+ AccessibilityNodeInfo.ACTION_CLICK,
+ getString(R.string.submit_action)
+ )
+ )
+ }
+ }
+}
+
+// Announce changes
+binding.counter.announceForAccessibility("Count updated to $count")
+```
+
+## Gesture Accessibility
+
+### Alternative Gestures
+
+```tsx
+// React Native: Provide alternatives to complex gestures
+function SwipeableCard({ item, onDelete }) {
+ const [showDelete, setShowDelete] = useState(false);
+
+ return (
+ {
+ if (event.nativeEvent.actionName === 'delete') {
+ onDelete(item);
+ }
+ }}
+ >
+ (
+ onDelete(item)}
+ accessibilityLabel="Delete"
+ >
+ Delete
+
+ )}
+ >
+ {item.title}
+
+
+ {/* Alternative for screen reader users */}
+ onDelete(item)}
+ style={{ position: 'absolute', right: 0 }}
+ >
+ Delete
+
+
+ );
+}
+```
+
+### Motion and Animation
+
+```tsx
+// Respect reduced motion preference
+import { AccessibilityInfo } from 'react-native';
+
+function AnimatedComponent() {
+ const [reduceMotion, setReduceMotion] = useState(false);
+
+ useEffect(() => {
+ AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion);
+
+ const subscription = AccessibilityInfo.addEventListener(
+ 'reduceMotionChanged',
+ setReduceMotion
+ );
+
+ return () => subscription.remove();
+ }, []);
+
+ return (
+
+
+
+ );
+}
+```
+
+## Dynamic Type / Text Scaling
+
+### iOS Dynamic Type
+
+```swift
+// SwiftUI
+Text("Hello, World!")
+ .font(.body) // Automatically scales with Dynamic Type
+
+Text("Fixed Size")
+ .font(.system(size: 16, design: .default))
+ .dynamicTypeSize(.large) // Cap at large
+
+// Allow unlimited scaling
+Text("Scalable")
+ .font(.body)
+ .minimumScaleFactor(0.5)
+ .lineLimit(nil)
+```
+
+### Android Text Scaling
+
+```xml
+
+
+
+
+
+```
+
+```kotlin
+// Compose: Text automatically scales
+Text(
+ text = "Hello, World!",
+ style = MaterialTheme.typography.bodyLarge
+)
+
+// Limit scaling if needed
+Text(
+ text = "Limited scaling",
+ fontSize = 16.sp,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+)
+```
+
+### React Native Text Scaling
+
+```tsx
+import { Text, PixelRatio } from 'react-native';
+
+// Allow text scaling (default)
+Scalable text
+
+// Limit maximum scale
+Limited scaling
+
+// Disable scaling (use sparingly)
+Fixed size
+
+// Responsive font size
+const scaledFontSize = (size: number) => {
+ const scale = PixelRatio.getFontScale();
+ return size * Math.min(scale, 1.5); // Cap at 1.5x
+};
+```
+
+## Testing Checklist
+
+```markdown
+## VoiceOver (iOS) Testing
+- [ ] All interactive elements have labels
+- [ ] Swipe navigation covers all content in logical order
+- [ ] Custom actions available for complex interactions
+- [ ] Announcements made for dynamic content
+- [ ] Headings navigable via rotor
+- [ ] Images have appropriate descriptions or are hidden
+
+## TalkBack (Android) Testing
+- [ ] Focus order is logical
+- [ ] Touch exploration works correctly
+- [ ] Custom actions available
+- [ ] Live regions announce updates
+- [ ] Headings properly marked
+- [ ] Grouped content read together
+
+## Motor Accessibility
+- [ ] Touch targets at least 44x44 points
+- [ ] Adequate spacing between targets (8dp minimum)
+- [ ] Alternatives to complex gestures
+- [ ] No time-limited interactions
+
+## Visual Accessibility
+- [ ] Text scales to 200% without loss
+- [ ] Content visible in high contrast mode
+- [ ] Color not sole indicator
+- [ ] Animations respect reduced motion
+```
+
+## Resources
+
+- [Apple Accessibility Programming Guide](https://developer.apple.com/accessibility/)
+- [Android Accessibility Developer Guide](https://developer.android.com/guide/topics/ui/accessibility)
+- [React Native Accessibility](https://reactnative.dev/docs/accessibility)
+- [Mobile Accessibility WCAG](https://www.w3.org/TR/mobile-accessibility-mapping/)
diff --git a/plugins/ui-design/skills/accessibility-compliance/references/wcag-guidelines.md b/plugins/ui-design/skills/accessibility-compliance/references/wcag-guidelines.md
new file mode 100644
index 0000000..d2fbae0
--- /dev/null
+++ b/plugins/ui-design/skills/accessibility-compliance/references/wcag-guidelines.md
@@ -0,0 +1,632 @@
+# WCAG 2.2 Guidelines Reference
+
+## Overview
+
+The Web Content Accessibility Guidelines (WCAG) 2.2 provide recommendations for making web content more accessible. They are organized into four principles (POUR): Perceivable, Operable, Understandable, and Robust.
+
+## Conformance Levels
+
+- **Level A**: Minimum accessibility (must satisfy)
+- **Level AA**: Standard accessibility (should satisfy)
+- **Level AAA**: Enhanced accessibility (may satisfy)
+
+Most organizations target Level AA compliance.
+
+## Principle 1: Perceivable
+
+Content must be presentable in ways users can perceive.
+
+### 1.1 Text Alternatives
+
+#### 1.1.1 Non-text Content (Level A)
+
+All non-text content needs text alternatives.
+
+```tsx
+// Images
+
+
+// Decorative images
+
+
+// Complex images with long descriptions
+
+
+
+ The CEO reports to the board. Three VPs report to the CEO:
+ VP Engineering, VP Sales, and VP Marketing...
+
+
+
+// Icons with meaning
+
+
+// Icon buttons with visible text
+
+```
+
+### 1.2 Time-based Media
+
+#### 1.2.1 Audio-only and Video-only (Level A)
+
+```tsx
+// Audio with transcript
+
+
+ View transcript
+ Full transcript text here...
+
+
+// Video with captions
+
+```
+
+### 1.3 Adaptable
+
+#### 1.3.1 Info and Relationships (Level A)
+
+Structure and relationships must be programmatically determinable.
+
+```tsx
+// Proper heading hierarchy
+
+ Page Title
+
+ Section Title
+ Subsection
+
+
+
+// Data tables with headers
+
+ Quarterly Sales Report
+
+
+ | Product |
+ Q1 |
+ Q2 |
+
+
+
+
+ | Widget A |
+ $10,000 |
+ $12,000 |
+
+
+
+
+// Lists for grouped content
+
+```
+
+#### 1.3.5 Identify Input Purpose (Level AA)
+
+```tsx
+// Input with autocomplete for autofill
+
+```
+
+### 1.4 Distinguishable
+
+#### 1.4.1 Use of Color (Level A)
+
+```tsx
+// Bad: Color only indicates error
+
+
+// Good: Color plus icon and text
+
+
+ {hasError && (
+
+
+ This field is required
+
+ )}
+
+```
+
+#### 1.4.3 Contrast (Minimum) (Level AA)
+
+```css
+/* Minimum contrast ratios */
+/* Normal text: 4.5:1 */
+/* Large text (18pt+ or 14pt bold+): 3:1 */
+
+/* Good contrast examples */
+.text-on-white {
+ color: #595959; /* 7:1 ratio on white */
+}
+
+.text-on-dark {
+ color: #ffffff;
+ background: #333333; /* 12.6:1 ratio */
+}
+
+/* Link must be distinguishable from surrounding text */
+.link {
+ color: #0066cc; /* 4.5:1 on white */
+ text-decoration: underline; /* Additional visual cue */
+}
+```
+
+#### 1.4.11 Non-text Contrast (Level AA)
+
+```css
+/* UI components need 3:1 contrast */
+.button {
+ border: 2px solid #767676; /* 3:1 against white */
+ background: white;
+}
+
+.input {
+ border: 1px solid #767676;
+}
+
+.input:focus {
+ outline: 2px solid #0066cc; /* Focus indicator needs 3:1 */
+ outline-offset: 2px;
+}
+
+/* Custom checkbox */
+.checkbox {
+ border: 2px solid #767676;
+}
+
+.checkbox:checked {
+ background: #0066cc;
+ border-color: #0066cc;
+}
+```
+
+#### 1.4.12 Text Spacing (Level AA)
+
+Content must not be lost when user adjusts text spacing.
+
+```css
+/* Allow text spacing adjustments without breaking layout */
+.content {
+ /* Use relative units */
+ line-height: 1.5; /* At least 1.5x font size */
+ letter-spacing: 0.12em; /* Support for 0.12em */
+ word-spacing: 0.16em; /* Support for 0.16em */
+
+ /* Don't use fixed heights on text containers */
+ min-height: auto;
+
+ /* Allow wrapping */
+ overflow-wrap: break-word;
+}
+
+/* Test with these values: */
+/* Line height: 1.5x font size */
+/* Letter spacing: 0.12em */
+/* Word spacing: 0.16em */
+/* Paragraph spacing: 2x font size */
+```
+
+#### 1.4.13 Content on Hover or Focus (Level AA)
+
+```tsx
+// Tooltip pattern
+function Tooltip({ content, children }) {
+ const [isVisible, setIsVisible] = useState(false);
+
+ return (
+ setIsVisible(true)}
+ onMouseLeave={() => setIsVisible(false)}
+ onFocus={() => setIsVisible(true)}
+ onBlur={() => setIsVisible(false)}
+ >
+ {children}
+ {isVisible && (
+
e.key === 'Escape' && setIsVisible(false)}
+ // Hoverable: content stays visible when pointer moves to it
+ onMouseEnter={() => setIsVisible(true)}
+ onMouseLeave={() => setIsVisible(false)}
+ // Persistent: stays until trigger loses focus/hover
+ >
+ {content}
+
+ )}
+
+ );
+}
+```
+
+## Principle 2: Operable
+
+Interface components must be operable by all users.
+
+### 2.1 Keyboard Accessible
+
+#### 2.1.1 Keyboard (Level A)
+
+All functionality must be operable via keyboard.
+
+```tsx
+// Custom interactive element
+function CustomButton({ onClick, children }) {
+ return (
+ {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onClick();
+ }
+ }}
+ >
+ {children}
+
+ );
+}
+
+// Better: just use a button
+function BetterButton({ onClick, children }) {
+ return ;
+}
+```
+
+#### 2.1.2 No Keyboard Trap (Level A)
+
+```tsx
+// Modal with proper focus management
+function Modal({ isOpen, onClose, children }) {
+ const closeButtonRef = useRef(null);
+
+ // Return focus on close
+ useEffect(() => {
+ if (!isOpen) return;
+
+ const previousFocus = document.activeElement;
+ closeButtonRef.current?.focus();
+
+ return () => {
+ (previousFocus as HTMLElement)?.focus();
+ };
+ }, [isOpen]);
+
+ // Allow Escape to close
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') onClose();
+ };
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, [onClose]);
+
+ return (
+
+
+
+ {children}
+
+
+ );
+}
+```
+
+### 2.4 Navigable
+
+#### 2.4.1 Bypass Blocks (Level A)
+
+```tsx
+// Skip links
+
+ Skip to main content
+ Skip to navigation
+
+
+
+
+
+
+ {/* Main content */}
+
+
+```
+
+#### 2.4.4 Link Purpose (In Context) (Level A)
+
+```tsx
+// Bad: Ambiguous link text
+Click here
+Read more
+
+// Good: Descriptive link text
+View quarterly sales report
+
+// Good: Context provides meaning
+
+ Quarterly Sales Report
+ Sales increased by 25% this quarter...
+ Read full report
+
+
+// Good: Visually hidden text for context
+
+ Read more
+ about quarterly sales report
+
+```
+
+#### 2.4.7 Focus Visible (Level AA)
+
+```css
+/* Always show focus indicator */
+:focus-visible {
+ outline: 2px solid var(--color-focus);
+ outline-offset: 2px;
+}
+
+/* Custom focus styles */
+.button:focus-visible {
+ outline: none;
+ box-shadow: 0 0 0 3px var(--color-focus);
+}
+
+/* High visibility focus for links */
+.link:focus-visible {
+ outline: 3px solid var(--color-focus);
+ outline-offset: 2px;
+ background: var(--color-focus-bg);
+}
+```
+
+### 2.5 Input Modalities (New in 2.2)
+
+#### 2.5.8 Target Size (Minimum) (Level AA) - NEW
+
+Interactive targets must be at least 24x24 CSS pixels.
+
+```css
+/* Minimum target size */
+.interactive {
+ min-width: 24px;
+ min-height: 24px;
+}
+
+/* Recommended size for touch (44x44) */
+.touch-target {
+ min-width: 44px;
+ min-height: 44px;
+}
+
+/* Inline links are exempt if they have adequate spacing */
+.link {
+ /* Inline text links don't need minimum size */
+ /* but should have adequate line-height */
+ line-height: 1.5;
+}
+```
+
+## Principle 3: Understandable
+
+Content and interface must be understandable.
+
+### 3.1 Readable
+
+#### 3.1.1 Language of Page (Level A)
+
+```html
+
+
+ ...
+ ...
+
+```
+
+#### 3.1.2 Language of Parts (Level AA)
+
+```tsx
+
+ The French phrase c'est la vie means "that's life."
+
+```
+
+### 3.2 Predictable
+
+#### 3.2.2 On Input (Level A)
+
+Don't automatically change context on input.
+
+```tsx
+// Bad: Auto-submit on selection
+
+
+// Good: Explicit submit action
+
+
+```
+
+### 3.3 Input Assistance
+
+#### 3.3.1 Error Identification (Level A)
+
+```tsx
+function FormField({ id, label, error, ...props }) {
+ return (
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+}
+```
+
+#### 3.3.7 Redundant Entry (Level A) - NEW
+
+Don't require users to re-enter previously provided information.
+
+```tsx
+// Auto-fill shipping address from billing
+function CheckoutForm() {
+ const [sameAsBilling, setSameAsBilling] = useState(false);
+ const [billing, setBilling] = useState({});
+ const [shipping, setShipping] = useState({});
+
+ return (
+
+ );
+}
+```
+
+## Principle 4: Robust
+
+Content must be robust enough for assistive technologies.
+
+### 4.1 Compatible
+
+#### 4.1.2 Name, Role, Value (Level A)
+
+```tsx
+// Custom components must expose name, role, and value
+function CustomCheckbox({ checked, onChange, label }) {
+ return (
+
+ );
+}
+
+// Custom slider
+function CustomSlider({ value, min, max, label, onChange }) {
+ return (
+ {
+ if (e.key === 'ArrowRight') onChange(Math.min(value + 1, max));
+ if (e.key === 'ArrowLeft') onChange(Math.max(value - 1, min));
+ }}
+ >
+
+
+ );
+}
+```
+
+## Testing Checklist
+
+```markdown
+## Keyboard Testing
+- [ ] All interactive elements focusable with Tab
+- [ ] Focus order matches visual order
+- [ ] Focus indicator always visible
+- [ ] No keyboard traps
+- [ ] Escape closes modals/dropdowns
+- [ ] Enter/Space activates buttons and links
+
+## Screen Reader Testing
+- [ ] All images have alt text
+- [ ] Form inputs have labels
+- [ ] Headings in logical order
+- [ ] Landmarks present (main, nav, header, footer)
+- [ ] Dynamic content announced
+- [ ] Error messages announced
+
+## Visual Testing
+- [ ] Text contrast at least 4.5:1
+- [ ] UI component contrast at least 3:1
+- [ ] Works at 200% zoom
+- [ ] Content readable with text spacing
+- [ ] Focus indicators visible
+- [ ] Color not sole indicator of meaning
+```
+
+## Resources
+
+- [WCAG 2.2 Quick Reference](https://www.w3.org/WAI/WCAG22/quickref/)
+- [Understanding WCAG 2.2](https://www.w3.org/WAI/WCAG22/Understanding/)
+- [Techniques for WCAG 2.2](https://www.w3.org/WAI/WCAG22/Techniques/)
diff --git a/plugins/ui-design/skills/design-system-patterns/SKILL.md b/plugins/ui-design/skills/design-system-patterns/SKILL.md
new file mode 100644
index 0000000..92ac32f
--- /dev/null
+++ b/plugins/ui-design/skills/design-system-patterns/SKILL.md
@@ -0,0 +1,318 @@
+---
+name: design-system-patterns
+description: Build scalable design systems with design tokens, theming infrastructure, and component architecture patterns. Use when creating design tokens, implementing theme switching, building component libraries, or establishing design system foundations.
+---
+
+# Design System Patterns
+
+Master design system architecture to create consistent, maintainable, and scalable UI foundations across web and mobile applications.
+
+## When to Use This Skill
+
+- Creating design tokens for colors, typography, spacing, and shadows
+- Implementing light/dark theme switching with CSS custom properties
+- Building multi-brand theming systems
+- Architecting component libraries with consistent APIs
+- Establishing design-to-code workflows with Figma tokens
+- Creating semantic token hierarchies (primitive, semantic, component)
+- Setting up design system documentation and guidelines
+
+## Core Capabilities
+
+### 1. Design Tokens
+- Primitive tokens (raw values: colors, sizes, fonts)
+- Semantic tokens (contextual meaning: text-primary, surface-elevated)
+- Component tokens (specific usage: button-bg, card-border)
+- Token naming conventions and organization
+- Multi-platform token generation (CSS, iOS, Android)
+
+### 2. Theming Infrastructure
+- CSS custom properties architecture
+- Theme context providers in React
+- Dynamic theme switching
+- System preference detection (prefers-color-scheme)
+- Persistent theme storage
+- Reduced motion and high contrast modes
+
+### 3. Component Architecture
+- Compound component patterns
+- Polymorphic components (as prop)
+- Variant and size systems
+- Slot-based composition
+- Headless UI patterns
+- Style props and responsive variants
+
+### 4. Token Pipeline
+- Figma to code synchronization
+- Style Dictionary configuration
+- Token transformation and formatting
+- CI/CD integration for token updates
+
+## Quick Start
+
+```typescript
+// Design tokens with CSS custom properties
+const tokens = {
+ colors: {
+ // Primitive tokens
+ gray: {
+ 50: '#fafafa',
+ 100: '#f5f5f5',
+ 900: '#171717',
+ },
+ blue: {
+ 500: '#3b82f6',
+ 600: '#2563eb',
+ },
+ },
+ // Semantic tokens (reference primitives)
+ semantic: {
+ light: {
+ 'text-primary': 'var(--color-gray-900)',
+ 'text-secondary': 'var(--color-gray-600)',
+ 'surface-default': 'var(--color-white)',
+ 'surface-elevated': 'var(--color-gray-50)',
+ 'border-default': 'var(--color-gray-200)',
+ 'interactive-primary': 'var(--color-blue-500)',
+ },
+ dark: {
+ 'text-primary': 'var(--color-gray-50)',
+ 'text-secondary': 'var(--color-gray-400)',
+ 'surface-default': 'var(--color-gray-900)',
+ 'surface-elevated': 'var(--color-gray-800)',
+ 'border-default': 'var(--color-gray-700)',
+ 'interactive-primary': 'var(--color-blue-400)',
+ },
+ },
+};
+```
+
+## Key Patterns
+
+### Pattern 1: Token Hierarchy
+
+```css
+/* Layer 1: Primitive tokens (raw values) */
+:root {
+ --color-blue-500: #3b82f6;
+ --color-blue-600: #2563eb;
+ --color-gray-50: #fafafa;
+ --color-gray-900: #171717;
+
+ --space-1: 0.25rem;
+ --space-2: 0.5rem;
+ --space-4: 1rem;
+
+ --font-size-sm: 0.875rem;
+ --font-size-base: 1rem;
+ --font-size-lg: 1.125rem;
+
+ --radius-sm: 0.25rem;
+ --radius-md: 0.5rem;
+ --radius-lg: 1rem;
+}
+
+/* Layer 2: Semantic tokens (meaning) */
+:root {
+ --text-primary: var(--color-gray-900);
+ --text-secondary: var(--color-gray-600);
+ --surface-default: white;
+ --interactive-primary: var(--color-blue-500);
+ --interactive-primary-hover: var(--color-blue-600);
+}
+
+/* Layer 3: Component tokens (specific usage) */
+:root {
+ --button-bg: var(--interactive-primary);
+ --button-bg-hover: var(--interactive-primary-hover);
+ --button-text: white;
+ --button-radius: var(--radius-md);
+ --button-padding-x: var(--space-4);
+ --button-padding-y: var(--space-2);
+}
+```
+
+### Pattern 2: Theme Switching with React
+
+```tsx
+import { createContext, useContext, useEffect, useState } from 'react';
+
+type Theme = 'light' | 'dark' | 'system';
+
+interface ThemeContextValue {
+ theme: Theme;
+ resolvedTheme: 'light' | 'dark';
+ setTheme: (theme: Theme) => void;
+}
+
+const ThemeContext = createContext(null);
+
+export function ThemeProvider({ children }: { children: React.ReactNode }) {
+ const [theme, setTheme] = useState(() => {
+ if (typeof window !== 'undefined') {
+ return (localStorage.getItem('theme') as Theme) || 'system';
+ }
+ return 'system';
+ });
+
+ const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light');
+
+ useEffect(() => {
+ const root = document.documentElement;
+
+ const applyTheme = (isDark: boolean) => {
+ root.classList.remove('light', 'dark');
+ root.classList.add(isDark ? 'dark' : 'light');
+ setResolvedTheme(isDark ? 'dark' : 'light');
+ };
+
+ if (theme === 'system') {
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+ applyTheme(mediaQuery.matches);
+
+ const handler = (e: MediaQueryListEvent) => applyTheme(e.matches);
+ mediaQuery.addEventListener('change', handler);
+ return () => mediaQuery.removeEventListener('change', handler);
+ } else {
+ applyTheme(theme === 'dark');
+ }
+ }, [theme]);
+
+ useEffect(() => {
+ localStorage.setItem('theme', theme);
+ }, [theme]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export const useTheme = () => {
+ const context = useContext(ThemeContext);
+ if (!context) throw new Error('useTheme must be used within ThemeProvider');
+ return context;
+};
+```
+
+### Pattern 3: Variant System with CVA
+
+```tsx
+import { cva, type VariantProps } from 'class-variance-authority';
+import { cn } from '@/lib/utils';
+
+const buttonVariants = cva(
+ // Base styles
+ 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50',
+ {
+ variants: {
+ variant: {
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
+ destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
+ outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
+ link: 'text-primary underline-offset-4 hover:underline',
+ },
+ size: {
+ sm: 'h-9 px-3 text-sm',
+ md: 'h-10 px-4 text-sm',
+ lg: 'h-11 px-8 text-base',
+ icon: 'h-10 w-10',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'md',
+ },
+ }
+);
+
+interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean;
+}
+
+export function Button({ className, variant, size, ...props }: ButtonProps) {
+ return (
+
+ );
+}
+```
+
+### Pattern 4: Style Dictionary Configuration
+
+```javascript
+// style-dictionary.config.js
+module.exports = {
+ source: ['tokens/**/*.json'],
+ platforms: {
+ css: {
+ transformGroup: 'css',
+ buildPath: 'dist/css/',
+ files: [{
+ destination: 'variables.css',
+ format: 'css/variables',
+ options: {
+ outputReferences: true, // Preserve token references
+ },
+ }],
+ },
+ scss: {
+ transformGroup: 'scss',
+ buildPath: 'dist/scss/',
+ files: [{
+ destination: '_variables.scss',
+ format: 'scss/variables',
+ }],
+ },
+ ios: {
+ transformGroup: 'ios-swift',
+ buildPath: 'dist/ios/',
+ files: [{
+ destination: 'DesignTokens.swift',
+ format: 'ios-swift/class.swift',
+ className: 'DesignTokens',
+ }],
+ },
+ android: {
+ transformGroup: 'android',
+ buildPath: 'dist/android/',
+ files: [{
+ destination: 'colors.xml',
+ format: 'android/colors',
+ filter: { attributes: { category: 'color' } },
+ }],
+ },
+ },
+};
+```
+
+## Best Practices
+
+1. **Name Tokens by Purpose**: Use semantic names (text-primary) not visual descriptions (dark-gray)
+2. **Maintain Token Hierarchy**: Primitives > Semantic > Component tokens
+3. **Document Token Usage**: Include usage guidelines with token definitions
+4. **Version Tokens**: Treat token changes as API changes with semver
+5. **Test Theme Combinations**: Verify all themes work with all components
+6. **Automate Token Pipeline**: CI/CD for Figma-to-code synchronization
+7. **Provide Migration Paths**: Deprecate tokens gradually with clear alternatives
+
+## Common Issues
+
+- **Token Sprawl**: Too many tokens without clear hierarchy
+- **Inconsistent Naming**: Mixed conventions (camelCase vs kebab-case)
+- **Missing Dark Mode**: Tokens that don't adapt to theme changes
+- **Hardcoded Values**: Using raw values instead of tokens
+- **Circular References**: Tokens referencing each other in loops
+- **Platform Gaps**: Tokens missing for some platforms (web but not mobile)
+
+## Resources
+
+- [Style Dictionary Documentation](https://amzn.github.io/style-dictionary/)
+- [Tokens Studio for Figma](https://tokens.studio/)
+- [Design Tokens W3C Spec](https://design-tokens.github.io/community-group/format/)
+- [Radix UI Themes](https://www.radix-ui.com/themes)
+- [shadcn/ui Theming](https://ui.shadcn.com/docs/theming)
diff --git a/plugins/ui-design/skills/design-system-patterns/references/component-architecture.md b/plugins/ui-design/skills/design-system-patterns/references/component-architecture.md
new file mode 100644
index 0000000..98bd333
--- /dev/null
+++ b/plugins/ui-design/skills/design-system-patterns/references/component-architecture.md
@@ -0,0 +1,603 @@
+# Component Architecture Patterns
+
+## Overview
+
+Well-architected components are reusable, composable, and maintainable. This guide covers patterns for building flexible component APIs that scale across design systems.
+
+## Compound Components
+
+Compound components share implicit state through React context, allowing flexible composition.
+
+```tsx
+// Compound component pattern
+import * as React from 'react';
+
+interface AccordionContextValue {
+ openItems: Set;
+ toggle: (id: string) => void;
+ type: 'single' | 'multiple';
+}
+
+const AccordionContext = React.createContext(null);
+
+function useAccordionContext() {
+ const context = React.useContext(AccordionContext);
+ if (!context) {
+ throw new Error('Accordion components must be used within an Accordion');
+ }
+ return context;
+}
+
+// Root component
+interface AccordionProps {
+ children: React.ReactNode;
+ type?: 'single' | 'multiple';
+ defaultOpen?: string[];
+}
+
+function Accordion({ children, type = 'single', defaultOpen = [] }: AccordionProps) {
+ const [openItems, setOpenItems] = React.useState>(
+ new Set(defaultOpen)
+ );
+
+ const toggle = React.useCallback(
+ (id: string) => {
+ setOpenItems((prev) => {
+ const next = new Set(prev);
+ if (next.has(id)) {
+ next.delete(id);
+ } else {
+ if (type === 'single') {
+ next.clear();
+ }
+ next.add(id);
+ }
+ return next;
+ });
+ },
+ [type]
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
+// Item component
+interface AccordionItemProps {
+ children: React.ReactNode;
+ id: string;
+}
+
+function AccordionItem({ children, id }: AccordionItemProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+// Trigger component
+function AccordionTrigger({ children }: { children: React.ReactNode }) {
+ const { toggle, openItems } = useAccordionContext();
+ const { id } = useAccordionItemContext();
+ const isOpen = openItems.has(id);
+
+ return (
+
+ );
+}
+
+// Content component
+function AccordionContent({ children }: { children: React.ReactNode }) {
+ const { openItems } = useAccordionContext();
+ const { id } = useAccordionItemContext();
+ const isOpen = openItems.has(id);
+
+ if (!isOpen) return null;
+
+ return {children}
;
+}
+
+// Export compound component
+export const AccordionCompound = Object.assign(Accordion, {
+ Item: AccordionItem,
+ Trigger: AccordionTrigger,
+ Content: AccordionContent,
+});
+
+// Usage
+function Example() {
+ return (
+
+
+ Is it accessible?
+
+ Yes. It follows WAI-ARIA patterns.
+
+
+
+ Is it styled?
+
+ Yes. It uses Tailwind CSS.
+
+
+
+ );
+}
+```
+
+## Polymorphic Components
+
+Polymorphic components can render as different HTML elements or other components.
+
+```tsx
+// Polymorphic component with proper TypeScript support
+import * as React from 'react';
+
+type AsProp = {
+ as?: C;
+};
+
+type PropsToOmit = keyof (AsProp & P);
+
+type PolymorphicComponentProp<
+ C extends React.ElementType,
+ Props = {}
+> = React.PropsWithChildren> &
+ Omit, PropsToOmit>;
+
+type PolymorphicRef =
+ React.ComponentPropsWithRef['ref'];
+
+type PolymorphicComponentPropWithRef<
+ C extends React.ElementType,
+ Props = {}
+> = PolymorphicComponentProp & { ref?: PolymorphicRef };
+
+// Button component
+interface ButtonOwnProps {
+ variant?: 'default' | 'outline' | 'ghost';
+ size?: 'sm' | 'md' | 'lg';
+}
+
+type ButtonProps =
+ PolymorphicComponentPropWithRef;
+
+const Button = React.forwardRef(
+ (
+ { as, variant = 'default', size = 'md', className, children, ...props }: ButtonProps,
+ ref?: PolymorphicRef
+ ) => {
+ const Component = as || 'button';
+
+ const variantClasses = {
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
+ outline: 'border border-input bg-background hover:bg-accent',
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
+ };
+
+ const sizeClasses = {
+ sm: 'h-8 px-3 text-sm',
+ md: 'h-10 px-4 text-sm',
+ lg: 'h-12 px-6 text-base',
+ };
+
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+Button.displayName = 'Button';
+
+// Usage
+function Example() {
+ return (
+ <>
+ {/* As button (default) */}
+
+
+ {/* As anchor link */}
+
+
+ {/* As Next.js Link */}
+
+ >
+ );
+}
+```
+
+## Slot Pattern
+
+Slots allow users to replace default elements with custom implementations.
+
+```tsx
+// Slot pattern for customizable components
+import * as React from 'react';
+import { Slot } from '@radix-ui/react-slot';
+
+interface ButtonProps extends React.ButtonHTMLAttributes {
+ asChild?: boolean;
+ variant?: 'default' | 'outline';
+}
+
+const Button = React.forwardRef(
+ ({ asChild = false, variant = 'default', className, ...props }, ref) => {
+ const Comp = asChild ? Slot : 'button';
+
+ return (
+
+ );
+ }
+);
+
+// Usage - Button styles applied to child element
+function Example() {
+ return (
+
+ );
+}
+```
+
+## Headless Components
+
+Headless components provide behavior without styling, enabling complete visual customization.
+
+```tsx
+// Headless toggle hook
+import * as React from 'react';
+
+interface UseToggleProps {
+ defaultPressed?: boolean;
+ pressed?: boolean;
+ onPressedChange?: (pressed: boolean) => void;
+}
+
+function useToggle({
+ defaultPressed = false,
+ pressed: controlledPressed,
+ onPressedChange,
+}: UseToggleProps = {}) {
+ const [uncontrolledPressed, setUncontrolledPressed] =
+ React.useState(defaultPressed);
+
+ const isControlled = controlledPressed !== undefined;
+ const pressed = isControlled ? controlledPressed : uncontrolledPressed;
+
+ const toggle = React.useCallback(() => {
+ if (!isControlled) {
+ setUncontrolledPressed((prev) => !prev);
+ }
+ onPressedChange?.(!pressed);
+ }, [isControlled, pressed, onPressedChange]);
+
+ return {
+ pressed,
+ toggle,
+ buttonProps: {
+ role: 'switch' as const,
+ 'aria-checked': pressed,
+ onClick: toggle,
+ },
+ };
+}
+
+// Headless listbox hook
+interface UseListboxProps {
+ items: T[];
+ defaultSelectedIndex?: number;
+ onSelect?: (item: T, index: number) => void;
+}
+
+function useListbox({
+ items,
+ defaultSelectedIndex = -1,
+ onSelect,
+}: UseListboxProps) {
+ const [selectedIndex, setSelectedIndex] = React.useState(defaultSelectedIndex);
+ const [highlightedIndex, setHighlightedIndex] = React.useState(-1);
+
+ const select = React.useCallback(
+ (index: number) => {
+ setSelectedIndex(index);
+ onSelect?.(items[index], index);
+ },
+ [items, onSelect]
+ );
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ switch (event.key) {
+ case 'ArrowDown':
+ event.preventDefault();
+ setHighlightedIndex((prev) =>
+ prev < items.length - 1 ? prev + 1 : prev
+ );
+ break;
+ case 'ArrowUp':
+ event.preventDefault();
+ setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : prev));
+ break;
+ case 'Enter':
+ case ' ':
+ event.preventDefault();
+ if (highlightedIndex >= 0) {
+ select(highlightedIndex);
+ }
+ break;
+ case 'Home':
+ event.preventDefault();
+ setHighlightedIndex(0);
+ break;
+ case 'End':
+ event.preventDefault();
+ setHighlightedIndex(items.length - 1);
+ break;
+ }
+ },
+ [items.length, highlightedIndex, select]
+ );
+
+ return {
+ selectedIndex,
+ highlightedIndex,
+ select,
+ setHighlightedIndex,
+ listboxProps: {
+ role: 'listbox' as const,
+ tabIndex: 0,
+ onKeyDown: handleKeyDown,
+ },
+ getOptionProps: (index: number) => ({
+ role: 'option' as const,
+ 'aria-selected': index === selectedIndex,
+ onClick: () => select(index),
+ onMouseEnter: () => setHighlightedIndex(index),
+ }),
+ };
+}
+```
+
+## Variant System with CVA
+
+Class Variance Authority (CVA) provides type-safe variant management.
+
+```tsx
+import { cva, type VariantProps } from 'class-variance-authority';
+import { cn } from '@/lib/utils';
+
+// Define variants
+const badgeVariants = cva(
+ // Base classes
+ 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors',
+ {
+ variants: {
+ variant: {
+ default: 'border-transparent bg-primary text-primary-foreground',
+ secondary: 'border-transparent bg-secondary text-secondary-foreground',
+ destructive: 'border-transparent bg-destructive text-destructive-foreground',
+ outline: 'text-foreground',
+ success: 'border-transparent bg-green-500 text-white',
+ warning: 'border-transparent bg-amber-500 text-white',
+ },
+ size: {
+ sm: 'text-xs px-2 py-0.5',
+ md: 'text-sm px-2.5 py-0.5',
+ lg: 'text-sm px-3 py-1',
+ },
+ },
+ compoundVariants: [
+ // Outline variant with sizes
+ {
+ variant: 'outline',
+ size: 'lg',
+ className: 'border-2',
+ },
+ ],
+ defaultVariants: {
+ variant: 'default',
+ size: 'md',
+ },
+ }
+);
+
+// Component with variants
+interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, size, ...props }: BadgeProps) {
+ return (
+
+ );
+}
+
+// Usage
+Active
+Error
+Draft
+```
+
+## Responsive Variants
+
+```tsx
+import { cva } from 'class-variance-authority';
+
+// Responsive variant configuration
+const containerVariants = cva('mx-auto w-full px-4', {
+ variants: {
+ size: {
+ sm: 'max-w-screen-sm',
+ md: 'max-w-screen-md',
+ lg: 'max-w-screen-lg',
+ xl: 'max-w-screen-xl',
+ full: 'max-w-full',
+ },
+ padding: {
+ none: 'px-0',
+ sm: 'px-4 md:px-6',
+ md: 'px-4 md:px-8 lg:px-12',
+ lg: 'px-6 md:px-12 lg:px-20',
+ },
+ },
+ defaultVariants: {
+ size: 'lg',
+ padding: 'md',
+ },
+});
+
+// Responsive prop pattern
+interface ResponsiveValue {
+ base?: T;
+ sm?: T;
+ md?: T;
+ lg?: T;
+ xl?: T;
+}
+
+function getResponsiveClasses(
+ prop: T | ResponsiveValue | undefined,
+ classMap: Record,
+ responsiveClassMap: Record>
+): string {
+ if (!prop) return '';
+
+ if (typeof prop === 'string') {
+ return classMap[prop];
+ }
+
+ return Object.entries(prop)
+ .map(([breakpoint, value]) => {
+ if (breakpoint === 'base') {
+ return classMap[value as T];
+ }
+ return responsiveClassMap[breakpoint]?.[value as T];
+ })
+ .filter(Boolean)
+ .join(' ');
+}
+```
+
+## Composition Patterns
+
+### Render Props
+
+```tsx
+interface DataListProps {
+ items: T[];
+ renderItem: (item: T, index: number) => React.ReactNode;
+ renderEmpty?: () => React.ReactNode;
+ keyExtractor: (item: T) => string;
+}
+
+function DataList({
+ items,
+ renderItem,
+ renderEmpty,
+ keyExtractor,
+}: DataListProps) {
+ if (items.length === 0 && renderEmpty) {
+ return <>{renderEmpty()}>;
+ }
+
+ return (
+
+ {items.map((item, index) => (
+ - {renderItem(item, index)}
+ ))}
+
+ );
+}
+
+// Usage
+