## [Component Choice Workflow](./component-choice-workflow.md)
- Pick the primitive by count, exclusivity, and screen real estate
- Switch applies immediately, checkbox applies on form submit
- Default to native pickers and bottom sheets on touch surfaces
- Tooltips do not exist on touch, never hide critical info there
- Use skeletons matching final layout for perceived performance wins
- Check 44pt touch targets, keyboard nav, and reduced motion before committing
## [Button](./button.md)
- Extend ButtonHTMLAttributes so disabled, type, and form props pass through
- A discriminated union on href renders as button or Next.js Link
- useFormStatus reads pending state only from a child of form
- focus-visible ring shows focus on keyboard navigation, not mouse clicks
- Disabled links need aria-disabled, tabIndex minus-one, and pointer-events-none simulation
- Always disable buttons while loading to prevent double submissions
## [Modal](./modal.md)
- Native dialog with showModal gives focus trap, Escape, and backdrop
- showModal traps focus, plain show skips backdrop and focus trapping
- Backdrop click does not close by default, check click coordinates
- Body scroll lock via fixed position preserves and restores scroll offset
- requestAnimationFrame triggers entrance animation after the dialog mounts
- Compound ModalHeader, Body, Footer share close via context and createPortal
## [Card](./card.md)
- Static cards omit use client since they need no interactivity
- Polymorphic root uses a discriminated union on href for div or Link
- Compound parts compose freely via React context for hover styling
- Next.js Image needs a sizes hint to avoid oversized mobile downloads
- Add overflow-hidden so image corners respect the card border radius
- Use flex-col with flex-1 body so grid card footers stay aligned
## [Accordion](./accordion.md)
- Single open mode tracks one openId, multiple open uses a Set
- Animate panels by transitioning maxHeight to the measured scrollHeight
- Compound component pattern composes Root, Item, Trigger, and Content via context
- useId generates stable SSR-safe IDs linking triggers to their content
- Wire aria-expanded, aria-controls, and aria-labelledby for screen reader support
- Always use a button, not a div, for keyboard-accessible triggers
## [Tabs](./tabs.md)
- Prefer string IDs over indices so tabs survive reordering without breaking state
- Compound pattern with TabsRoot, TabList, Tab, and TabPanel sharing context
- Roving tabindex keeps only the active tab focusable for cleaner keyboard flow
- Full ARIA roles tablist, tab, tabpanel with aria-selected and aria-controls
- Conditionally rendering panels destroys inner state, hide them to preserve it
- Sync the active tab to URL search params for shareable deep links
## [Toast](./toast.md)
- useEffect cleanup clears the dismiss timer to prevent stale state updates
- Context-driven toast function fires notifications from anywhere without prop drilling
- createPortal renders the container on document.body to avoid stacking context issues
- A useRef flag pauses the countdown on hover without triggering re-renders
- aria-live polite or role=alert ensures screen readers announce new toasts
- Use transition-none so CSS does not fight the JavaScript-driven progress bar
## [Dropdown](./dropdown.md)
- Document mousedown listener detects outside clicks to close the menu
- Portal the menu to body so it escapes overflow-hidden and stacking contexts
- Check both trigger and menu refs when click-outside uses a portal
- Implement WAI-ARIA menu pattern with arrow, Home, End, and Escape keys
- Return focus to the trigger button when the menu closes
- Establish a documented z-index scale to avoid floating element conflicts
## [Tooltip](./tooltip.md)
- Position above the trigger with bottom-full and center using left-1/2 -translate-x-1/2
- Add onFocus and onBlur plus aria-describedby so keyboard and screen reader users get it
- createPortal escapes overflow hidden and stacking context clipping on the trigger
- Compound Tooltip, TooltipTrigger, TooltipContent share open state through context
- Store the show delay timer in a ref to avoid re-renders, clear it on leave
- CSS border tricks build the arrow with no extra SVG or image assets
## [Input](./input.md)
- onChange callback returns the string value so parents skip e.target.value
- Initialize state as empty string to avoid uncontrolled to controlled warnings
- useId links label, aria-describedby, and error or helper text
- Error message takes priority over helper text to prevent stacking
- Use type email, tel, or inputMode numeric for correct mobile keyboards
- pointer-events-none on the left icon passes clicks through to the input
## [Avatar](./avatar.md)
- Extract initials from a name and fall back when no image exists
- Catch the onError event to swap broken image URLs for initials
- Deterministic name hashing assigns each user a stable background color
- Status dots need ring-white borders to stay visible on any background
- AvatarGroup overlaps with negative space and shows a plus-N overflow count
- Use role img with aria-label so initials announce the full name
## [Badge](./badge.md)
- Map color names to static Tailwind class pairs for readable contrast
- Solid, soft, and outline variants nest inside Record color-to-variant maps
- CountBadge returns null at zero and caps overflow at max-plus
- Dismissible badges use asymmetric padding and an aria-label including the text
- Never build dynamic class strings, Tailwind purge drops them silently
- Pair color with dots or icons so colorblind users grasp meaning
## [Skeleton](./skeleton.md)
- Match skeleton dimensions to real content to prevent layout shift
- Shimmer animation needs a custom keyframe registered in tailwind.config.ts
- Overlay mode renders children invisibly for pixel-perfect size matching
- Use role status with sr-only Loading text plus aria-hidden on shapes
- Prefer one parent animate-pulse over dozens of pulsing child elements
- Respect prefers-reduced-motion via motion-safe:animate-pulse or animation none prop
## [Sidebar](./sidebar.md)
- usePathname highlights the active route, startsWith catches nested child routes
- Mobile drawer locks body scroll and closes on Escape or route change
- SidebarProvider context shares collapsed and mobileOpen state without prop drilling
- Dual rendering with lg:flex and lg:hidden avoids JS breakpoint detection
- Collapsed mode hides labels, centers icons, and uses title attribute tooltips
- Label each nav with aria-label so screen readers distinguish multiple navs
## [Breadcrumb](./breadcrumb.md)
- Use an ordered list so screen readers announce position in the sequence
- Mark separators aria-hidden and the last item with aria-current page
- DynamicBreadcrumb builds the trail from usePathname with title-cased segment overrides
- Truncation collapses middle items into an ellipsis or dropdown menu
- Context provider plus useSetBreadcrumbs lets pages declaratively set their trail
- Serialize the items array to JSON to avoid infinite effect loops
## [Pagination](./pagination.md)
- Mark current page with aria-current and disable buttons at boundaries
- generatePages inserts ellipsis using unique start and end keys
- Reset to page 1 when page size or filters change
- Cursor-based Load More with useTransition keeps the UI responsive
- Clamp navigation to valid range and return null for single page
- Sync currentPage and pageSize to URL via useSearchParams
## [Textarea](./textarea.md)
- Auto-resize resets height to auto then sets it to scrollHeight on each change
- Pair programmatic height with resize-none so the drag handle does not fight
- Dual API exposes native onChange plus a simplified onValueChange string callback
- Character counter turns red near maxLength to warn users before the limit
- useActionState wires the textarea to server validation via its name attribute
- Windows form data preserves newlines as carriage-return line-feed, normalize if needed
## [Select](./select.md)
- Native select gives free keyboard nav and mobile pickers for accessibility
- appearance-none plus inline SVG background replaces the default dropdown arrow
- Custom combobox needs role combobox, aria-expanded, and ArrowUp/Down handling
- Searchable mode filters options with useMemo and auto-focuses the input
- Custom button selects skip native form submission, add a hidden input
- Disabled placeholder option cannot be re-selected without a separate clear control
## [Checkbox](./checkbox.md)
- Indeterminate is not an HTML attribute, set it via a ref
- Checked checkboxes send 'on' in FormData, unchecked are absent entirely
- useId links label, description, and error text for screen readers
- Dual onChange and onCheckedChange API suits form libraries and simple state
- Wrap inputs in label or pad them to meet 44px touch targets
- Controlled checkbox needs both checked and onChange to avoid warnings
## [Radio Group](./radio-group.md)
- Shared name attribute enforces mutual exclusivity and native arrow-key navigation
- Wrap options in fieldset and legend so screen readers announce the group
- Radio cannot be unchecked, add a None option to allow clearing
- Hidden sr-only radios need focus styles on the card label
- Compound RadioGroup and RadioGroupItem share value and onChange via context
- Cascading disabled via fieldset disabled plus context, items override individually
## [Switch](./switch.md)
- Built on a button with role=switch and aria-checked, no hidden checkbox needed
- Thumb slides via translate-x while the track transitions color between states
- Always set type=button so the switch does not submit its parent form
- Add a hidden input so the value appears in FormData on native submission
- Explicit Enter and Space handlers ensure consistent keyboard toggling across browsers
- aria-describedby links description text for screen readers in settings layouts
## [Label](./label.md)
- htmlFor must match an input id, or wrap the input
- Hide decorative asterisk with aria-hidden, rely on aria-required for inputs
- Mark optional fields when most are required, less noisy than asterisks
- useId generates SSR-safe ids, avoiding hydration mismatch from Math.random
- Polymorphic as prop renders span to avoid invalid nested labels
- sr-only hides label visually while keeping it accessible to screen readers
## [Slider](./slider.md)
- Native input type=range styled with appearance-none and accent-blue for the thumb
- onChange returns a string, always convert with Number before calculations
- Dual-thumb range uses pointer-events tricks and dynamic z-index to avoid overlap
- Vertical sliders rely on writing-mode and direction rtl, which is non-standard
- Custom track fill is an absolutely positioned div sized by computed percent
- ARIA valuenow, valuetext, plus Home/End keys match the WAI-ARIA slider pattern
## [Toggle Group](./toggle-group.md)
- Use role radiogroup with radio for single select, group with checkbox for multi
- Discriminated union on mode prop types value and onChange correctly per call site
- Roving tabindex with arrow keys moves focus and selection like WAI-ARIA radios
- first:rounded-l-lg and last:rounded-r-lg round only outer corners into a connected pill
- Always add type='button' so buttons inside a form do not submit
- Variant class maps for default, pill, outline keep rendering logic clean
## [Separator](./separator.md)
- Use border-t over a bare hr to avoid the double-line effect
- Vertical dividers need a flex parent with height or they collapse
- Labeled 'or' separator splits two flex-1 lines around centered text
- One component handles both orientations by swapping dimension and border classes
- Interpolated border-${color} classes get purged, use a static class map
- Decorative dividers should use role none, not role separator
## [AspectRatio](./aspect-ratio.md)
- Padding-bottom trick reserves height from width, preventing cumulative layout shift
- Tailwind aspect-video, aspect-square, and inline aspectRatio cover common ratio needs
- Next.js Image in fill mode requires a position relative parent
- The sizes prop drives responsive image downloads, avoiding oversized fetches on mobile
- Preset ratio names like video and portrait map string aliases to numbers
- Native aspect-ratio CSS lacks support in Safari versions before 15
## [ScrollArea](./scroll-area.md)
- Style WebKit scrollbars via Tailwind arbitrary selectors like [&::-webkit-scrollbar]
- Axis prop drives overflow-x and overflow-y plus scrollbar dimensions
- Auto-hide thumb reappears on mouse enter, mimicking macOS overlay scrollbars
- onScrollEnd callback enables infinite scroll without an intersection observer
- Firefox ignores webkit pseudo-elements, needs scrollbar-width and scrollbar-color
- Use scrollbar-gutter stable to stop content shifting under the scrollbar
## [Popover](./popover.md)
- Attach click-outside mousedown listener only while open, clean up on close
- Exclude trigger from outside-click check to prevent instant reopen
- Compound Popover, Trigger, Content, Close communicate through React context
- Trigger needs aria-expanded, aria-haspopup, and aria-controls for screen readers
- Return focus to trigger on Escape or close for keyboard flow
- Use createPortal to escape overflow hidden ancestors clipping the panel
## [Alert](./alert.md)
- Static alerts need role=alert but no use client without interactivity
- Drive info, success, warning, error variants from a typed styles record
- Adaptive aria-live uses assertive for errors and polite elsewhere
- autoClose dismisses after a delay, clearing the timeout on unmount
- Pair color with an icon and text for colorblind accessibility
- forwardRef lets parents measure height or scroll the alert into view
## [Code Group (Multi-Runtime Tabs)](./code-group.md)
- Wrap two or more fenced code blocks in <CodeGroup> to render them as tabs
- labels sets the tab text in block order; a comma-string or array both work
- The reader's language choice is remembered and synced across every group on the site
- sync=false opts a one-off group out of the site-wide language preference
- Pure MDX engine component -- no config, no feature flag, no branding coupling
- Tabs are keyboard accessible with Arrow, Home, and End navigation