Browser and interaction utilities
Explicit client-only behavior, debouncing, media queries and outside-pointer handling.
Prefer declarative layout
Use CSS breakpoints and container queries for layout. useMediaQuery is for genuinely behavioral decisions and accepts a stable server snapshot. It must not create incompatible server and hydration trees.
ClientOnly is an explicit browser-only boundary. Do not wrap every troublesome component in it and call the system SSR-compatible.
State and lifecycle
useControllableState forbids switching between controlled and uncontrolled operation. useDisclosure is local UI state. useDebouncedValue validates its delay and cancels outdated timers.
ClickAwayListener is a structural wrapper with an explicit outside-pointer policy. It is not a competing dismissal implementation for Base UI menus and dialogs.
Live examples
Try the controls below, then read or copy the complete example to adapt it for your application.
Hydration-safe browser access
Browser-only content pending.
Local interaction helpers
Read the complete example
'use client';
import { useState } from 'react';
import { ClientOnly } from '@fayegram-ai/faye-ui/hooks/client-only';
import { useMediaQuery, useDebouncedValue, useDisclosure } from '@fayegram-ai/faye-ui/hooks';
import { Button } from '@fayegram-ai/faye-ui/controls/button';
import { Input } from '@fayegram-ai/faye-ui/controls/input';
import { Section, Result } from '../example';
export default function Utilities() {
const [query, setQuery] = useState(''), debounced = useDebouncedValue(query, 200), wide = useMediaQuery('(min-width: 60rem)', false), disclosure = useDisclosure();
return <><Section title="Hydration-safe browser access"><ClientOnly fallback={<p>Browser-only content pending.</p>}><p>Client-only content mounted after hydration.</p></ClientOnly><Result>Wide viewport: {String(wide)}</Result></Section><Section title="Local interaction helpers"><Input aria-label="Debounced query" value={query} onChange={e => setQuery(e.target.value)}/><Result>Debounced value: {debounced || 'empty'}</Result><Button aria-expanded={disclosure.open} onClick={disclosure.toggle}>Toggle disclosure state</Button>{disclosure.open && <p>Open state is application-owned.</p>}</Section></>;
}