Yugen UI

Hooks

Nine shared hooks that Yugen UI's own components use: state, screen, storage, shortcuts and measuring.

The library exports the hooks its own components use, so apps don't copy them or pull in a second hooks library: useControllableState (the x / defaultX / onXChange pattern), useDisclosure, useMediaQuery, useLocalStorage (SSR-safe, synced across tabs), useClipboard, useHotkeys (skips form fields, arrow keys follow reading direction in RTL), useDebouncedValue, useIntersection and useElementSize. All of them are safe to render on the server: without a window they return their starting value and update after hydration.
The parent controls the second button: open

Import

Install from npm, copy the source into your project with @yugen-ui/cli or the shadcn CLI, or import directly when working in this repo.

TSX
// npm install yugen-uiimport {  useControllableState,  useDisclosure,  useMediaQuery,  useLocalStorage,  useClipboard,  useHotkeys,  useDebouncedValue,  useIntersection,  useElementSize,} from "yugen-ui";

Variants

Each tile is a standalone example: hover to copy, or press Code to see the source.

The panel is closed.

useDisclosure

Open / close / toggle for dialogs, menus and panels. The functions have stable identity, so pass them straight to onClick.

Narrow screenSystem: lightFull motion

useMediaQuery

Follows a CSS media query. Resize the window or switch the OS color scheme to see it update at once, without a flash on re-render.

Reload the page or open a second tab: the values stay.

useLocalStorage

Stores JSON in localStorage, synced between hooks with the same key and between tabs. The server and the first hydration render use the initial value, so the HTML never mismatches.

yugen_live_4f9a2c71

useClipboard

Copies text to the clipboard; copied turns itself off after the timeout. A fallback for insecure contexts is built in.

Click this box, then press Ctrl / ⌘ + K, ? or an arrow key. In the input below, plain keys are ignored and Ctrl / ⌘ shortcuts still run.

    useHotkeys

    Keys are key combinations; mod is ⌘ on Mac and Ctrl elsewhere. Plain shortcuts don't fire while typing in a field; start / end flip in RTL.

    Typing: “”

    After 400ms: “”, “API” called 0 times

    useDebouncedValue

    The value only changes once the user stops typing for the delay. Call your API with the debounced value instead of on every key.

    Card is out of view
    Scroll down…
    Tracked card

    useIntersection

    Tells you whether an element is in view. Use once for lazy loading or an effect that should run a single time.

    0 × 0 px

    Compact layout. Drag the bottom-right corner to resize.

    useElementSize

    Measures an element with ResizeObserver, so layout can follow its container instead of the window size.

    Usage

    Do
    • Use useControllableState in every component with an x / defaultX / onXChange pair, so controlled and uncontrolled behave the same.
    • Use useMediaQuery for logic (swapping a component, changing the column count); use Tailwind breakpoints for purely visual changes.
    • Prefix useLocalStorage keys with your app's name, and store only small, non-sensitive data.
    • Register global shortcuts in a few high-level places, and let useHotkeys skip form fields instead of filtering yourself.
    Don't
    • Don't read a useLocalStorage value while rendering to decide the server HTML. The first render is always the initial value.
    • Don't put tokens or personal data in localStorage.
    • Don't use left / right for reading-direction navigation shortcuts. Use start / end so RTL works.
    • Don't debounce a value people need to see respond immediately (the input still binds the raw value).

    Props

    Prop · TypeDefaultDescription
    useControllableState({ value, defaultValue, onChange })[T, (next: SetStateAction<T>) => void]Default: —The setter has stable identity and does nothing when the value is unchanged. value === undefined means uncontrolled.
    useDisclosure({ open, defaultOpen, onOpenChange }){ open, setOpen, onOpen, onClose, onToggle }Default: defaultOpen: falseOpen / closed state; also works in controlled mode.
    useMediaQuery(query, { defaultValue })booleanDefault: falseThe value when there is no window (server, tests). Built on useSyncExternalStore, so it doesn't flash.
    useLocalStorage(key, initialValue, { serialize, deserialize })[T, set, remove]Default: —set takes a value or an updater; remove() deletes the key and returns to the initial value. Corrupt data or blocked localStorage never throws. Values are stored as JSON; serialize / deserialize read an existing key in another format.
    useClipboard({ timeout }){ copy(text): Promise<boolean>, copied, reset }Default: 1600timeout (ms) is how long copied stays true.
    useHotkeys(hotkeys, options)voidDefault: —options: target (document by default), enabled (true), enableOnFormTags (false), preventDefault (true), repeat (false: a held key counts once). Valid keys: mod / ctrl / meta / alt / shift + a key, plus the aliases esc, space, up, down, left, right, start, end.
    useDebouncedValue(value, delay)TDefault: 300Returns the value at once on the first render; afterwards it only changes once value has been still for delay ms.
    useIntersection({ root, rootMargin, threshold, once }){ ref, isIntersecting, entry }Default: —Attach ref to the element. Without IntersectionObserver it is always false.
    useElementSize({ box, enabled }){ ref, width, height }Default: box: "content-box", enabled: true0 × 0 until measured (server, first render). The first measure runs before the browser paints, so nothing flashes. Also works with vertical writing modes. Pass enabled: false when the size is already known (a fixed width, for example): no measuring, no ResizeObserver, no extra render, and the result stays 0 × 0.