Hooks
moco/hooksFreeComponent
Scroll and motion hooks: useReducedMotion, useScrollspy, useReadProgress, useInView, plus smoothScrollTo.
Live preview
live · rendered by the registry
Rendered by moco on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://mocoui.site/r/hooks.jsonSource
1"use client";23import * as React from "react";45export function useReducedMotion(): boolean {6 const [reduced, setReduced] = React.useState(false);7 React.useEffect(() => {8 const mq = window.matchMedia("(prefers-reduced-motion: reduce)");9 const on = () => setReduced(mq.matches);10 on();11 mq.addEventListener("change", on);12 return () => mq.removeEventListener("change", on);13 }, []);14 return reduced;15}1617/** Index of the section currently crossing the middle band of the viewport. */18export function useScrollspy(ids: string[]): number {19 const [active, setActive] = React.useState(0);2021 React.useEffect(() => {22 if (!ids.length) return;23 const seen = new Map<string, number>();24 const io = new IntersectionObserver(25 (entries) => {26 for (const e of entries) seen.set(e.target.id, e.intersectionRatio);27 // The last heading whose top has passed the band wins.28 let next = 0;29 ids.forEach((id, i) => {30 const el = document.getElementById(id);31 if (el && el.getBoundingClientRect().top <= window.innerHeight * 0.35) next = i;32 });33 setActive(next);34 },35 { rootMargin: "-30% 0px -60% 0px", threshold: [0, 0.5, 1] },36 );37 const els = ids.map((id) => document.getElementById(id)).filter(Boolean) as Element[];38 els.forEach((el) => io.observe(el));3940 const onScroll = () => {41 let next = 0;42 ids.forEach((id, i) => {43 const el = document.getElementById(id);44 if (el && el.getBoundingClientRect().top <= window.innerHeight * 0.35) next = i;45 });46 setActive(next);47 };48 onScroll();49 window.addEventListener("scroll", onScroll, { passive: true });50 return () => {51 io.disconnect();52 window.removeEventListener("scroll", onScroll);53 };54 }, [ids]);5556 return active;57}5859/** 0→1 read progress through the first element matching the selector. */60export function useReadProgress(selector = "#article-root"): number {61 const [p, setP] = React.useState(0);62 React.useEffect(() => {63 const onScroll = () => {64 const el = document.querySelector<HTMLElement>(selector);65 if (!el) return;66 const start = el.offsetTop;67 const end = start + el.offsetHeight - window.innerHeight;68 const y = window.scrollY;69 const raw = end > start ? (y - start) / (end - start) : 0;70 setP(Math.min(1, Math.max(0, raw)));71 };72 onScroll();73 window.addEventListener("scroll", onScroll, { passive: true });74// … truncated
Files it writes
More from moco
All 20 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Codecode | moco | Components | Free | 1 dep · 3 files | |
| Comparecompare | moco | Components | Free | no deps · 2 files | |
| Count Upcountup | moco | Components | Free | no deps · 2 files | |
| Covercover | moco | Components | Free | no deps · 2 files | |
| Diagram Kitdiagram | moco | Components | Free | no deps · 2 files | |
| Dot Griddotgrid | moco | Components | Free | no deps · 2 files | |
| Figurefigure | moco | Components | Free | no deps · 2 files | |
| Islandisland | moco | Components | Free | no deps · 2 files |
