Notes
moco/notesFreeComponent
Tufte-style numbered margin notes (CSS-counter ordered, tap-to-expand below 1280px) and dotted-underline term popovers with viewport-edge flip.
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/notes.jsonSource
1"use client";23import * as React from "react";4import "./notes.css";56/**7 * Tufte-style margin note. Numbering comes from a CSS counter, so it is always8 * document order with no registration bookkeeping. On screens ≥1280px it sits9 * in the right gutter; below that it collapses to a tap-to-expand disclosure.10 * There is no jump-to-bottom footnote anywhere.11 *12 * Wrap your prose column in `.moco-notes-scope` — it initializes the counter13 * and is the positioned ancestor the margin notes hang off.14 */15export interface NoteProps {16 children: React.ReactNode;17}1819/* Margin notes are absolutely positioned at their anchor's line, so two notes20 anchored close together would overlap in the gutter. After layout, walk the21 scope's notes in document order and push each below the previous one.22 Idempotent: safe to run from every note on mount, resize, or font load. */23function relayoutScope(scope: Element) {24 const notes = scope.querySelectorAll<HTMLElement>(".moco-sidenote-wrap");25 let prevBottom = -Infinity;26 notes.forEach((n) => {27 n.style.removeProperty("--moco-note-shift");28 const r = n.getBoundingClientRect();29 const shift = Math.max(0, prevBottom + 10 - r.top);30 if (shift > 0) n.style.setProperty("--moco-note-shift", `${shift}px`);31 prevBottom = Math.max(prevBottom, r.top + shift + r.height);32 });33}3435function useNoteLayout(ref: React.RefObject<HTMLElement | null>) {36 React.useLayoutEffect(() => {37 const scope = ref.current?.closest(".moco-notes-scope");38 if (!scope) return;39 let raf = 0;40 const schedule = () => {41 cancelAnimationFrame(raf);42 raf = requestAnimationFrame(() => relayoutScope(scope));43 };44 schedule();45 document.fonts?.ready.then(schedule).catch(() => {});46 window.addEventListener("resize", schedule);47 return () => {48 cancelAnimationFrame(raf);49 window.removeEventListener("resize", schedule);50 };51 }, [ref]);52}5354export function Note({ children }: NoteProps) {55 const [open, setOpen] = React.useState(false);56 const id = React.useId();57 const wrap = React.useRef<HTMLSpanElement>(null);58 useNoteLayout(wrap);59 return (60 <span className="moco-note">61 <span className="moco-noteref" aria-hidden="true" />62 <span className="moco-sidenote-wrap" ref={wrap}>63 <button64 type="button"65 className="moco-sidenote-toggle"66 aria-expanded={open}67 aria-controls={id}68 onClick={() => setOpen((v) => !v)}69 >70// … truncated
What it pulls in
Other registry items
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 | |
| Hookshooks | moco | Components | Free | no deps |
