A collection of 82+ zero-dependency hooks for SolidJS forked directly from Mantine Hooks, with some improvements.
import { useClickOutside } from 'bagon-hooks'; import { createSignal, Show } from 'solid-js'; export function UseClickOutsideExample() { const ref = useClickOutside(() => { setClicked(true); setTimeout(() => { setClicked(false); }, 500); }); const [clicked, setClicked] = createSignal(false); return ( <div class="flex h-full w-full flex-1 items-center justify-center rounded-md border p-3 py-10"> <div class={`select-none rounded-full border bg-neutral-50 p-2 text-xs transition ${clicked() ? 'scale-95' : ''}`} ref={ref} > <Show when={clicked()} fallback={'No detections'}> You clicked outside! </Show> </div> </div> ); }
import { useClipboard } from 'bagon-hooks'; import { Show } from 'solid-js'; export function UseClipboardExample() { const { copied, copy, reset } = useClipboard(); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm"> <span class="text-center">Bagon is awesome!</span> <button class="transition active:scale-90" onClick={() => copy('Bagon is awesome!')}> <Show when={copied()} fallback={ <div class="flex items-center gap-x-1"> <IconCopy class="h-8 w-8" /> Copy </div> } children={ <div class="flex items-center gap-x-1 text-green-500"> <IconCheck class="h-8 w-8" /> Copied! </div> } /> </button> </div> ); }
Collapsible content stays in the DOM while height animates.
State: exited
import { createSignal } from 'solid-js'; import { useCollapse } from 'bagon-hooks'; export function UseCollapseExample() { const [expanded, setExpanded] = createSignal(false); const { getCollapseProps, state } = useCollapse({ expanded }); const props = getCollapseProps(); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center"> <button class="rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95" type="button" onClick={() => setExpanded(value => !value)} > {expanded() ? 'Collapse' : 'Expand'} </button> <div ref={props.ref} style={props.style()} aria-hidden={props['aria-hidden']()} onTransitionEnd={props.onTransitionEnd} class="w-full max-w-md overflow-hidden rounded-md border text-left" > <div class="space-y-2 p-3 text-sm text-neutral-600"> <p>Collapsible content stays in the DOM while height animates.</p> <p>State: {state()}</p> </div> </div> </div> ); }
import { useColorScheme } from 'bagon-hooks'; export function UseColorSchemeExample() { const colorScheme = useColorScheme(); return ( <div class="flex h-full w-full items-center justify-center gap-x-1 rounded-md border p-3 py-10 text-center text-sm"> <div class="rounded-md border px-4 py-2 text-sm" style={{ background: colorScheme() === 'dark' ? '#000' : '#fff', color: colorScheme() ? '#fff' : '#000', }} > Your system color scheme is: {colorScheme()} </div> </div> ); }
import { useCounter } from 'bagon-hooks'; export function UseCounterExample() { const [count, { decrement, increment, reset, set }] = useCounter(5, { min: 1, max: 10 }); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-5 gap-x-1 rounded-md border p-3 py-10 text-center"> <span>{count()}</span> <div class="flex gap-x-2 text-sm"> <button class="rounded-md border p-1 px-1.5 transition active:scale-90" onClick={decrement} > - </button> <button class="rounded-md border p-1 px-1.5 transition active:scale-90" onClick={increment} > + </button> <button class="rounded-md border p-1 px-1.5 transition active:scale-90" onClick={reset}> Reset </button> <button class="rounded-md border p-1 px-1.5 transition active:scale-90" onClick={() => set(Math.floor(Math.random() * 10))} > Set (To Random) </button> </div> </div> ); }
import { useDebouncedCallback } from 'bagon-hooks'; import { createSignal, For, JSX, Show } from 'solid-js'; function getSearchResults(query: string): Promise<{ id: number; title: string }[]> { return new Promise(resolve => { setTimeout(() => { resolve( query.trim() === '' ? [] : Array(5) .fill(0) .map((_, index) => ({ id: index, title: `${query} ${index + 1}` })), ); }, 1000); }); } export function UseDebouncedCallbackExample() { const [search, setSearch] = createSignal(''); const [searchResults, setSearchResults] = createSignal<{ id: number; title: string }[]>([]); const [loading, setLoading] = createSignal(false); const debouncedSearch = useDebouncedCallback(async (query: string) => { setLoading(true); setSearchResults(await getSearchResults(query)); setLoading(false); }, 500); const handleInput: JSX.EventHandler<HTMLInputElement, InputEvent> = event => { setSearch(event.currentTarget.value); debouncedSearch(event.currentTarget.value); }; return ( <div class="flex h-full w-full flex-col items-center justify-center gap-x-1 gap-y-2 rounded-md border p-3 py-10 text-center text-sm"> <input value={search()} onInput={handleInput} class="rounded-md border p-2" placeholder="Search..." /> <Show when={loading()} children={<>Loading...</>} fallback={ <For each={searchResults()}>{result => <div class="text-xs">{result.title}</div>}</For> } /> </div> ); }
import { useDebouncedSignal } from 'bagon-hooks'; export function UseDebouncedSignalExample() { const [signal, setSignal] = useDebouncedSignal('', 500); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-x-1 gap-y-2 rounded-md border p-3 py-10 text-center text-sm"> <input value={signal()} onInput={e => setSignal(e.currentTarget.value)} class="rounded-md border p-2" /> <span>State: {JSON.stringify(signal())}</span> </div> ); }
import { useDebouncedValue } from 'bagon-hooks'; import { createSignal } from 'solid-js'; export function UseDebouncedValueExample() { const [signal, setSignal] = createSignal(''); const [value, cancel] = useDebouncedValue(signal, 500); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-x-1 gap-y-2 rounded-md border p-3 py-10 text-center text-sm"> <input value={signal()} onInput={e => setSignal(e.currentTarget.value)} class="rounded-md border p-2" /> <div class="flex items-center gap-x-2"> <span>State: {JSON.stringify(signal())}</span> <span>|</span> <span>Value: {JSON.stringify(value())}</span> </div> </div> ); }
This logs "Did Update X" to the console. Notice that it doesn't log "Did Update 0" since it happens on mount.
import { useDidUpdate } from 'bagon-hooks'; import { createSignal } from 'solid-js'; export function UseDidUpdateExample() { const [signal, setSignal] = createSignal(0); useDidUpdate(() => { console.log('Did Update', signal()); }, signal); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-x-1 gap-y-2 rounded-md border p-3 py-10 text-center text-sm"> <p class="max-w-xs text-center text-xs"> This logs "Did Update {signal() === 0 ? 'X' : signal()}" to the console. Notice that it doesn't log "Did Update 0" since it happens on mount. </p> <button class="rounded-md bg-primary p-2 text-white transition active:scale-95" onClick={() => { setSignal(signal() + 1); }} > Simulate an Update {signal()} </button> </div> ); }
import { useDisclosure } from 'bagon-hooks'; import { Show } from 'solid-js'; export function UseDisclosureExample() { const [opened, handlers] = useDisclosure(false); return ( <div class="flex h-full w-full items-center justify-center gap-x-1 rounded-md border p-3 py-10 text-center text-sm"> <button onClick={handlers.open} class="rounded bg-blue-500 px-4 py-2 text-white"> Open Dialog </button> <Show when={opened()}> <div class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"> <div class="rounded-lg bg-white p-6 shadow-lg"> <h2 class="mb-4 text-lg font-bold">Dialog Title</h2> <p class="mb-4">This is an example dialog using useDisclosure</p> <button onClick={handlers.close} class="rounded bg-gray-500 px-4 py-2 text-white"> Close </button> </div> </div> </Show> </div> ); }
import { useDisclosureData } from 'bagon-hooks'; import { For, Show } from 'solid-js'; export function UseDisclosureDataExample() { const items = [ { id: '1', title: 'Item 1' }, { id: '2', title: 'Item 2' }, ]; // If you have multiple modals, // I recommend prefixing `data`, `open`, `handlers` with the same name. // i.e. editModalData, editModalOpen, editModalHandlers const [data, open, handlers] = useDisclosureData<(typeof items)[number]>(null); return ( <div class="flex h-full w-full items-center justify-center gap-x-1 rounded-md border p-3 py-10 text-center text-sm"> <ul class="flex flex-col gap-2"> <For each={items}> {item => ( <li> <button onClick={() => { handlers.open(item); }} class="rounded bg-blue-500 px-4 py-2 text-white" > Open Dialog for {item.title} </button> </li> )} </For> </ul> <Show when={open()}> <div class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"> <div class="rounded-lg bg-white p-6 shadow-lg"> <h2 class="mb-4 text-lg font-bold">{data()?.title || 'Dialog Title'}</h2> <p class="mb-4"> This is an example dialog using useDisclosure and data: {data()?.id} </p> <button onClick={handlers.close} class="rounded bg-gray-500 px-4 py-2 text-white"> Close </button> </div> </div> </Show> </div> ); }
import { useDocumentTitle, useToggle } from 'bagon-hooks'; export function UseDocumentTitleExample() { const [title, setTitle] = useDocumentTitle(); const [_, cycle] = useToggle(['Home', 'About', 'Awesome']); function _cycle() { cycle(); setTitle(_() as any); } return ( <div class="flex h-full w-full flex-col items-center justify-center gap-x-1 rounded-md border p-3 py-10 text-center text-sm"> <div class="flex flex-col items-center gap-y-7"> <div class="flex items-center gap-x-2"> <Kbd activated={title() === 'Home'}>Home</Kbd> <Kbd activated={title() === 'About'}>About</Kbd> <Kbd activated={title() === 'Awesome'}>Awesome</Kbd> </div> <button onClick={_cycle}>Toggle</button> </div> </div> ); }
import { useDocumentVisibility } from 'bagon-hooks'; export function UseDocumentVisibilityExample() { const visible = useDocumentVisibility(); return ( <div class="flex h-full w-full items-center justify-center gap-x-1 rounded-md border p-3 py-10 text-center text-sm"> <div class="flex items-center gap-x-1"> <div class="h-2 w-2 rounded-full" /> Tab is currently {visible()} </div> </div> ); }
import { createSignal } from 'solid-js'; import { useDrag } from 'bagon-hooks'; export function UseDragExample() { const [pos, setPos] = createSignal({ x: 16, y: 16 }); const [origin, setOrigin] = createSignal({ x: 16, y: 16 }); let el!: HTMLDivElement; const clamp = (n: number, min: number, max: number) => Math.min(Math.max(n, min), max); const drag = useDrag(state => { let from = origin(); if (state.first) { from = pos(); setOrigin(from); } const parent = el.parentElement!.getBoundingClientRect(); const w = el.offsetWidth; const h = el.offsetHeight; const nextX = clamp(from.x + state.movement[0], 0, parent.width - w); const nextY = clamp(from.y + state.movement[1], 0, parent.height - h); setPos({ x: nextX, y: nextY }); }); return ( <div class="relative h-56 w-full overflow-hidden rounded-md border bg-neutral-50"> <div ref={node => { el = node as HTMLDivElement; drag.ref(node as HTMLDivElement); }} class="absolute flex h-16 w-16 cursor-grab items-center justify-center rounded-md bg-primary text-xs font-medium text-white active:cursor-grabbing" style={{ left: `${pos().x}px`, top: `${pos().y}px`, }} > Drag </div> </div> ); }
import { useElementSize, useIdle, useNetwork, useOs, useResizeObserver } from 'bagon-hooks'; export function UseElementSizeExample() { const { ref, height, width } = useElementSize(); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-start"> <div class="flex flex-col items-center"> <span>Width: {width().toFixed(2)}</span> <span>Height: {height().toFixed(2)}</span> </div> <div class="relative grid flex-1 place-items-center overflow-hidden"> <textarea ref={ref} class="h-20 w-20 resize rounded-md border"></textarea> <div class="pointer-events-none absolute inset-0 grid place-items-center truncate text-center text-xs text-neutral-400"> Resize Me </div> </div> </div> ); }
import { useEventListener } from 'bagon-hooks'; import { createSignal } from 'solid-js'; export function UseEventListenerExample() { const [count, setCount] = createSignal(0); const ref = useEventListener('click', () => setCount(c => c + 1)); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm"> <button ref={ref} class="rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95"> Click me </button> <span>Clicks: {count()}</span> </div> ); }
import { useEyeDropper } from 'bagon-hooks'; import { Show } from 'solid-js'; export function UseEyeDropperExample() { const { color, supported, pickColor } = useEyeDropper(); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-5 rounded-md border p-3 py-10 text-center"> <div class="flex items-center gap-x-2"> <button class="transition active:scale-95" onClick={pickColor} disabled={!supported()}> <IconEyeDropper /> </button> <div class="flex items-center gap-x-2 text-sm"> Picked Color: {color()} <div class="h-8 w-8 rounded-full border" style={{ 'background-color': color() }} /> </div> </div> <Show when={supported() !== undefined && !supported()}> <span class="text-xs text-red-500">Your browser does not support EyeDropper.</span> </Show> </div> ); }
import { useFavicon } from 'bagon-hooks'; import { createSignal } from 'solid-js'; /** Improved Bagon implementation - You can choose to set it when you want. */ export function UseFaviconExample() { const [_favicon, setFavicon] = useFavicon(); // The secret is: just don't pass an accessor in the hook. const setXFavicon = () => { setCurrentIcon('x'); setFavicon('https://x.com/favicon.ico'); }; const setSolidFavicon = () => { setCurrentIcon('solid'); setFavicon('https://docs.solidjs.com/favicon.svg'); }; return ( <div class="relative flex h-full w-full flex-col items-center justify-center gap-3 overflow-hidden rounded-md border p-3 py-10 text-center text-sm"> <IconSolidJS class="absolute -bottom-10 -right-10 h-48 w-48 rotate-45 transition" style={{ opacity: currentIcon() === 'solid' ? 1 : 0 }} /> <IconX class="absolute -bottom-10 -right-10 h-48 w-48 rotate-45 transition" style={{ opacity: currentIcon() === 'x' ? 1 : 0 }} /> <button onClick={() => { setXFavicon(); }} class="relative rounded-md border bg-white px-2 py-1.5 transition active:scale-95" > X favicon </button> <button onClick={() => { setSolidFavicon(); }} class="relative rounded-md border bg-white px-2 py-1.5 transition active:scale-95" > Solid favicon </button> </div> ); } /** Based on Mantine's implementation - it always runs onMount. */ export function UseFaviconExampleMantine() { const [favicon, setFavicon] = createSignal('https://docs.solidjs.com/favicon.svg'); const setXFavicon = () => setFavicon('https://x.com/favicon.ico'); const setSolidFavicon = () => setFavicon('https://docs.solidjs.com/favicon.svg'); useFavicon(favicon); // Will always run at the start. return ( <> <button onClick={setXFavicon}>Use X Favicon</button> <button onClick={setSolidFavicon}>Use SolidJS Favicon</button> </> ); }
import { useFetch } from 'bagon-hooks'; import { Show } from 'solid-js'; export function UseFetchExample() { const { data, error, loading, refetch } = useFetch<{ userId: number; id: number; title: string; completed: boolean }>( 'https://jsonplaceholder.typicode.com/todos/1', ); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm"> <Show when={loading()}> <span class="text-xs">Loading...</span> </Show> <Show when={error()}> <span class="text-xs text-red-500">{error()!.message}</span> </Show> <Show when={data() && !loading()}> <pre class="max-w-full overflow-auto rounded-md border bg-neutral-100 p-3 px-5 text-left text-xs"> {JSON.stringify(data(), null, 2)} </pre> </Show> <button class="rounded-md border px-2 py-1 text-sm transition active:scale-90" onClick={() => refetch()}> Refetch </button> </div> ); }
import { useFileDialog } from 'bagon-hooks'; import { For, Show } from 'solid-js'; export function UseFileDialogExample() { const { files, open, reset } = useFileDialog({ multiple: true, accept: 'image/*' }); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm"> <div class="flex gap-2"> <button class="rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95" onClick={open}> Open </button> <button class="rounded-md bg-gray-400 px-3 py-1.5 text-white transition active:scale-95" onClick={reset}> Reset </button> </div> <Show when={files()} fallback={<span class="text-xs text-neutral-500">No files selected</span>} > <ul class="text-xs"> <For each={Array.from(files()!)}>{file => <li>{file.name}</li>}</For> </ul> </Show> </div> ); }
strategy: 'absolute' and clamped to this card.import { onMount } from 'solid-js'; import { useFloatingWindow } from 'bagon-hooks'; export function UseFloatingWindowExample() { const { ref, setPosition } = useFloatingWindow({ strategy: 'absolute', constrainToViewport: true, // clamp to parent when absolute initialPosition: { top: 56, left: 16 }, dragHandleSelector: '.drag-handle', }); let playground!: HTMLDivElement; let windowEl!: HTMLDivElement; const place = (corner: 'tl' | 'tr' | 'bl' | 'br') => { const width = playground.clientWidth; const height = playground.clientHeight; const w = windowEl.offsetWidth; const h = windowEl.offsetHeight; const pad = 8; if (corner === 'tl') setPosition({ top: pad, left: pad }); if (corner === 'tr') setPosition({ top: pad, left: width - w - pad }); if (corner === 'bl') setPosition({ top: height - h - pad, left: pad }); if (corner === 'br') setPosition({ top: height - h - pad, left: width - w - pad }); }; onMount(() => { // Ensure measured sizes are available before placing. place('tl'); }); return ( <div ref={playground} class="relative h-64 w-full overflow-hidden rounded-md border bg-neutral-50" > <div class="absolute left-2 top-2 z-10 flex flex-wrap gap-1"> <button type="button" class="rounded bg-white px-2 py-1 text-xs shadow border" onClick={() => place('tl')} > Top-left </button> <button type="button" class="rounded bg-white px-2 py-1 text-xs shadow border" onClick={() => place('tr')} > Top-right </button> <button type="button" class="rounded bg-white px-2 py-1 text-xs shadow border" onClick={() => place('bl')} > Bottom-left </button> <button type="button" class="rounded bg-white px-2 py-1 text-xs shadow border" onClick={() => place('br')} > Bottom-right </button> </div> <div ref={el => { ref(el); windowEl = el; }} class="absolute z-20 w-48 overflow-hidden rounded-md border bg-white shadow" > <div class="drag-handle cursor-move bg-primary px-3 py-2 text-sm font-medium text-white"> Drag me </div> <div class="p-3 text-sm text-neutral-600"> Positioned with <code>strategy: 'absolute'</code> and clamped to this card. </div> </div> </div> ); }
Focus the button, open the dialog, type in dialog content, then close it — focus returns to the trigger.
import { createSignal, Show } from 'solid-js'; import { useFocusReturn } from 'bagon-hooks'; export function UseFocusReturnExample() { const [opened, setOpened] = createSignal(false); // Return focus to the trigger when the dialog closes. useFocusReturn({ opened: () => opened(), shouldReturnFocus: true, }); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center"> <button type="button" class="rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95 focus:outline-none focus:ring-2 focus:ring-blue-900" onClick={() => setOpened(true)} > Open dialog </button> <p class="max-w-sm text-center text-xs text-neutral-500"> Focus the button, open the dialog, type in dialog content, then close it — focus returns to the trigger. </p> <Show when={opened()}> <div role="dialog" aria-modal="true" class="flex w-full max-w-sm flex-col gap-3 rounded-md border bg-white p-4 shadow-sm" > <p class="text-sm">Dialog content</p> <input autofocus class="rounded-md border px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-900" placeholder="Type here…" /> <button type="button" class="rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-neutral-800 transition active:scale-95 focus:outline-none focus:ring-2 focus:ring-blue-900" onClick={() => setOpened(false)} > Close (returns focus) </button> </div> </Show> </div> ); }
import { createSignal, Show } from 'solid-js'; import { useFocusTrap } from 'bagon-hooks'; export function UseFocusTrapExample() { const [active, setActive] = createSignal(false); const trapRef = useFocusTrap(active); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center"> <button type="button" class="rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95" onClick={() => setActive(true)} > Open trap </button> <Show when={active()}> <div ref={trapRef} class="flex w-full max-w-sm flex-col gap-3 rounded-md border bg-white p-4 shadow-sm" > <p class="text-sm">Focus is trapped — try Tab / Shift+Tab.</p> <input class="rounded-md border px-2 py-1 text-sm" placeholder="First input" /> <input class="rounded-md border px-2 py-1 text-sm" placeholder="Second input" /> <div class="flex justify-center gap-2"> <button type="button" class="rounded-md border px-2 py-1 text-sm transition active:scale-90"> Action </button> <button type="button" class="rounded-md bg-gray-400 px-3 py-1.5 text-white transition active:scale-95" onClick={() => setActive(false)} > Close </button> </div> </div> </Show> <button type="button" class="rounded-md border px-2 py-1 text-sm transition active:scale-90"> Outside button (unreachable while trapped) </button> </div> ); }
import { useFocusWithin } from 'bagon-hooks'; export function UseFocusWithinExample() { const { ref, focused } = useFocusWithin(); return ( <div class="flex h-full w-full items-center justify-center rounded-md border p-3 py-10 text-center text-sm"> <div ref={ref} class={`flex flex-col gap-2 rounded-md border p-4 ${focused() ? 'border-blue-500 bg-blue-50' : 'bg-neutral-50'}`} > <span class="text-xs">Focused: {String(focused())}</span> <input class="rounded-md border p-2" placeholder="Focus me" /> <button class="rounded-md border px-2 py-1 text-sm transition active:scale-90">Or me</button> </div> </div> ); }
import { useFullscreen } from 'bagon-hooks'; import { Show } from 'solid-js'; export function UseFullScreenExample() { const { fullscreen, toggle } = useFullscreen(); return ( <div class="flex h-full w-full items-center justify-center gap-x-1 rounded-md border p-3 py-10 text-center"> <button class={`rounded-md px-3 py-1.5 text-white transition active:scale-95 ${fullscreen() ? 'bg-red-500' : 'bg-primary'}`} onClick={toggle} > <Show when={fullscreen()} children={'Exit Fullscreen'} fallback={'Enter Fullscreen'} /> </button> </div> ); }
import { randomId, useHash } from 'bagon-hooks'; export function UseHashExample() { const [hash, setHash] = useHash(); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center"> <button class="rounded-md border px-3 py-1.5 text-sm text-typography transition active:scale-95" onClick={() => setHash(randomId())} > Set hash </button> <span class="flex gap-x-1 text-sm"> Current hash: <code class="rounded-md bg-neutral-300 px-1.5 py-0.5">{hash()}</code> </span> </div> ); }
Scroll the page (not this panel) to pin / unpin the header.
pinned: true
scrollProgress: 100%
export function UseHeadroomExample() { const { pinned, scrollProgress } = useHeadroom({ fixedAt: 40, scrollDistance: 80, }); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm"> <div class="w-full overflow-hidden rounded-md border"> <div class="flex h-12 items-center justify-between bg-neutral-50 px-3 transition-transform duration-200" style={{ transform: `translateY(${(scrollProgress() - 1) * 100}%)` }} > <span class="font-medium">Header</span> <span class="text-neutral-500"> pinned={String(pinned())} · {Math.round(scrollProgress() * 100)}% </span> </div> <div class="space-y-2 p-3 text-left text-xs text-neutral-600"> <p>Scroll the <strong>page</strong> (not this panel) to pin / unpin the header.</p> <p>pinned: {String(pinned())}</p> <p>scrollProgress: {Math.round(scrollProgress() * 100)}%</p> </div> </div> </div> ); }
Side panel
State: entered
import { createSignal } from 'solid-js'; import { useHorizontalCollapse } from 'bagon-hooks'; export function UseHorizontalCollapseExample() { const [expanded, setExpanded] = createSignal(true); const { getCollapseProps, state } = useHorizontalCollapse({ expanded }); const props = getCollapseProps(); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center"> <button class="rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95" type="button" onClick={() => setExpanded(value => !value)} > {expanded() ? 'Collapse' : 'Expand'} </button> <div class="flex h-32 w-full max-w-md overflow-hidden rounded-md border"> <div ref={props.ref} style={props.style()} aria-hidden={props['aria-hidden']()} onTransitionEnd={props.onTransitionEnd} class="overflow-hidden bg-sky-50" > <div class="w-40 p-3 text-left text-sm text-neutral-600"> <p>Side panel</p> <p>State: {state()}</p> </div> </div> <div class="flex flex-1 items-center justify-center bg-neutral-50 text-sm">Main content</div> </div> </div> ); }
import { useHotkeys, useOs } from 'bagon-hooks'; import { createSignal, FlowProps, VoidProps } from 'solid-js'; export function UseHotkeysExample() { const [activatedHotkey, setActivatedHotkey] = createSignal<-1 | 0 | 1 | 2>(-1); let timeout: ReturnType<typeof window.setTimeout>; function handleKeyPress(index: ReturnType<typeof activatedHotkey>) { if (timeout) clearTimeout(timeout); setActivatedHotkey(index); timeout = setTimeout(() => { setActivatedHotkey(-1); }, 400); } useHotkeys([ ['mod+a', () => handleKeyPress(0)], ['mod+Enter', () => handleKeyPress(1)], ['shift+g', () => handleKeyPress(2)], ]); const os = useOs(); return ( <div class="flex h-full w-full flex-wrap items-center justify-center gap-3 rounded-md border p-3 py-10 text-center"> <Key activated={activatedHotkey() === 0}>{os() === 'macos' ? 'cmd' : 'ctrl'} + a</Key> <Key activated={activatedHotkey() === 1}>{os() === 'macos' ? 'cmd' : 'ctrl'} + Enter</Key> <Key activated={activatedHotkey() === 2}>shift + g</Key> </div> ); } function Key(props: FlowProps<{ activated: boolean }>) { return ( <div class="relative text-xs"> <div class="absolute inset-0 rounded-md bg-neutral-200 transition"></div> <div class="relative transform rounded-md border bg-neutral-50 px-2 py-1.5 transition-transform" style={{ transform: props.activated ? 'translateY(0px)' : 'translateY(-5px)', }} > {props.children} </div> </div> ); }
import { useClickOutside, useHover } from 'bagon-hooks'; import { createSignal, Show } from 'solid-js'; export function UseHoverExample() { const { ref, hovered } = useHover(); return ( <div class="flex h-full w-full flex-1 items-center justify-center rounded-md border p-3 py-10"> <div class={`cursor-pointer select-none rounded-full border bg-neutral-50 p-2 text-xs transition ${hovered() ? 'scale-95' : ''}`} ref={ref} > <Show when={hovered()} fallback={'No detections'}> You hovered me! </Show> </div> </div> ); }
import { useId } from 'bagon-hooks'; export function UseIdExample() { const id = useId(); return ( <div class="flex h-full w-full items-center justify-center gap-x-1 rounded-md border p-3 py-10 text-center text-sm"> Random ID: <span class="rounded-md bg-neutral-300 px-1.5 py-0.5">{id()}</span> </div> ); }
import { useIdle, useOs } from 'bagon-hooks'; import { Show } from 'solid-js'; export function UseIdleExample() { const idle = useIdle(1000); return ( <div class="flex h-full w-full items-center justify-center gap-x-1 rounded-md border p-3 py-10 text-center"> <span>Is Idle (1s):</span> <Show when={idle()} fallback={ <span> <span class="text-green-500">false</span> </span> } > <span class="text-red-500">true</span> </Show> </div> ); }
import { useInViewport } from 'bagon-hooks'; import { Show } from 'solid-js'; export function UseInViewportExample() { const { ref, inViewport } = useInViewport(); return ( <div class="relative flex h-full min-h-32 w-full flex-col items-center justify-center gap-x-1 overflow-y-scroll rounded-md border p-3 text-center text-sm"> <div class="sticky left-0 right-0 top-0 text-center"> <Show when={inViewport()} fallback={<>Scroll to See Box</>} children={<>Box is visible</>} /> </div> <div class="relative top-[calc(60%)] pb-5 pt-20"> <div ref={ref} class={`rounded-md p-5 text-white ${inViewport() ? 'bg-green-500' : 'bg-red-500'}`} > <Show when={inViewport()} children={<>Fully Intersecting</>} fallback={<>Obscured</>} /> </div> </div> </div> ); }
{"input":"","checkbox":false}import { useInputState } from 'bagon-hooks'; export function UseInputStateExample() { const [input, handleInput] = useInputState(''); const [checkbox, handleCheckbox] = useInputState(false); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-x-1 gap-y-2 rounded-md border p-3 py-10 text-center text-sm"> <pre class="rounded bg-neutral-200 p-1 text-xs"> {JSON.stringify({ input: input(), checkbox: checkbox(), })} </pre> <input value={input()} onInput={handleInput} class="rounded-md border p-2" /> <input type="checkbox" checked={checkbox()} onChange={handleCheckbox} class="rounded-md border p-2" /> </div> ); }
import { useIntersection } from 'bagon-hooks'; import { Show } from 'solid-js'; export function UseIntersectionExample() { const { ref, entry } = useIntersection({ threshold: 0.75, // At least 75% of the element must "intersect" with the viewport }); return ( <div class="relative flex h-full min-h-32 w-full items-center justify-center gap-x-1 overflow-y-scroll rounded-md border p-3 text-center text-sm"> <div class="relative top-[calc(60%)] pb-5"> <div ref={ref} class={`rounded-md p-5 text-white ${entry()?.isIntersecting ? 'bg-green-500' : 'bg-red-500'}`} > <Show when={entry()?.isIntersecting} children={<>Fully Intersecting</>} fallback={<>Obscured</>} /> </div> </div> </div> ); }
Page loaded 0 seconds ago.
import { useInterval } from 'bagon-hooks'; import { createSignal } from 'solid-js'; export function UseIntervalExample() { const [seconds, setSeconds] = createSignal(0); const interval = useInterval( () => { setSeconds(s => s + 1); }, 1000, { autoInvoke: true }, ); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-x-3 gap-y-3 rounded-md border p-3 py-10 text-center"> <p class="text-xl font-medium">Page loaded {seconds()} seconds ago.</p> <button class={`rounded-md ${ !interval.active() ? 'bg-primary' : 'bg-red-500' } px-3 py-1.5 text-white transition active:scale-95`} onClick={interval.toggle} > {interval.active() ? 'Stop' : 'Start'} counting </button> </div> ); }
import { useKeyboard } from 'bagon-hooks'; import { createStore } from 'solid-js/store'; export function UseKeyboardExample() { const [store, setStore] = createStore({ a: false, b: false, c: false, d: false }); useKeyboard({ onKeyDown(event) { // Refactor this into if statements. if (event.key === 'a') { setStore('a', true); } else if (event.key === 'b') { setStore('b', true); } else if (event.key === 'c') { setStore('c', true); } else if (event.key === 'd') { setStore('d', true); } }, onKeyUp(event) { // Refactor this into if statements. if (event.key === 'a') { setStore('a', false); } else if (event.key === 'b') { setStore('b', false); } else if (event.key === 'c') { setStore('c', false); } else if (event.key === 'd') { setStore('d', false); } }, }); return ( <div class="flex h-full w-full items-center justify-center gap-x-1 rounded-md border p-3 py-10 text-center"> <Kbd activated={store.a}>a</Kbd> <Kbd activated={store.b}>b</Kbd> <Kbd activated={store.c}>c</Kbd> <Kbd activated={store.d}>d</Kbd> </div> ); }
import { For } from 'solid-js';
export function UseListStateExample() {
const [values, handlers] = useListState(['Apple', 'Banana', 'Cherry']);
return (
<div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm">
<div class="flex flex-wrap justify-center gap-2">
<button
class="rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95"
onClick={() => handlers.append(`Item ${values().length + 1}`)}
>
Append
</button>
<button
class="rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95"
onClick={() => handlers.prepend(`Item ${values().length + 1}`)}
>
Prepend
</button>
<button
class="rounded-md border px-2 py-1 text-sm transition active:scale-90"
onClick={() => handlers.reorder({ from: 0, to: values().length - 1 })}
>
Move first → last
</button>
<button
class="rounded-md bg-gray-400 px-3 py-1.5 text-white transition active:scale-95"
onClick={() => handlers.setState(['Apple', 'Banana', 'Cherry'])}
>
Reset
</button>
</div>
<ul class="max-h-40 w-full max-w-sm space-y-1 overflow-y-auto text-sm">
<For each={values()}>
{(item, index) => (
<li class="flex items-center justify-between gap-2 rounded-md border px-2 py-1">
<span>
{index()}: {item}
</span>
<button
class="rounded-md bg-gray-400 px-2 py-1 text-xs text-white transition active:scale-90"
onClick={() => handlers.remove(index())}
>
remove
</button>
</li>
)}
</For>
</ul>
</div>
);
}
import { useLocalStorage } from 'bagon-hooks'; import { FlowProps } from 'solid-js'; export function UseLocalStorageExample() { const [value, setValue] = useLocalStorage({ key: 'favorite-fruit', defaultValue: 'apple', }); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center transition-colors"> <div class="flex flex-wrap gap-3"> <Key activated={value() === 'apple'} onClick={() => setValue('apple')}> 🍎 apple </Key> <Key activated={value() === 'orange'} onClick={() => setValue('orange')}> 🍊 orange </Key> <Key activated={value() === 'grape'} onClick={() => setValue('grape')}> 🍇 grape </Key> <Key activated={value() === 'kiwi'} onClick={() => setValue('kiwi')}> 🥝 kiwi{' '} </Key> </div> <span class="text-sm text-neutral-500">Favorite Fruit: {value()}</span> </div> ); } function Key(props: FlowProps<{ activated: boolean; onClick: () => void }>) { return ( <button onClick={props.onClick} class="relative text-xs"> <div class="absolute inset-0 rounded-md bg-neutral-200 transition" /> <div class="relative transform rounded-md border bg-neutral-50 px-2 py-1.5 transition-transform" style={{ transform: props.activated ? 'translateY(0px)' : 'translateY(-5px)', }} > {props.children} </div> </button> ); }
import { useLocalStorageStore } from 'bagon-hooks'; import { For } from 'solid-js'; import { produce } from 'solid-js/store'; export function UseLocalStorageStoreExample() { const [value, setValue] = useLocalStorageStore<{ id: string; name: string }[]>({ key: 'todos-store', defaultValue: [], }); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center transition-colors"> <For each={value}> {(todo, index) => ( <div class="flex flex-wrap items-center gap-1 text-sm"> <input class="rounded-md border p-1" value={todo.name} onInput={event => { setValue( produce(_value => { if (!_value[index()]) return; _value[index()]!.name = event.target.value ?? ''; }), ); }} /> <button onClick={() => { setValue( produce(_value => { _value.splice(index(), 1); }), ); }} > <IconClose width={18} height={18} /> </button> </div> )} </For> <button class={`rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95`} onClick={() => { setValue( produce(_value => { _value.push({ id: Math.random().toString(), name: 'New Todo' }); }), ); }} > New Todo </button> </div> ); }
import { createSignal } from 'solid-js'; import { useLongPress } from 'bagon-hooks'; export function UseLongPressExample() { const [count, setCount] = createSignal(0); const handlers = useLongPress(() => setCount(c => c + 1), { threshold: 500 }); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm"> <button class="select-none rounded-md border px-4 py-2 transition active:scale-95" {...handlers} > Press and hold </button> <strong>Triggered: {count()}</strong> </div> ); }
import { For } from 'solid-js';
export function UseMapExample() {
const map = useMap<string, number>([
['apples', 2],
['oranges', 5],
]);
return (
<div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm">
<div class="flex flex-wrap justify-center gap-2">
<button
class="rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95"
onClick={() => map.set(`item-${map().size + 1}`, Math.floor(Math.random() * 10))}
>
Add
</button>
<button
class="rounded-md bg-gray-400 px-3 py-1.5 text-white transition active:scale-95"
onClick={() => map.clear()}
>
Clear
</button>
</div>
<ul class="max-h-40 w-full max-w-sm space-y-1 overflow-y-auto text-sm">
<For each={[...map()]}>
{([key, value]) => (
<li class="flex items-center justify-between gap-2 rounded-md border px-2 py-1">
<span>
{key}: {value}
</span>
<button
class="rounded-md bg-gray-400 px-2 py-1 text-xs text-white transition active:scale-90"
onClick={() => map.delete(key)}
>
delete
</button>
</li>
)}
</For>
</ul>
<div class="text-xs text-neutral-500">size: {map().size}</div>
</div>
);
}
import { useMask } from 'bagon-hooks'; export function UseMaskExample() { const mask = useMask({ mask: '(999) 999-9999', slotChar: '_', }); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm"> <input ref={mask.ref} value={mask.value()} class="w-full max-w-xs rounded-md border px-3 py-2 text-left" placeholder="(999) 999-9999" /> <div class="text-xs text-neutral-500">Raw: {mask.rawValue() || '(empty)'}</div> <div class="text-xs text-neutral-500">Complete: {mask.isComplete() ? 'yes' : 'no'}</div> <button class="rounded-md bg-gray-400 px-3 py-1.5 text-white transition active:scale-95" type="button" onClick={() => mask.reset()} > Reset </button> </div> ); }
import { useMediaQuery } from 'bagon-hooks'; import { Match, Switch } from 'solid-js'; export function UseMediaQueryExample() { const sm = useMediaQuery(() => '(min-width: 640px)'); const md = useMediaQuery(() => '(min-width: 768px)'); const lg = useMediaQuery(() => '(min-width: 1024px)'); const xl = useMediaQuery(() => '(min-width: 1280px)'); const xxl = useMediaQuery(() => '(min-width: 1536px)'); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-x-1 rounded-md border p-3 py-10 text-center text-sm"> <Switch fallback="No match"> <Match when={xxl()}>2xl: (min-width: 1536px)</Match> <Match when={xl()}>xl: (min-width: 1280px)</Match> <Match when={lg()}>lg: (min-width: 1024px)</Match> <Match when={md()}>md: (min-width: 768px)</Match> <Match when={sm()}>sm: (min-width: 640px)</Match> </Switch> </div> ); }
import { useIdle, useMounted, useOs } from 'bagon-hooks'; import { Show } from 'solid-js'; export function UseMountedExample() { const mounted = useMounted(); return ( <div class="flex h-full w-full items-center justify-center gap-x-1 rounded-md border p-3 py-10 text-center"> <Show when={mounted()}>This only shows on the client.</Show> </div> ); }
{"x":0,"y":0}import { useMouse } from 'bagon-hooks'; export function UseMouseExample() { const { ref, position } = useMouse(); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-4 gap-x-1 rounded-md border p-3 py-10 text-center text-sm"> <div ref={ref} class="flex h-40 w-40 items-center justify-center rounded border bg-neutral-100 text-sm" > Track Here </div> Mouse coordinates{' '} <code class="rounded-md bg-neutral-300 px-1.5 py-0.5">{JSON.stringify(position())}</code> </div> ); }
{"x":"0.50","y":"0.50"}import { useMove } from 'bagon-hooks'; import { createSignal } from 'solid-js'; export function UseMoveExample() { const [value, setValue] = createSignal({ x: 0.5, y: 0.5 }); const { ref, active } = useMove(({ x, y }) => { setValue({ x, y }); }, {}); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-x-1 gap-y-3 rounded-md border p-3 py-10 text-center"> <div ref={ref} class="h-40 w-full rounded bg-blue-400/50" style={{ position: 'relative', }} > <div style={{ position: 'absolute', left: `calc(${value().x * 100}% - ${8}px)`, top: `calc(${value().y * 100}% - ${8}px)`, width: '16px', height: '16px', 'background-color': active() ? '#22c55e' : '#3b82f6', }} /> </div> <div class="flex justify-center"> Values:{' '} <code class="rounded-md bg-neutral-300 px-1.5 py-0.5"> {JSON.stringify({ x: value().x.toFixed(2), y: value().y.toFixed(2), })} </code> </div> </div> ); }
import { createSignal, For } from 'solid-js'; import { useMutationObserver } from 'bagon-hooks'; export function UseMutationObserverExample() { const [items, setItems] = createSignal(['Item 1', 'Item 2']); const [mutations, setMutations] = createSignal(0); const { ref } = useMutationObserver( () => setMutations(c => c + 1), { childList: true, subtree: true }, ); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm"> <div ref={ref} class="min-w-40 rounded-md border bg-neutral-50 p-3"> <ul class="space-y-1 text-left text-xs"> <For each={items()}>{item => <li class="rounded bg-white px-2 py-1 shadow-sm">{item}</li>}</For> </ul> </div> <button class="rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95" onClick={() => setItems(prev => [...prev, `Item ${prev.length + 1}`])} > Append child </button> <span class="text-xs text-neutral-500">Mutations observed: {mutations()}</span> </div> ); }
{
"online": true
}import { useIdle, useNetwork, useOs } from 'bagon-hooks'; export function UseNetworkExample() { const networkStatus = useNetwork(); return ( <div class="flex h-full w-full items-center justify-center gap-x-1 rounded-md border p-3 py-10 text-start"> <pre class={`rounded-md border p-3 px-5 ${networkStatus().online ? 'bg-neutral-100' : 'border-red-500 bg-red-200'}`} > {JSON.stringify(networkStatus(), null, 2)} </pre> </div> ); }
{
"angle": 0,
"type": "landscape-primary"
}import { useOrientation } from 'bagon-hooks'; export function UseOrientationExample() { const orientation = useOrientation(); return ( <div class="flex h-full w-full items-center justify-center gap-x-1 rounded-md border p-3 py-10 text-center text-sm"> <pre class={`rounded-md border bg-neutral-100 p-3 px-5 text-start text-xs`}> {JSON.stringify(orientation(), null, 2)} </pre> </div> ); }
import { useOs } from 'bagon-hooks'; export function UseOsExample() { const os = useOs(); return ( <div class="flex h-full w-full items-center justify-center rounded-md border p-3 py-10 text-center"> Current OS: {os()} </div> ); }
import { createSignal } from 'solid-js'; import { usePageLeave } from 'bagon-hooks'; export function UsePageLeaveExample() { const [left, setLeft] = createSignal(0); usePageLeave(() => setLeft(n => n + 1)); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-2 rounded-md border p-3 py-10 text-center text-sm"> <span>Move your mouse out of the page</span> <strong>Left count: {left()}</strong> </div> ); }
import { For, Show } from 'solid-js';
export function UsePaginationExample() {
const pagination = usePagination({ total: 10, siblings: 1, boundaries: 1 });
return (
<div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm">
<div class="text-sm">Active page: {pagination.active()}</div>
<div class="flex flex-wrap items-center justify-center gap-1">
<button
class="rounded-md border px-2 py-1 text-sm transition active:scale-90"
onClick={pagination.previous}
>
Prev
</button>
<For each={pagination.range()}>
{page => (
<Show
when={page !== 'dots'}
fallback={<span class="px-2 text-xs text-neutral-500">...</span>}
>
<button
class="rounded-md px-2 py-1 text-sm transition active:scale-90"
classList={{
'bg-primary text-white': page === pagination.active(),
border: page !== pagination.active(),
}}
onClick={() => pagination.setPage(page as number)}
>
{page}
</button>
</Show>
)}
</For>
<button
class="rounded-md border px-2 py-1 text-sm transition active:scale-90"
onClick={pagination.next}
>
Next
</button>
</div>
<div class="flex gap-2">
<button
class="rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95"
onClick={pagination.first}
>
First
</button>
<button
class="rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95"
onClick={pagination.last}
>
Last
</button>
</div>
</div>
);
}
import { createSignal } from 'solid-js'; import { usePrevious } from 'bagon-hooks'; export function UsePreviousExample() { const [count, setCount] = createSignal(0); const previous = usePrevious(count); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm"> <div> Current: <strong>{count()}</strong> </div> <div> Previous: <strong>{previous() ?? 'undefined'}</strong> </div> <button class="rounded-md border px-3 py-1.5 transition active:scale-95" onClick={() => setCount(c => c + 1)} > Increment </button> </div> ); }
Only `limit` items stay in state; overflow goes to queue.
“Update all (+1)” maps every item in state and queue.
import { For } from 'solid-js';
export function UseQueueExample() {
let nextId = 4;
const { state, queue, add, update, cleanQueue } = useQueue<number>({
initialValues: [1, 2, 3],
limit: 2,
});
return (
<div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm">
<p class="max-w-sm text-xs text-neutral-500">
Only `limit` items stay in state; overflow goes to queue.
</p>
<div class="flex flex-wrap justify-center gap-2">
<button
class="rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95"
onClick={() => add(nextId++)}
>
Add
</button>
<button
class="rounded-md border px-2 py-1 text-sm transition active:scale-90"
onClick={() => update(current => current.map(value => value + 1))}
>
Update all (+1)
</button>
<button
class="rounded-md bg-gray-400 px-3 py-1.5 text-white transition active:scale-95"
onClick={cleanQueue}
>
Clean queue
</button>
</div>
<div class="grid w-full max-w-sm grid-cols-2 gap-3 text-left text-sm">
<div class="rounded-md border p-2">
<div class="mb-1 font-medium">State (active, limit {2})</div>
<ul class="space-y-1 font-mono text-xs">
<For each={state()} fallback={<li class="text-neutral-400">(empty)</li>}>
{item => <li class="rounded bg-neutral-50 px-2 py-1">{item}</li>}
</For>
</ul>
</div>
<div class="rounded-md border p-2">
<div class="mb-1 font-medium">Queue (backlog)</div>
<ul class="space-y-1 font-mono text-xs">
<For each={queue()} fallback={<li class="text-neutral-400">(empty)</li>}>
{item => <li class="rounded bg-neutral-50 px-2 py-1">{item}</li>}
</For>
</ul>
</div>
</div>
<p class="max-w-sm text-xs text-neutral-400">
“Update all (+1)” maps every item in state and queue.
</p>
</div>
);
}
Drag around the circle
import { createSignal } from 'solid-js'; import { useRadialMove } from 'bagon-hooks'; export function UseRadialMoveExample() { const [value, setValue] = createSignal(45); const radial = useRadialMove(setValue, { step: 1 }); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center"> <div ref={radial.ref} class="relative flex h-40 w-40 items-center justify-center rounded-full border bg-neutral-50" style={{ cursor: radial.active() ? 'grabbing' : 'grab' }} > <div class="absolute h-2 w-16 origin-left rounded-full bg-primary" style={{ left: '50%', top: '50%', transform: `rotate(${value() - 90}deg) translateY(-50%)`, }} /> <span class="relative z-10 rounded bg-white px-2 py-1 text-sm shadow-sm">{Math.round(value())}°</span> </div> <p class="text-xs text-neutral-500">{radial.active() ? 'Dragging…' : 'Drag around the circle'}</p> </div> ); }
import { useReducedMotion } from 'bagon-hooks'; export function UseReducedMotionExample() { const reducedMotion = useReducedMotion(); return ( <div class="flex h-full w-full items-center justify-center gap-x-1 rounded-md border p-3 py-10 text-center text-sm"> <div class="rounded-md border px-4 py-2 text-sm"> Prefers reduced motion: <strong>{String(reducedMotion())}</strong> </div> </div> ); }
{
"rect": {
"x": 0,
"y": 0,
"width": 0,
"height": 0,
"top": 0,
"left": 0,
"bottom": 0,
"right": 0
}
}import { useIdle, useNetwork, useOs, useResizeObserver } from 'bagon-hooks'; export function UseResizeObserverExample() { const [ref, rectStore] = useResizeObserver(); return ( <div class="flex h-full w-full items-center justify-center gap-x-3 rounded-md border p-3 py-10 text-start"> <pre class={`rounded-md border bg-neutral-100 p-3 px-5 text-xs`}> {JSON.stringify(rectStore, null, 2)} </pre> <div class="relative grid place-items-center overflow-hidden"> <textarea ref={ref} class="h-20 w-20 resize rounded-md border"></textarea> <div class="pointer-events-none absolute inset-0 grid place-items-center truncate text-xs text-neutral-400"> Resize Me </div> </div> </div> ); }
Focus the toolbar, then use Arrow keys / Home / End.
Active index: 0
import { createSignal, For } from 'solid-js'; import { useRovingIndex } from 'bagon-hooks'; const ITEMS = ['Home', 'Search', 'Library', 'Settings'] as const; export function UseRovingIndexExample() { const [index, setIndex] = createSignal(0); const { getElementProps, getElementRef, onKeyDown } = useRovingIndex(index, setIndex, { total: ITEMS.length, loop: true, }); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center"> <p class="text-xs text-neutral-500">Focus the toolbar, then use Arrow keys / Home / End.</p> <div role="toolbar" aria-label="Demo toolbar" class="flex flex-wrap justify-center gap-2" onKeyDown={onKeyDown} > <For each={[...ITEMS]}> {(label, i) => ( <button type="button" ref={getElementRef(i())} {...getElementProps(i())} class="rounded-md border px-2 py-1 text-sm transition active:scale-90 outline-none focus-visible:ring-2 focus-visible:ring-blue-500" classList={{ 'bg-primary text-white border-transparent': index() === i(), }} onClick={() => setIndex(i())} > {label} </button> )} </For> </div> <p class="text-sm">Active index: {index()}</p> </div> ); }
import { useScrollDirection } from 'bagon-hooks'; export function UseScrollDirectionExample() { const direction = useScrollDirection(); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-2 rounded-md border p-3 py-10 text-center text-sm"> <span>Scroll the page up or down</span> <strong>Direction: {direction()}</strong> </div> ); }
Spacer block 1
Spacer block 2
Spacer block 3
Spacer block 4
Spacer block 5
Spacer block 6
Spacer block 7
Spacer block 8
Trailing block 1
Trailing block 2
Trailing block 3
Trailing block 4
Trailing block 5
Trailing block 6
Trailing block 7
Trailing block 8
export function UseScrollIntoViewExample() { const { scrollIntoView, targetRef, scrollableRef, scrolling } = useScrollIntoView({ offset: 12, }); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm"> <div class="flex items-center justify-center gap-2"> <button class="rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95" onClick={() => scrollIntoView({ alignment: 'center' })} > Scroll to target </button> <span class="text-xs text-neutral-500">scrolling: {String(scrolling())}</span> </div> <div ref={scrollableRef} class="max-h-56 w-full overflow-y-auto rounded-md border p-3 text-left" > {Array.from({ length: 8 }, (_, i) => ( <p class="mb-3 rounded-md border bg-neutral-50 p-3 text-sm text-neutral-600"> Spacer block {i + 1} </p> ))} <div ref={targetRef} class="rounded-md border border-emerald-500 bg-emerald-50 p-4 text-sm text-emerald-800" > Target element </div> {Array.from({ length: 8 }, (_, i) => ( <p class="mt-3 rounded-md border bg-neutral-50 p-3 text-sm text-neutral-600"> Trailing block {i + 1} </p> ))} </div> </div> ); }
Scroll this pane. The nav on the left tracks the last heading whose top is at or above the offset inside this container — not the whole page.
Pass a local `scrollHost` and scope the selector to headings inside `.spy-demo`. Nav clicks use `spy.scrollTo(index)` so only the host scrolls.
Active detection uses relative tops against the host bounding rect, so nested scroll containers work the same as window scrollspies.
Keep enough content height so each section can reach the top of the scroll host. The offset here is 8px.
Extra space below helps the last heading become active when scrolled into place.
import { createSignal, For } from 'solid-js'; import { useScrollSpy } from 'bagon-hooks'; export function UseScrollSpyExample() { const [scrollRef, setScrollRef] = createSignal<HTMLDivElement | null>(null); const spy = useScrollSpy({ selector: '.spy-demo h2', scrollHost: () => scrollRef(), offset: 8, }); return ( <div class="flex h-64 w-full overflow-hidden rounded-md border text-left text-sm"> <nav class="w-36 shrink-0 space-y-1 overflow-y-auto border-r bg-neutral-50 p-2"> <For each={spy.data()}> {(heading, index) => ( <button type="button" class="block w-full rounded px-2 py-1 text-left text-xs transition" classList={{ 'bg-primary text-white': spy.active() === index(), 'hover:bg-neutral-200': spy.active() !== index(), }} onClick={() => heading.getNode().scrollIntoView({ block: 'start', behavior: 'smooth' }) } > {heading.value} </button> )} </For> </nav> <div ref={setScrollRef} class="spy-demo h-full flex-1 space-y-6 overflow-y-auto p-4"> <section> <h2 id="spy-intro" class="mb-2 text-base font-semibold"> Introduction </h2> <p class="text-neutral-600"> Scroll this pane. The nav on the left tracks the last heading whose top is at or above the offset inside this container — not the whole page. </p> </section> <section> <h2 id="spy-setup" class="mb-2 text-base font-semibold"> Setup </h2> <p class="text-neutral-600"> Pass a local `scrollHost` and scope the selector to headings inside `.spy-demo`. Clicking a nav item scrolls within the card via `scrollIntoView`. </p> </section> <section> <h2 id="spy-usage" class="mb-2 text-base font-semibold"> Usage </h2> <p class="text-neutral-600"> Active detection uses relative tops against the host bounding rect, so nested scroll containers work the same as window scrollspies. </p> </section> <section> <h2 id="spy-notes" class="mb-2 text-base font-semibold"> Notes </h2> <p class="text-neutral-600"> Keep enough content height so each section can reach the top of the scroll host. The offset here is 8px. </p> <p class="mt-4 text-neutral-600"> Extra space below helps the last heading become active when scrolled into place. </p> <div class="h-40" /> </section> </div> </div> ); }
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10
Line 11
Line 12
Line 13
Line 14
Line 15
Line 16
Line 17
Line 18
Line 19
Line 20
Line 21
Line 22
Line 23
Line 24
Line 25
Line 26
Line 27
Line 28
Line 29
Line 30
export function UseScrollerExample() { const { ref, scrollToTop, scrollToBottom, scrollTo } = useScroller({ behavior: 'smooth' }); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center"> <div class="flex flex-wrap justify-center gap-2"> <button class="rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95" type="button" onClick={scrollToTop} > Top </button> <button class="rounded-md border px-2 py-1 text-sm transition active:scale-90" type="button" onClick={() => scrollTo({ y: 120 })} > Mid </button> <button class="rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95" type="button" onClick={scrollToBottom} > Bottom </button> </div> <div ref={ref} class="h-40 w-full overflow-auto rounded-md border p-3 text-left text-sm"> {Array.from({ length: 30 }, (_, index) => ( <p class="mb-2">Line {index + 1}</p> ))} </div> </div> ); }
Select some of this text with the mouse, or use the buttons below to set a range programmatically.
import { createSignal } from 'solid-js'; export function UseSelectionExample() { const [paragraph, setParagraph] = createSignal<HTMLElement | null>(null); const selection = useSelection(); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm"> <p ref={setParagraph} class="max-w-md select-text rounded-md border bg-neutral-50 p-3 text-left text-sm text-neutral-700" > Select some of this text with the mouse, or use the buttons below to set a range programmatically. </p> <div class="flex flex-wrap justify-center gap-2"> <button class="rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95" type="button" onClick={() => selection.setSelection({ start: 0, end: 6 }, paragraph())} > Select first word </button> <button class="rounded-md bg-gray-400 px-3 py-1.5 text-white transition active:scale-95" type="button" onClick={() => selection.clearSelection()} > Clear </button> </div> <div class="text-sm">Selected: {selection.text() || '(none)'}</div> <div class="text-sm">Collapsed: {selection.isCollapsed() ? 'yes' : 'no'}</div> </div> ); }
import { useSessionStorage } from 'bagon-hooks'; export function UseSessionStorageExample() { const [value, setValue, removeValue] = useSessionStorage('bagon-session-demo', { defaultValue: 'hello session', }); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm"> <input value={value()} onInput={e => setValue(e.currentTarget.value)} class="rounded-md border p-2" /> <span class="text-xs text-neutral-500">Stored: {value()}</span> <button class="rounded-md bg-gray-400 px-3 py-1.5 text-white transition active:scale-95" onClick={() => removeValue()} > Remove </button> </div> ); }
import { For } from 'solid-js';
export function UseSetExample() {
const set = useSet(['react', 'solid', 'vue']);
return (
<div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm">
<div class="flex flex-wrap justify-center gap-2">
<button
class="rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95"
onClick={() => set.add(`item-${set().size + 1}`)}
>
Add
</button>
<button
class="rounded-md bg-gray-400 px-3 py-1.5 text-white transition active:scale-95"
onClick={() => set.clear()}
>
Clear
</button>
</div>
<ul class="max-h-40 w-full max-w-sm space-y-1 overflow-y-auto text-sm">
<For each={[...set()]}>
{item => (
<li class="flex items-center justify-between gap-2 rounded-md border px-2 py-1">
<span>{item}</span>
<button
class="rounded-md bg-gray-400 px-2 py-1 text-xs text-white transition active:scale-90"
onClick={() => set.delete(item)}
>
delete
</button>
</li>
)}
</For>
</ul>
<div class="text-xs text-neutral-500">size: {set().size}</div>
</div>
);
}
export function UseSplitterExample() { const splitter = useSplitter({ orientation: 'horizontal', panels: [ { defaultSize: 40, min: 20 }, { defaultSize: 60, min: 20 }, ], }); const formatSize = (size: number | string) => typeof size === 'number' ? `${Math.round(size)}%` : String(size); return ( <div class="flex h-full w-full items-center justify-center rounded-md border p-3 py-10"> <div ref={splitter.ref} class="flex h-48 w-full overflow-hidden rounded-md border"> <div class="flex items-center justify-center overflow-hidden bg-sky-50 text-sm" style={{ width: formatSize(splitter.sizes()[0]!) }} > Primary ({formatSize(splitter.sizes()[0]!)}) </div> <div class="w-1 shrink-0 cursor-col-resize bg-neutral-300 transition hover:bg-blue-400" {...splitter.getHandleProps({ index: 0 })} /> <div class="flex flex-1 items-center justify-center overflow-hidden bg-pink-50 text-sm"> Secondary </div> </div> </div> ); }
export function UseStateHistoryExample() {
const [value, handlers, history] = useStateHistory(0);
return (
<div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm">
<div class="text-4xl font-bold">{value()}</div>
<div class="text-xs text-neutral-500">
history: {JSON.stringify(history())}
</div>
<div class="flex flex-wrap justify-center gap-2">
<button
class="rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95"
onClick={() => handlers.set(value() + 1)}
>
Increment
</button>
<button
class="rounded-md border px-2 py-1 text-sm transition active:scale-90"
onClick={() => handlers.back()}
>
Back
</button>
<button
class="rounded-md border px-2 py-1 text-sm transition active:scale-90"
onClick={() => handlers.forward()}
>
Forward
</button>
<button
class="rounded-md bg-gray-400 px-3 py-1.5 text-white transition active:scale-95"
onClick={() => handlers.reset()}
>
Reset
</button>
</div>
</div>
);
}
Select any part of this paragraph. The hook listens to `selectionchange` and updates even though `document.getSelection()` returns the same object reference. Try selecting “Solid” or a longer phrase below.
Solid reactivity needs an explicit version bump when APIs reuse object identity. That is why this demo updates live while you change the selection.
import { createMemo, Show } from 'solid-js'; import { useTextSelection } from 'bagon-hooks'; export function UseTextSelectionExample() { const selection = useTextSelection(); const selectedText = createMemo(() => selection()?.toString() || ''); return ( <div class="flex w-full flex-col gap-3 rounded-md border p-4 text-left text-sm"> <p class="select-text leading-relaxed text-neutral-700"> Select any part of this paragraph. The hook listens to `selectionchange` and updates even though `document.getSelection()` returns the same object reference. Try selecting “Solid” or a longer phrase below. </p> <p class="select-text leading-relaxed text-neutral-700"> Solid reactivity needs an explicit version bump when APIs reuse object identity. That is why this demo updates live while you change the selection. </p> <div class="rounded-md bg-neutral-50 px-3 py-2 font-mono text-xs"> <div class="mb-1 text-[10px] uppercase tracking-wide text-neutral-500">Selected text</div> <Show when={selectedText()} fallback={<span class="text-neutral-400">(none)</span>}> <span class="text-primary">{selectedText()}</span> </Show> </div> </div> ); }
import { useThrottledCallback } from 'bagon-hooks'; import { createSignal, For } from 'solid-js'; export function UseThrottledCallbackExample() { const [searchResults, setSearchResults] = createSignal< { title: string; description: string }[] >([]); const handleSearch = useThrottledCallback(async (query: string) => { if (!query) { setSearchResults([]); return; } const response = await fetch(`https://dummyjson.com/products/search?q=${query}&limit=3`); const data = await response.json(); setSearchResults( data.products.map((p: any) => ({ title: p.title, description: p.description, })), ); }, 500); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm"> <input placeholder="Search products..." onInput={e => handleSearch(e.currentTarget.value)} class="rounded-md border p-2" /> <div class="flex flex-col gap-1"> <For each={searchResults()}> {result => <div class="text-xs">{result.title}</div>} </For> </div> </div> ); }
import { useThrottledState } from 'bagon-hooks'; export function UseThrottledStateExample() { const [value, setValue] = useThrottledState('', 500); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm"> <input onInput={e => setValue(e.currentTarget.value)} class="rounded-md border p-2" placeholder="Type quickly..." /> <span>Throttled value: {JSON.stringify(value())}</span> </div> ); }
import { useThrottledValue } from 'bagon-hooks'; import { createSignal } from 'solid-js'; export function UseThrottledValueExample() { const [value, setValue] = createSignal(''); const throttled = useThrottledValue(value, 500); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm"> <input value={value()} onInput={e => setValue(e.currentTarget.value)} class="rounded-md border p-2" /> <span>Value: {JSON.stringify(value())}</span> <span>Throttled: {JSON.stringify(throttled())}</span> </div> ); }
You: not awesome
You will become Awesome in 1 second after pressing 'Start'. You can also cancel.
import { useTimeout } from 'bagon-hooks'; import { createSignal } from 'solid-js'; export function UseTimeoutExample() { const [awesomeState, setAwesomeState] = createSignal('not awesome'); const { start, clear } = useTimeout(() => { setAwesomeState('awesome'); }, 1000); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-x-3 gap-y-3 rounded-md border p-3 py-10 text-center"> <p class="text-xl font-medium">You: {awesomeState()}</p> <p class="max-w-xs text-sm"> You will become <b>Awesome</b> in 1 second after pressing 'Start'. You can also cancel. </p> <div class="flex gap-4"> <button class={`w-20 rounded-md bg-primary px-3 py-1.5 text-white transition active:scale-95`} onClick={() => { start(1000); }} > Start </button> <button class="w-20 rounded-md bg-gray-400 px-3 py-1.5 text-white transition active:scale-95" onClick={() => { clear(); setAwesomeState('not awesome'); }} > Cancel </button> </div> </div> ); }
import { useToggle } from 'bagon-hooks'; import { createMemo, FlowProps } from 'solid-js'; export function UseToggleExample() { const [value, toggle] = useToggle(['apple', 'orange', 'grape', 'kiwi'] as const); const color = createMemo(() => { if (value() === 'apple') return '#e5312f'; if (value() === 'orange') return '#fc8627'; if (value() === 'grape') return '#bc3d73'; if (value() === 'kiwi') return '#acc144'; return undefined; }); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center transition-colors" style={{ 'background-color': color(), }} > <div class="flex flex-wrap gap-3"> <Key activated={value() === 'apple'}>🍎 apple</Key> <Key activated={value() === 'orange'}>🍊 orange</Key> <Key activated={value() === 'grape'}>🍇 grape</Key> <Key activated={value() === 'kiwi'}>🥝 kiwi </Key> </div> <button onClick={() => toggle()} class="text-white transition active:scale-95"> Click me to Toggle </button> </div> ); } function Key(props: FlowProps<{ activated: boolean }>) { return ( <div class="relative text-xs"> <div class="absolute inset-0 rounded-md bg-neutral-200 transition"></div> <div class="relative transform rounded-md border bg-neutral-50 px-2 py-1.5 transition-transform" style={{ transform: props.activated ? 'translateY(0px)' : 'translateY(-5px)', }} > {props.children} </div> </div> ); }
{
"value": "",
"isControlled": true
}import { useUncontrolled } from 'bagon-hooks'; import { createSignal, Show } from 'solid-js'; export function UseUncontrolledExample() { const [isControlled, setIsControlled] = createSignal<boolean>(false); return ( <div class="flex w-full flex-col items-center justify-center gap-5 gap-x-1 rounded-md border p-3 text-center"> <label class="flex items-center gap-2 text-sm"> <input type="checkbox" checked={isControlled()} onChange={e => { setIsControlled(e.currentTarget.checked); }} /> Show Controlled </label> <Show when={isControlled()} fallback={<UncontrolledUsage />} children={<ControlledUsage />} /> </div> ); } function ControlledUsage() { const [value, setValue] = createSignal<string>('controlled text'); return ( <div class="flex flex-col gap-3"> <h3 class="text-lg font-semibold">Controlled Mode</h3> <pre class="rounded bg-neutral-200 p-1 text-start text-xs"> {JSON.stringify({ parent_value: value() }, null, 2)} </pre> <div class="flex gap-1"> <CustomInput value={value()} onChange={setValue} class="rounded-md border p-2" placeholder="Type something..." /> <input class="h-[42px] rounded-md border p-2" value={value()} onInput={e => setValue(e.currentTarget.value)} /> </div> </div> ); } function UncontrolledUsage() { return ( <div class="flex flex-col gap-3"> <h3 class="text-lg font-semibold">Uncontrolled Mode</h3> <CustomInput class="rounded-md border p-2" placeholder="Type something..." /> </div> ); } function CustomInput(props: { value?: string; defaultValue?: string; onChange?: (value: string) => void; class?: string; placeholder?: string; }) { const [value, handleChange, isControlled] = useUncontrolled<string>({ value: () => props.value, defaultValue: props.defaultValue, finalValue: '', onChange: props.onChange, }); return ( <div class="flex flex-col gap-2"> <input type="text" value={value() ?? ''} onInput={event => handleChange(event.currentTarget.value)} class={props.class} placeholder={props.placeholder} /> <pre class="rounded bg-neutral-200 p-1 text-start text-xs"> {JSON.stringify({ value: value(), isControlled: isControlled }, null, 2)} </pre> </div> ); }
export function UseValidatedStateExample() {
const [{ value, lastValidValue, valid }, setEmail] = useValidatedState(
'',
val => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),
);
return (
<div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm">
<input
class="w-full max-w-xs rounded-md border px-3 py-2 text-sm outline-none focus:border-blue-500"
type="email"
placeholder="email@example.com"
value={value()}
onInput={event => setEmail(event.currentTarget.value)}
/>
<div class="space-y-1 text-center text-sm">
<div>
valid:{' '}
<span class={valid() ? 'text-green-600' : 'text-red-600'}>{String(valid())}</span>
</div>
<div class="text-neutral-500">last valid: {lastValidValue() ?? '—'}</div>
<div class="text-xs text-neutral-400">Try `carlo@.` (invalid) vs `carlo@a.com` (valid).</div>
</div>
</div>
);
}
{
"width": 0,
"height": 0
}Resize the window to updateimport { useViewportSize } from 'bagon-hooks'; export function UseViewportSizeExample() { const { width, height } = useViewportSize(); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm"> <pre class="rounded-md border bg-neutral-100 p-3 px-5 text-left text-xs"> {JSON.stringify({ width: width(), height: height() }, null, 2)} </pre> <span class="text-xs text-neutral-500">Resize the window to update</span> </div> ); }
{
"x": 0,
"y": 0
}import { useWindowScroll } from 'bagon-hooks'; export function UseWindowScrollExample() { const [position, scrollTo] = useWindowScroll(); return ( <div class="flex h-full w-full flex-col items-center justify-center gap-3 rounded-md border p-3 py-10 text-center text-sm"> <pre class="rounded-md border bg-neutral-100 p-3 px-5 text-left text-xs"> {JSON.stringify(position(), null, 2)} </pre> <div class="flex gap-2"> <button class="rounded-md border px-2 py-1 text-sm transition active:scale-90" onClick={() => scrollTo({ y: 0 })} > Top </button> <button class="rounded-md border px-2 py-1 text-sm transition active:scale-90" onClick={() => scrollTo({ y: 500 })} > y: 500 </button> </div> </div> ); }