원클릭으로
wire-ui
AI-native unstyled primitives framework. Headless, compound components with zero CSS.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
AI-native unstyled primitives framework. Headless, compound components with zero CSS.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
| name | wire-ui |
| description | AI-native unstyled primitives framework. Headless, compound components with zero CSS. |
| version | 0.5.0 |
| tags | ["react","headless","unstyled","compound-components","data-attributes","ai-native"] |
AI-native unstyled primitives framework. Headless, compound components with zero CSS — style via data-* attributes.
This skill covers building UIs with @wire-ui/react. It helps you:
data-* attribute selectorsnpm install @wire-ui/react
Peer dependencies: react >= 19.0.0, react-dom >= 19.0.0.
Wire UI ships no CSS. All interactive states are exposed as data attributes. Style them with any CSS approach.
Attributes available on interactive components:
| Attribute | When present |
|---|---|
data-hover | Mouse is over the element |
data-focus-visible | Keyboard focus (mirrors :focus-visible) |
data-active | Element is being pressed |
data-disabled | Element is disabled |
data-state | "open" / "closed", "checked" / "unchecked" — varies per component |
data-invalid | Consumer-controlled via invalidType |
data-success | Consumer-controlled via isSuccess |
Styling with Tailwind:
<Button className="[data-hover]:bg-blue-700 [data-active]:scale-95 [data-disabled]:opacity-50">
Styling with plain CSS:
button[data-hover] { background: #1d4ed8; }
button[data-active] { transform: scale(0.95); }
button[data-disabled] { opacity: 0.5; }
Complex components use the Component.Part pattern. You compose the parts and control markup order.
<Input.Root value={email} onChange={setEmail} invalidType={error}>
<Input.Label>Email</Input.Label>
<Input.Field type="email" />
<Input.Error />
</Input.Root>
Pass asChild to merge all behaviour onto a child element. The component renders the child instead of its default element.
<Button asChild>
<a href="/dashboard">Go to dashboard</a>
</Button>
Decision rule: Use asChild when you need a Wire UI component to render as a different HTML element (e.g., <a>, <Link>, custom component).
Form components never validate internally. You set invalidType when your logic decides something is invalid.
<Input.Root
invalidType={error}
errorMessage={{ required: 'Required', email: 'Invalid email' }}
>
<Input.Field type="email" />
<Input.Error />
</Input.Root>
Decision rule: Always set invalidType from your own validation logic (onBlur, onSubmit, etc.). Never expect the component to validate for you.
import { Button } from '@wire-ui/react'
<Button>Click me</Button>
<Button disabled>Disabled</Button>
<Button asChild><a href="/link">As link</a></Button>
Props: All <button> HTML attributes + asChild, autoFocus, disabled.
Data attributes: data-hover, data-focus-visible, data-active, data-disabled, data-autofocus.
import { Input } from '@wire-ui/react'
<Input.Root value={value} onChange={setValue} invalidType={error} errorMessage={{ required: 'Required' }}>
<Input.Label>Email</Input.Label>
<Input.Field type="email" placeholder="you@example.com" />
<Input.Error />
</Input.Root>
Parts: Root, Label, Field, Error.
Root props: value, defaultValue, onChange, onFocus, onBlur, invalidType, errorMessage, isRequired, isSuccess, id.
Field data attributes: data-active, data-invalid, data-success.
Same compound API as Input but renders <textarea>.
import { Textarea } from '@wire-ui/react'
<Textarea.Root>
<Textarea.Label>Message</Textarea.Label>
<Textarea.Field rows={4} placeholder="Write something..." />
<Textarea.Error />
</Textarea.Root>
import { Password } from '@wire-ui/react'
<Password.Root>
<Password.Label>Password</Password.Label>
<Password.Field placeholder="••••••••" />
<Password.Toggle />
<Password.Error />
</Password.Root>
Toggle data attribute: data-visible (password is shown).
import { Checkbox } from '@wire-ui/react'
<Checkbox.Root value={selected} onChange={setSelected}>
<Checkbox.Item value="a">
<Checkbox.Indicator>✓</Checkbox.Indicator>
<Checkbox.Label>Option A</Checkbox.Label>
</Checkbox.Item>
</Checkbox.Root>
Root props: value (array), defaultValue, onChange, disabled.
Item data attribute: data-checked.
import { Radio } from '@wire-ui/react'
<Radio.Root value={selected} onChange={setSelected} name="options">
<Radio.Item value="a">
<Radio.Indicator />
<Radio.Label>Option A</Radio.Label>
</Radio.Item>
</Radio.Root>
Root props: value, defaultValue, onChange, name, disabled.
Item data attribute: data-checked.
import { Switch } from '@wire-ui/react'
<Switch.Root checked={on} onCheckedChange={setOn}>
<Switch.Thumb />
</Switch.Root>
Data attributes: data-state ("checked" / "unchecked"), data-disabled.
import { Select } from '@wire-ui/react'
<Select.Root value={value} onChange={setValue}>
<Select.Trigger>
<Select.Value placeholder="Pick one" />
</Select.Trigger>
<Select.Content>
<Select.Item value="a">Option A</Select.Item>
<Select.Item value="b">Option B</Select.Item>
</Select.Content>
</Select.Root>
Supports: Select.Group, Select.GroupLabel, Select.Separator.
Trigger data attribute: data-state ("open" / "closed").
Item data attributes: data-selected, data-hover, data-disabled.
import { Search } from '@wire-ui/react'
<Search.Root onSearchChange={setQuery} onSelect={(opt) => console.log(opt)}>
<Search.Input placeholder="Search..." />
<Search.Content>
{results.map(r => <Search.Item key={r.id} option={r}>{r.title}</Search.Item>)}
<Search.Empty>No results</Search.Empty>
</Search.Content>
</Search.Root>
Root props: onSearchChange, onSelect, loading, searchDelay.
Item data attribute: data-highlighted.
Keyboard: Arrow keys navigate, Enter selects, Escape closes.
import { OTP } from '@wire-ui/react'
<OTP.Root length={6} onComplete={(code) => verify(code)}>
<OTP.Slot index={0} />
<OTP.Slot index={1} />
<OTP.Slot index={2} />
<OTP.Separator />
<OTP.Slot index={3} />
<OTP.Slot index={4} />
<OTP.Slot index={5} />
</OTP.Root>
Root props: length, value, onChange, onComplete, disabled, alphanumeric.
Root data attribute: data-complete.
Slot data attributes: data-active, data-filled.
import { Modal } from '@wire-ui/react'
<Modal.Root open={open} onOpenChange={setOpen}>
<Modal.Portal>
<Modal.Overlay>
<Modal.Content>
<h2>Title</h2>
<p>Body</p>
<Modal.Close>Close</Modal.Close>
</Modal.Content>
</Modal.Overlay>
</Modal.Portal>
</Modal.Root>
Parts: Root, Portal, Overlay, Content, Close.
Root props: open, defaultOpen, onOpenChange.
Data attribute: data-state ("open" / "closed") on Overlay and Content.
Closes on: Escape key, overlay click.
Same compound API as Modal. Renders as a side panel.
import { Drawer } from '@wire-ui/react'
<Drawer.Root open={open} onOpenChange={setOpen}>
<Drawer.Portal>
<Drawer.Overlay>
<Drawer.Content>
<Drawer.Header>
<h2>Title</h2>
<Drawer.Close>✕</Drawer.Close>
</Drawer.Header>
<p>Body</p>
</Drawer.Content>
</Drawer.Overlay>
</Drawer.Portal>
</Drawer.Root>
import { Dropdown } from '@wire-ui/react'
<Dropdown.Root>
<Dropdown.Trigger>Options</Dropdown.Trigger>
<Dropdown.Menu>
<button>Edit</button>
<button>Delete</button>
</Dropdown.Menu>
</Dropdown.Root>
Menu data attribute: data-state ("open" / "closed").
Hide closed menu: className="data-[state=closed]:hidden".
import { Tooltip } from '@wire-ui/react'
<Tooltip.Root delayDuration={300}>
<Tooltip.Trigger><button>Hover me</button></Tooltip.Trigger>
<Tooltip.Content side="top">Tooltip text</Tooltip.Content>
</Tooltip.Root>
Content props: side ("top" / "bottom" / "left" / "right").
Content data attributes: data-state, data-side.
import { Accordion } from '@wire-ui/react'
<Accordion.Root type="single" collapsible defaultValue="item-1">
<Accordion.Item value="item-1">
<Accordion.Trigger>Section 1</Accordion.Trigger>
<Accordion.Content>Content</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
Root types: type="single" (one open) or type="multiple" (many open).
Data attribute: data-state ("open" / "closed") on Item, Trigger, Content.
import { Rating } from '@wire-ui/react'
<Rating defaultValue={3} onChange={(v) => console.log(v)} />
<Rating value={4} readOnly />
<Rating value={2} disabled />
Props: value, defaultValue, onChange, max, readOnly, disabled, starClassName.
Star data attributes: data-filled, data-highlighted.
import { ProgressBar } from '@wire-ui/react'
<ProgressBar percentage={65} />
Props: value, min, max.
Renders role="progressbar" with aria-valuenow, aria-valuemin, aria-valuemax.
import { Alert } from '@wire-ui/react'
<Alert.Root status="warning" onDismiss={() => setVisible(false)}>
<Alert.Title>Warning</Alert.Title>
<Alert.Description>Check your input.</Alert.Description>
<Alert.Dismiss>✕</Alert.Dismiss>
</Alert.Root>
Root props: onDismiss, isAutoDismissable, dismissDuration.
import { Avatar } from '@wire-ui/react'
<Avatar.Root>
<Avatar.Image src="/avatar.jpg" alt="User" />
<Avatar.Fallback>JD</Avatar.Fallback>
</Avatar.Root>
Image data attribute: data-status ("loading" / "loaded" / "error").
Fallback renders when data-status is "error" or src is empty.
import { Badge } from '@wire-ui/react'
<Badge count={3} /> // renders "3"
<Badge count={12} /> // renders "9+"
<Badge count={0} /> // renders nothing
Simple container. Style with data-color and data-size attributes.
import { Card } from '@wire-ui/react'
<Card color="primary" size="large">Content</Card>
import { Divider } from '@wire-ui/react'
<Divider />
<Divider orientation="vertical" />
<Divider decorative={false} aria-label="Section break" />
import { List } from '@wire-ui/react'
<List><li>Item 1</li><li>Item 2</li></List>
<List isOrdered><li>First</li><li>Second</li></List>
import { Image } from '@wire-ui/react'
<Image src="/photo.jpg" alt="Photo" position="center" onImageLoaded={() => {}} />
Data attributes: data-position on wrapper, data-loaded on <img> after load.
import { Timeago } from '@wire-ui/react'
<Timeago datetime={new Date()} isDuration />
<Timeago datetime="2025-06-15T09:00:00" />
<Timeago datetime={new Date()} timeOnly />
Props: datetime, isDuration, timeOnly.
Renders consumer-supplied SVG strings by name. Ships no SVG assets.
import { Icon } from '@wire-ui/react'
<Icon name="check" icons={{ check: '<svg>...</svg>' }} size="medium" />
import { Carousel } from '@wire-ui/react'
<Carousel.Root>
<Carousel.Viewport>
<Carousel.Content>
<Carousel.Slide>Slide 1</Carousel.Slide>
<Carousel.Slide>Slide 2</Carousel.Slide>
</Carousel.Content>
</Carousel.Viewport>
<Carousel.Previous>‹</Carousel.Previous>
<Carousel.Next>›</Carousel.Next>
<Carousel.Indicators>
{({ index, selected, scrollTo }) => <button key={index} onClick={scrollTo} aria-current={selected} />}
</Carousel.Indicators>
</Carousel.Root>
Root props: orientation ("horizontal" / "vertical"), loop, index, defaultIndex, onIndexChange.
Data attributes: data-orientation on Root.
import { InfiniteScroll } from '@wire-ui/react'
<InfiniteScroll.Root onLoadMore={loadMore} hasMore={hasMore} loading={loading}>
<ul>{items.map((i) => <li key={i}>{i}</li>)}</ul>
<InfiniteScroll.Loader>Loading…</InfiniteScroll.Loader>
<InfiniteScroll.EndMessage>No more</InfiniteScroll.EndMessage>
<InfiniteScroll.Sentinel />
</InfiniteScroll.Root>
Root props: onLoadMore, hasMore, loading, disabled, rootMargin. Built on useIntersectionObserver.
Data attributes: data-loading, data-has-more on Root.
import { ScrollArea } from '@wire-ui/react'
<ScrollArea.Root>
<ScrollArea.Viewport>{/* content */}</ScrollArea.Viewport>
<ScrollArea.Scrollbar orientation="vertical"><ScrollArea.Thumb /></ScrollArea.Scrollbar>
</ScrollArea.Root>
Scrollbar props: orientation ("vertical" / "horizontal"), forceMount.
import { Virtualizer } from '@wire-ui/react'
<Virtualizer.Root count={1000} estimateSize={44}>
{({ index }) => <div>Row {index}</div>}
</Virtualizer.Root>
Root props: count, estimateSize, overscan, orientation, getItemKey. Render-prop children receive a VirtualItem ({ index, start, size }).
import { EmptyState } from '@wire-ui/react'
<EmptyState.Root>
<EmptyState.Media>🗂️</EmptyState.Media>
<EmptyState.Title>No projects yet</EmptyState.Title>
<EmptyState.Description>Create your first project.</EmptyState.Description>
<EmptyState.Actions><button>Create</button></EmptyState.Actions>
</EmptyState.Root>
Parts: Root (role="status"), Media (aria-hidden), Title, Description, Actions.
import { Stat } from '@wire-ui/react'
<Stat.Root>
<Stat.Label>Revenue</Stat.Label>
<Stat.Value>$48,250</Stat.Value>
<Stat.Delta value={12.5}>▲ 12.5%</Stat.Delta>
<Stat.HelpText>vs last month</Stat.HelpText>
<Stat.Sparkline data={[12, 18, 15, 22, 28]} />
</Stat.Root>
Stat.Delta infers direction from the sign of value (data-direction). Stat.Sparkline props: data, width, height, strokeWidth.
import { ColorPicker } from '@wire-ui/react'
<ColorPicker.Root value={color} onChange={setColor}>
<ColorPicker.Area><ColorPicker.AreaThumb /></ColorPicker.Area>
<ColorPicker.Hue><ColorPicker.HueThumb /></ColorPicker.Hue>
<ColorPicker.Alpha><ColorPicker.AlphaThumb /></ColorPicker.Alpha>
<ColorPicker.Swatch />
<ColorPicker.Input />
</ColorPicker.Root>
Root props: value / defaultValue (hex string), onChange, alpha.
import { Editable } from '@wire-ui/react'
<Editable.Root defaultValue="Click to edit" submitOnBlur>
<Editable.Preview />
<Editable.Input />
</Editable.Root>
Root props: value / defaultValue, editing, submitOnBlur, onSubmit, onCancel, onEdit. Parts also include Area, EditTrigger, SubmitTrigger, CancelTrigger.
import { RichText } from '@wire-ui/react'
<RichText.Root defaultMode="split" parse={parse}>
<RichText.Toolbar>
<RichText.Action wrap="**">B</RichText.Action>
<RichText.Action insert={'\n- '}>List</RichText.Action>
</RichText.Toolbar>
<RichText.Editor />
<RichText.Preview />
</RichText.Root>
Root props: value, defaultMode ("edit" / "preview" / "split"), parse, components. RichText.Action takes wrap (string | [string, string]) or insert. Built on Markdown.
import { Toggle, ToggleGroup } from '@wire-ui/react'
<Toggle defaultPressed aria-label="Bold"><b>B</b></Toggle>
<ToggleGroup.Root type="multiple" defaultValue={['bold']}>
<Toggle value="bold"><b>B</b></Toggle>
<Toggle value="italic"><i>I</i></Toggle>
</ToggleGroup.Root>
Toggle props: pressed / defaultPressed / onPressedChange (aria-pressed, data-state "on" / "off"). ToggleGroup.Root props: type ("single" / "multiple"), value / defaultValue / onValueChange, loop, orientation.
import { Stepper } from '@wire-ui/react'
<Stepper.Root count={3} defaultValue={0} linear>
<Stepper.List>
<Stepper.Item index={0}><Stepper.Trigger>Account</Stepper.Trigger><Stepper.Separator /></Stepper.Item>
<Stepper.Item index={1}><Stepper.Trigger>Review</Stepper.Trigger></Stepper.Item>
</Stepper.List>
<Stepper.Content index={0}>…</Stepper.Content>
<Stepper.PrevTrigger>Back</Stepper.PrevTrigger>
<Stepper.NextTrigger>Next</Stepper.NextTrigger>
</Stepper.Root>
Root props: count, value / defaultValue / onValueChange (0-based), orientation, linear (blocks forward jumps). Prev/Next triggers auto-disable at boundaries.
import { Toolbar } from '@wire-ui/react'
<Toolbar.Root aria-label="Format">
<Toolbar.Toggle aria-label="Bold"><b>B</b></Toolbar.Toggle>
<Toolbar.Separator />
<Toolbar.Button>Align</Toolbar.Button>
<Toolbar.Link href="#">Help</Toolbar.Link>
</Toolbar.Root>
role="toolbar" with roving focus + arrow-key navigation. Root props: orientation, loop. Toolbar.Toggle: pressed / onPressedChange (data-state).
import { Command } from '@wire-ui/react'
<Command.Root shortcut="mod+k">
<Command.Input placeholder="Search…" />
<Command.List>
<Command.Empty>No results.</Command.Empty>
<Command.Group heading="Actions">
<Command.Item value="New File" keywords={['create']} onSelect={create}>New File</Command.Item>
</Command.Group>
</Command.List>
</Command.Root>
Root props: filter, loop, shortcut (e.g. mod+k), searchValue / onSearchChange, open / onOpenChange. Command.Item: value, keywords, onSelect, disabled (data-highlighted on the active item).
import { HoverCard } from '@wire-ui/react'
<HoverCard.Root openDelay={200}>
<HoverCard.Trigger>@wire-ui</HoverCard.Trigger>
<HoverCard.Content side="bottom">Profile preview…</HoverCard.Content>
</HoverCard.Root>
Root props: openDelay, closeDelay, open / defaultOpen / onOpenChange. HoverCard.Content: side, sideOffset, forceMount. (Hover/focus intent — for click menus use Dropdown, for tooltips use Tooltip.)
import { Sheet } from '@wire-ui/react'
<Sheet.Root side="bottom" snapPoints={[0.4]} modal>
<Sheet.Trigger>Open</Sheet.Trigger>
<Sheet.Portal>
<Sheet.Overlay />
<Sheet.Content>
<Sheet.Handle />
<Sheet.Title>Bottom sheet</Sheet.Title>
<Sheet.Close>Done</Sheet.Close>
</Sheet.Content>
</Sheet.Portal>
</Sheet.Root>
Root props: side ("top" / "bottom"), snapPoints (fraction or px), modal, open / defaultOpen / onOpenChange, activeSnapPoint / onActiveSnapPointChange. Drag-to-dismiss via Sheet.Handle.
import { Chat } from '@wire-ui/react'
<Chat.Root onSubmit={send} isStreaming={streaming}>
<Chat.List count={messages.length}>
{({ index }) => <Chat.Message role={messages[index].role}>{messages[index].text}</Chat.Message>}
</Chat.List>
<Chat.Composer>
<Chat.Input placeholder="Send a message…" />
<Chat.Send>Send</Chat.Send>
</Chat.Composer>
</Chat.Root>
Root props: value / defaultValue / onValueChange, onSubmit, isStreaming (data-streaming). Chat.List: count, stickToBottom, overscan (virtualized). Chat.Message: role, streaming (data-role, data-streaming). Chat.Input: submitOnEnter, autoResize.
import { Citation } from '@wire-ui/react'
<Citation.Root sources={sources}>
<p>HTTP defines status codes<Citation.Ref for="rfc" />.</p>
<Citation.List />
</Citation.Root>
Root props: sources (array of { id, title, url }). Citation.Ref takes for (source id), renders role="doc-noteref"; Citation.List renders footnotes (role="doc-endnote"). Auto-numbered with data-index on both.
import { CodeBlock } from '@wire-ui/react'
<CodeBlock.Root code={src} language="js" startLine={1} highlightLines={[2]}>
<CodeBlock.CopyButton>{({ copied }) => (copied ? 'Copied!' : 'Copy')}</CodeBlock.CopyButton>
<CodeBlock.Code>
<CodeBlock.Lines>{({ line }) => <span>{line.number}: {line.content}</span>}</CodeBlock.Lines>
</CodeBlock.Code>
</CodeBlock.Root>
Root props: code, language, startLine, highlightLines, copyResetAfter. CodeBlock.Lines render-prop carries { number, content, diff, highlighted }; CodeBlock.CopyButton carries { copied }. data-language on Root and <pre>.
import { Diff } from '@wire-ui/react'
<Diff.Root oldValue={before} newValue={after}>
<Diff.Stats>{({ additions, deletions }) => <span>+{additions} −{deletions}</span>}</Diff.Stats>
<Diff.Unified>{({ line }) => <div>{line.content}</div>}</Diff.Unified>
</Diff.Root>
Root props: oldValue, newValue (LCS-based). Diff.Unified / Diff.Split render lines with type (equal / insert / delete) and 1-based oldLine / newLine. Diff.Stats carries { additions, deletions }.
import { Markdown } from '@wire-ui/react'
<Markdown content={text} parse={parse} components={{ a: MyLink }} />
// or pre-parsed:
<Markdown nodes={nodes} />
Parser-agnostic. Props: nodes (pre-parsed mdast tree) or content + parse, plus a components override map keyed by node type. Default renderers emit semantic HTML.
import { Mention } from '@wire-ui/react'
<Mention.Root options={people}>
<Mention.Input placeholder="Type @ to mention…" />
<Mention.Content>
<Mention.Items>{({ option }) => <div>{option.label}</div>}</Mention.Items>
<Mention.Empty>No matches</Mention.Empty>
</Mention.Content>
</Mention.Root>
Root props: options, trigger (default @), filter, appendSpace, onSelect, value / defaultValue. Mention.Input is a textarea; Mention.Items render-prop carries { option, active, index }.
import { Typewriter } from '@wire-ui/react'
<Typewriter.Root text="Composing a reply…" speed={45}>
<Typewriter.Text />
<Typewriter.Cursor>▋</Typewriter.Cursor>
</Typewriter.Root>
Root props: text (streaming-friendly — appends as it grows), granularity ("char" / "word"), speed, startDelay, autoStart, loop / loopDelay, onComplete. Render-prop state: { displayed, isTyping, isDone, progress }. Respects prefers-reduced-motion.
@wire-ui/react exports 33 hooks. Vue exposes the same APIs as useX composables; Solid exposes them as createX primitives. The list below uses React names.
Unified controlled/uncontrolled state. Returns [value, setValue].
const [value, setValue] = useControllableState({
value: props.value,
defaultValue: props.defaultValue ?? '',
onChange: props.onChange,
})
Boolean open/close with stable handlers.
const { isOpen, open, close, toggle } = useDisclosure({ defaultOpen: false })
SSR-safe unique id with an optional prefix.
const id = useId('field')
Returns the value from the previous render (undefined on first render).
const previous = usePrevious(count)
Typed finite state machine. Invalid events are silent no-ops.
const { state, send, can } = useStateMachine(
{ idle: { fetch: 'loading' }, loading: { resolve: 'idle' } },
{ initial: 'idle' },
)
Bounded undo/redo container. set clears the redo stack.
const { value, set, undo, redo, canUndo, canRedo } = useUndoRedo('', { limit: 100 })
handlers + dataAttributes for building custom interactive elements.
const { handlers, dataAttributes } = useInteractiveState({ disabled })
return <div {...handlers} {...dataAttributes}>Custom element</div>
Fires a callback on outside click/tap.
const ref = useRef(null)
useClickOutside(ref, () => setOpen(false))
Trap keyboard focus inside a container while active.
const ref = useRef(null)
useFocusTrap(ref, { isActive: isOpen })
Track whether focus arrived from the keyboard or a pointer.
Map keys to handlers with modifier matching.
useKeyboard({ Escape: onClose, s: [onSave, { meta: true, preventDefault: true }] })
Declarative hotkey binder. mod+k is ⌘K on macOS, Ctrl+K elsewhere. Suppressed inside inputs by default; supports scopes.
useHotkeys({
'mod+k': (e) => { e.preventDefault(); openPalette() },
'escape': closePalette,
})
Long-press gesture; spread the returned handlers onto an element.
const handlers = useLongPress((e) => console.log('held'), { threshold: 500 })
return <div {...handlers}>Press and hold</div>
Async clipboard write with a copied flag that auto-resets after resetAfter ms.
const { copy, copied } = useCopyToClipboard({ resetAfter: 2000 })
return <button onClick={() => copy(text)}>{copied ? 'Copied!' : 'Copy'}</button>
Position a floating element relative to a reference (used by Tooltip, Popover, Dropdown, etc.).
Lock document scrolling without layout shift.
Observe element visibility within a scroll container.
Observe an element's content-box { width, height }.
Observe DOM mutations on a ref target.
useMutationObserver(ref, (mutations) => { /* … */ }, { attributes: true })
Live content-box { width, height } of a referenced element. Thin wrapper over useResizeObserver.
Live { width, height } of the viewport. SSR-safe.
Managed setTimeout with start / stop / reset and isPending. Latest callback is always invoked.
const { isPending, start, stop, reset } = useTimeout(onDismiss, 3000, { autoStart: true })
Managed setInterval with pause/resume. Pass delay = null to pause without unmounting.
const { isRunning, start, stop } = useInterval(tick, 1000, { autoStart: true })
Debounce a value or a callback — only fires after delay ms of inactivity.
const debouncedQuery = useDebounce(query, 250)
const saveDraft = useDebouncedCallback((v) => fetch('/draft', { body: v }), 500)
Throttle a value or a callback — fires at most once per delay window.
useState-shaped hook backed by Web Storage. SSR-safe. useLocalStorage syncs across tabs by default; useSessionStorage does not.
const [theme, setTheme, removeTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light')
Typed addEventListener for window / document / element / ref. Tolerates null targets.
useEventListener('resize', () => console.log(window.innerWidth))
useEventListener('click', onClick, ref)
Reactive 'visible' | 'hidden'. SSR-safe (defaults to 'visible').
Reactive navigator.onLine boolean. SSR-safe (defaults to true).
Reactively match a CSS media query.
const isMobile = useMediaQuery('(max-width: 640px)')
Detect the OS-level reduced-motion preference.
useLayoutEffect on the client, useEffect on the server — silences SSR warnings while keeping synchronous DOM measurements on the client.
Merge multiple refs onto a single element. Accepts ref objects, callback refs, or null (skipped).
const mergedRef = useMergedRefs(localRef, forwardedRef)
InputTextareaPasswordOTPSelectSearchCheckboxRadioSwitchToggle / ToggleGroupRatingColorPickerEditableRichTextCommandModalDrawerSheetDropdownHoverCard (use Tooltip for plain text labels)TooltipChatMarkdown (editable? → RichText)CodeBlockDiffCitation@-mention autocomplete? → MentionTypewriterclassName="[data-hover]:bg-blue-700"[data-hover] { background: #1d4ed8; }button[data-hover] { background: #1d4ed8; }useDisclosureuseControllableStatehtmlFor / aria-labelledby)? → useIdusePreviousidle → loading → success | error)? → useStateMachineuseUndoRedouseHotkeys (not useKeyboard — that's for element-scoped key handlers)useClickOutsideuseFocusTrapuseCopyToClipboarduseLongPressuseFloatinguseScrollLockuseElementSize / useWindowSizeuseIntersectionObserveruseMutationObserversetTimeout with pause/resume? → useTimeoutuseIntervaluseDebounce / useThrottleuseLocalStorage (use useSessionStorage for per-tab)document.visibilityState? → useDocumentVisibilitynavigator.onLine? → useOnlineStatusaddEventListener (window/document/element/ref)? → useEventListeneruseMediaQueryprefers-reduced-motion? → useReduceMotionuseMergedRefsuseLayoutEffect semantics without an SSR warning? → useIsomorphicLayoutEffectdata-focus — Wire UI uses data-focus-visible (keyboard only, not mouse clicks).invalidType inside the component — validation is always consumer-controlled.data-state="true" or data-state="false" — attributes use empty string when present and are absent when not (except data-state which uses named values like "open" / "closed").type="submit" to Button expecting form submission by default — Button defaults to type="button" to prevent accidental form submission.Modal.Portal — Modal content must be wrapped in Portal for correct rendering.data-open / data-closed — use data-state="open" / data-state="closed" instead.