# Facet Facet is a plain HTML, CSS and JS design library. It is one CSS file and one JS file (plus a small optional service-worker engine, facet-sw.js, that powers offline/PWA) hosted at a public URL. Any page consumes it with one link tag and one script tag. No React, no build step, no npm install, never minified. This file is the full usage guide as plain text, for AI crawlers and builders — the exhaustive inventory of what Facet does and how to use it. The human version is the website (start at the site root); the rules for building and maintaining the library itself are in CLAUDE.md. Repo: https://github.com/tanishksharma/Facet Write pages as clean, semantic HTML: real header / nav / main / section / article / footer, real buttons and links with accessible names, one h1 per page, and not one extra wrapper (a container exists only to enable a real feature, like scrolling). The full markup rules are under "Rules for building on Facet" below. ## Building a whole product Facet ships a product-building method, not just parts. To build a complete product for a person, fetch /build.txt — the prompt pack: interview first, agree a v1 plan, build with the library, verify like a user, plus the backend addendum and the quality bar. The human-readable version is /product.html; both carry the same rules. ## Features — the complete capability inventory This list is the exhaustive inventory: if a capability is not on it, it does not exist. (The Features section on the homepage is a curated, human-friendly subset of this list.) Distribution: two files, one link + one script tag (plus the small optional service-worker engine, facet-sw.js, that powers offline/PWA), public URL, no framework, no npm, no build step, never minified. Until v1 every project always gets the latest straight from the web; from v1, frozen /lib/v1/ copies pin old projects and cached updates apply only at page boundaries. This file (llms.txt) is the exhaustive usage guide, and the readable, commented source of lib/facet.css and lib/facet.js is the ground truth — an AI reads those directly. App-ready (PWA): a one-line sw.js stub — importScripts of /lib/facet-sw.js — gives any project the shared caching strategy: pages network-first so deploys land on the very next refresh, cache as the offline fallback, other static assets cached instantly and revalidated behind. data-service-worker="/sw.js" on the script tag registers it; [data-install] buttons show only when the browser offers installation (facet.install() for programmatic prompts); /templates/manifest.json is the fill-in manifest with the icon checklist. Theming: one attribute (data-theme) restyles the whole page including layout containers; dark mode is its own attribute (data-mode="dark") and composes with every theme; both attributes work on any element for subtree theming; system dark preference honoured when the page does not choose; theme and mode ride the URL (?theme=, ?mode=) so shared links open identically; one-line switcher buttons via data-theme-switch and data-mode-toggle; setup by configuration — data attributes on the facet.js script tag boot a page into its look, and facet.set() changes theme, mode or any token live at runtime (skin keys persist across visits; a {persist: false} second argument applies them for this load only — per-load color cycles, session previews); native scrollbars and form controls follow via color-scheme and facet.js keeps the theme-color meta synced so the browser's UI follows the theme; custom accent as a supported recipe — override a rank's four tokens in one block, page-wide on :root or per-subtree on any element, and every component using that rank follows; Style Mixer on the Build-a-theme page — a picker per semantic token restyles the page live over any theme, derives hover/pressed/on-colors, rides the URL (?mix=) and exports a paste-ready facet.set config block; Skin Lab on the Build-a-theme page — every theme in light and dark side by side, the same real layout from identical markup, one attribute apart. Tokens: every design decision is a role-named CSS variable — never a number. The neutral ramp is a set of semantic roles: --background (the page), --surface (cards/inputs, one step above), --surface-hover and --surface-active (interactive fills), --border-subtle / --border / --border-strong (three line weights), and --text / --text-muted / --text-subtle (three text weights) — so a component picks the exact neutral it needs. Plus per-theme status colors, the vivid decorative palette (--color-1 … --color-5) with an AA text-safe -text companion per color, and type and spacing named by intent and each scaled by a global small/medium/large control. Typography is fully tokenised by role: five weights (--weight-body/-medium/-strong/ -heading/-eyebrow), line-height per role (--leading-display/-heading/-snug/-body/ -ui, tighter as type grows), tracking per role (--tracking-display/ -heading/-snug/-body/-label/-caps/-wide — large type tightens, caps labels open up), reading measure (--measure, --measure-narrow), and prose rhythm (--flow-heading-above/-below, --flow-paragraph, --flow-tight — em-based vertical margins that give raw semantic HTML its flow; gap layouts zero them so rhythm and gap never stack). Eighteen font roles, one face per job: the core seven (--font-heading/-body/-mono/-quote/-reading/ -numeric/-hand), the special-job faces (--font-hero/-signature/ -typewriter/-receipt/-barcode/-pixel/-children) and the script pairings (--font-japanese/-chinese/-korean/-devanagari) — all loaded by facet.css itself, every stack ending in a system fallback (full detail under Fonts below). Plus container widths, radii by role (--radius-control/-panel/-card/-pill), border WIDTHS by role (--border-hairline for separators, --border-element for outlines, --border-focus for the focus ring, --border-highlight for emphasis), layered whisper-light shadows, motion durations and easings, focus ring. Ranked accents: the base family plus accent-1 (the one primary action), accent-2 (every secondary action's fill) and accent-3 (links, labels, quiet affordances), each with hover, pressed and on-colors in every theme — components pick ranks, never colors. Adapts to the reader: all sizes in rem, so device/browser text-size settings scale the whole interface; fluid hero type via clamp(); 44px touch targets; no hover-only interactions; configurable motion — Lively springs by default, data-motion="calm" for quiet, "off" for none, prefers-reduced-motion above all of it; color eases by default — a base rule cross-fades paint-only properties on every element, so hover tints, status flips and whole theme or mode switches never snap; ambient motion from one engine (facet.motion) — data-parallax drifts elements and data-shine sweeps a specular light across them, both from the device tilt, the cursor, or an idle drift when no input exists; smooth page transitions — data-transition="page" animates cross-page navigation (old page eases away, new one springs in, URL changes normally), with a[data-no-transition] per-link opt-out and facet.go(url) for programmatic moves, instant fallback where unsupported; a full print & export system (see its section). Base layer: raw semantic HTML looks designed with no classes; one visible keyboard focus ring everywhere; selection colors, thin scrollbars, skip-to-content link, placeholder styling; the hidden attribute always wins; tabular figures on numeric UI. Components and behaviours: themed layout primitives (container in four widths, stack in three gaps, full-viewport snap sections); one-attribute page width — data-width="narrow|wide|xwide|full" on remaps every container from reading column to edge to edge, with a matching control in the settings panel; background materials in two variants (the grid looks and the breathing fluid field), pinnable to the viewport — .bg-fixed for a whole-page still backdrop, data-bg-fluid-fixed for pools the surface scrolls over; the view-stack app shell for screen-to-screen apps (main.view-stack of .view screens, hash-routed, facet.views.go/back, safe-area padded); buttons in three variants and three sizes working on TS ### Icon badge WHEN TO USE: on app icons 3.5rem and larger — launcher tiles, featured cards — to say new, updated or in the works at a glance; smaller list-row icons take an inline version chip instead. What it is: a round status dot pinned to an app icon's top-right corner — a 1.25rem plasticine bead, its top edge lit and its underside softly shaded, wearing a hairline ring of the page background so it pops off any icon, carrying one solid white glyph and no text (the word lives in title/aria-label). One attribute sets the tone and glyph: data-status="new" (info blue, sparkle), "updated" (success green, refresh arrow), "wip" (warning amber, wrench). What it is for: marking apps in a launcher grid or a featured card as new, freshly updated, or in the works. When to use it: only on icons 3.5rem and larger; smaller list-row icons take an inline version chip instead. Which status an app gets is the app's own decision — the library ships only the visual (a common app-side rule: wip while version < 1, else new if published within 30 days, else updated if a release is within 7 days, in that priority). Wrap the icon in an .icon-badge-holder (the positioning context; it also takes data-shine, sized and rounded to the icon) and put the badge beside the img inside it. ### Skeleton, spinner, empty state WHEN TO USE: a skeleton when the coming content's shape is known; a spinner when it is not; an empty state when there is nothing yet — that is content, not an error, and it offers the one action that fills it. What it is: three ways to show absence. Skeleton: a shimmering placeholder — put .skeleton on blocks sized like the content it stands in for; mark the group aria-hidden. Spinner: a small accent ring for indeterminate waits — give the row role=status and words; under reduced motion the ring stands still and the words do the talking. Empty state: .empty — a dashed quiet zone that names what would be here and offers the one action that fills it. Prefer skeletons over spinners whenever the shape is known; an empty state is content, not an error.

Loading…

No reports yet
### Loading skin WHEN TO USE: any region that waits — set aria-busy="true" on it and its own content becomes the pulsing placeholder; no separate skeleton markup to build. What it is: the page-refresh loading state. Put aria-busy="true" on any component or region whose content is being fetched and everything inside turns into quiet pulsing placeholders that trace the shapes already there: text becomes soft bars hugging its own lines, images and icons flatten to gray, controls go quiet and inert. Remove the attribute and the content is back — nothing was replaced, only skinned. What it is for: refreshing live data over a page that has already rendered — lists, cards, dashboards. When to use it: keep the stale content (or placeholder text) in place and skin it, instead of swapping markup for hand-built .skeleton blocks; .skeleton stays for first paint when no content exists yet. facet.busy(el, true|false) does the same and upgrades the bars to per-line pills that hug real text; an element carrying data-skeleton-lines="n" with no content yet gets n lines of generated placeholder words shaped like average text. Put data-static on anything inside that is not live data (a section heading, a label) to leave it readable. Under prefers-reduced-motion the pulse stands still. For table cells, use facet.busy (it wraps the text itself); the pure-CSS skin covers headings, paragraphs, list items, inline text, media and controls.
Monthly spend

₹ 48,600 across 31 transactions.

// refreshing from the server, the JS way (per-line pills): facet.busy(card, true); const data = await fetch("/api/spend").then(r => r.json()); render(card, data); facet.busy(card, false); ### Component states WHEN TO USE: whenever a component needs a state, ride the grammar — a native pseudo-class, an ARIA attribute, or data-status — never an invented class. This section is the whole state system. What it is: the library's state system. Every component state is carried one of three ways, never by an invented class: a native pseudo-class where the platform has one (:disabled, :checked, :indeterminate, :user-invalid); an ARIA attribute where a real semantic exists (aria-pressed, aria-selected, aria-current, aria-expanded, aria-busy, aria-invalid, aria-disabled) — the CSS styles the attribute itself, so looks and accessibility can never drift apart; and data-status="success|warning|error|info" for statuses with no ARIA home (fields, cards, rows). What it is for: selected cards and rows, disabled anything, field validation in three colors, working buttons. When to use it: set the attribute and the look follows, in every theme, light and dark. The older hooks (.field-invalid, the toast's data-kind) stay as permanent aliases. The states, component by component: - Button: hover, focus, pressed, :disabled built in. aria-disabled disables an
the same way. aria-busy="true" swaps the label for a spinner in the button's own ink, holds the width and blocks re-clicks — add disabled too if it should leave the tab order. - Field: data-status="error|warning|success" on the .field colors the control's border and the .field-msg line under it. aria-invalid on the control, or the native :user-invalid (required, pattern, type), lights the error state with no class at all. input[readonly] reads as fact: page fill, muted ink, still selectable. - Card: aria-selected / aria-pressed / aria-current "true" lights the accent ring (a picked card). data-status colors the reading edge — pair it with a badge that says why. - List row: aria-selected or aria-current fills the row and adds an accent bar at the reading edge; aria-disabled mutes it. - Table row: aria-selected="true" on the tints it with the accent. - Chip: aria-pressed toggles (built in); :disabled or aria-disabled mutes it. - Tabs / segmented control: aria-selected marks the open tab (built in); aria-disabled on a tab, or disabled on a segment's radio, mutes the option. - Dropdown menu: aria-disabled mutes an item. - Stepper: the minus/plus buttons disable themselves at min/max (facet.js sets :disabled live). - Checkbox: set el.indeterminate = true in JS for the mixed select-all state — the native mark, themed by accent-color. - Progress: a with no value attribute is indeterminate — an accent sweep slides until the real number arrives. - Any region: aria-busy="true" is the loading skin (above); .empty is the empty state; for failure show words — an .empty with the error and a retry button, or a data-status="error" card.
Picked
Renewal due
Status colors: the four theme tokens --success / --warning / --error / --info each ship a derived pair — --success-tint (soft fill) and --success-edge (border) — mixed once in facet.css, so badges, fields, cards and toasts all tint the same way in every theme. Components read the pair; never hand-roll a color-mix recipe. ### Flagship link WHEN TO USE: the one onward step at the end of a section — at most one or two per screen; ordinary references stay plain links. What it is: a large link — heading-sized text, a strong accent underline, and an arrow that shifts forward on hover. What it is for: the one onward step at the end of a section. When to use it: at most one or two per screen; ordinary references stay plain underlined links. Always an to a real URL, never a button styled as a link. See the full report ### Heading bar WHEN TO USE: the recommended treatment under a page or section heading — one short accent stroke, once or twice per page; never under every heading. What it is: a short thick accent stroke under a heading — add .heading-bar to any h1–h3 and a small pill-shaped bar in accent-3 draws beneath the text. .heading-bar-center centers the heading and its bar. What it is for: section titles on list pages, launchers and landing sections — the designed look between plain text and a font change. When to use it: on the page's main headings, once or twice per page; never under every heading.

Latest articles

Pricing

### Text clamp WHEN TO USE: to hold a long block — a card or list description, a feed excerpt — to a fixed number of lines with a soft fade at the foot, when a hard cut with an ellipsis would read as abrupt. Not for headings or anything a reader must finish on screen. What it is: a line clamp with a fade-out instead of an ellipsis — add .text-clamp to any text block and it shows the first --clamp-lines lines (default 3) and fades the last of them out at the bottom. Set the count per instance with --clamp-lines. The clamp is purely visual: the full text stays in the DOM, so screen readers, reader view, search crawlers and print get all of it (paper un-clamps automatically). What it is for: keeping cards and rows the same height when their copy runs long. When to use it: on long supporting text, never on a heading or the one line a reader must finish.

A long description held to three lines…

Held to two lines…

### Icons WHEN TO USE: every small symbol a product needs — write and facet.js fills it. Never paste icon paths by hand; extend the table instead. What it is: 24x24 line glyphs at a 1.5px stroke with round caps, carried as markup inside facet.js — no image files, no sprite fetch. Write and facet.js fills it; icons ride currentColor and scale with the surrounding text. In running text add .glyph — it makes the svg sit in the sentence like a character (inline-block, sized to the line, baseline-aligned) instead of breaking the line, because the base media rule makes every svg a block. Decorative by default (aria-hidden); an aria-label on the svg makes it meaningful. Extend: facet.icons.rocket = '' then facet.icon(). The set: search close check plus minus arrow-right arrow-left arrow-up arrow-down chevron-right chevron-left chevron-up chevron-down menu more settings home user users heart star bookmark bell calendar clock download upload share print external link copy edit trash filter eye lock mail globe image file folder sun moon info warning help play pause.

Updated an hour ago

### Parallax & idle motion WHEN TO USE: heroes, illustrations, decorative cards — movement at the edges, never on the reading path; one or two moving things per screen. What it is: ambient motion, on by default. data-parallax="depth" registers an element with facet.motion — it drifts up to depth pixels behind the desktop pointer, the scroll's momentum (a velocity impulse), the device tilt where no permission gate blocks it (iOS gates it; facet.motion.setMode("tilt") asks), or the engine's idle life where nothing else drives. Three modes: off | idle | responsive — idle is the DEFAULT, so nothing ever follows the device uninvited. Responsive resolves itself to the device — cursor on fine pointers (a desktop mouse, an iPad trackpad), tilt on touch — and the UI shows the resolved word (Cursor or Tilt), never "responsive"; switching to it from a tap asks the iOS orientation permission right there, and a denial drops honestly back to idle. The choice persists for the session (sessionStorage motion-mode; parallax-enabled mirrors it as "true" only while responsive, and an external "true" upgrades the mode on the next load); every change fires a facet:motion event on document. The engine boots on demand — registering a mover at runtime or switching the mode wakes it even on a page that loaded with none — and registering an element twice is safe; the second registration is ignored. Cursor and tilt mirror the real world directly — just enough smoothing to hide jitter, no easing lag, and a two-degree dead zone so hand tremor reads as stillness — and the moment the pointer or device stops, the vector eases back to centre (in and out, about half a second) and the idle life plays until real input returns. Idle rests at that same centre and, every few seconds, takes one fast smooth loop — down out of the centre, once around, and back up into it (about 1.1s) — so idle starts and ends exactly where everything else rests. The same engine's light channel drives the shine effect (next entry) from the same vector. The idle-* classes (.idle-float, .idle-sway, .idle-pulse) give resting elements a slow pulse; they animate the separate translate/rotate/scale properties so they compose with the engine's transform instead of colliding — the parallax exclusion, solved structurally. Never put data-parallax on elements with their own transform physics (velvet lift/press, the pager). Calm stills idle life; data-motion="off" and prefers-reduced-motion still everything.
Drifts gently
… ### Shine WHEN TO USE: app icons, hero cards, any raised or 3D-feeling piece that should catch light as the device moves. What it is: the shine effect — a specular light on any element with visual depth, the way iOS icons catch light. data-shine lays a soft light spot plus a rim over the element: a bright edge toward the light, a soft shade away from it. facet.motion's light channel writes the light's direction (--shine-x / --shine-y, each -1..1) from the device tilt, the cursor, or the idle drift — the same normalised vector that drives parallax, so the element that translates and the light that sweeps across it always agree on which way the device is leaning. At rest the light sits centred above. What it is for: app icons, hero cards, any raised or 3D-feeling piece that should catch light as the device moves. When to use it: on the element that carries the depth; the overlay is a .shine child span facet.js injects for you (the pseudo-elements stay free for the tooltip; add the span yourself for a static resting shine with JS off), it inherits the element's border-radius, and images need a wrapper because an cannot hold a child — the .icon-badge-holder doubles as one (give the wrapper the icon's radius). Unlike parallax, shine writes no transform — only custom properties — so the parallax exclusion does not apply; it composes with velvet lift/press, the pager, and data-parallax itself. --shine (0..1, default 0.5) tunes the strength.
Catches the light
### Snap section WHEN TO USE: a single-purpose page that tells one story one screen at a time — a calculator's intro, inputs, results. Not for documents (they just scroll), and not for apps with real screen-to-screen navigation (that is the view stack). What it is: full-viewport scroll-snap areas. The .snap wrapper scrolls one full screen at a time and each .snap-section centers its content at a readable width. What it is for: the calculator page skeleton — intro screen, then inputs, then results — and any page that tells one story one screen at a time. When to use it: single-purpose pages with a clear screen-by-screen flow. Not for ordinary documents; normal pages just scroll. The .snap wrapper exists to enable scrolling, which is the one reason the wrapper law allows a container inside a container.
intro
inputs
results
With facet.js present, a full-viewport .snap auto-upgrades to the iOS-proven pager (CSS mandatory snap breaks on iOS): the outer never free-scrolls — a spring (w=11, z=0.9, tuned and final) moves it between section tops — sections stay one viewport tall and scroll natively inside themselves, pulling past an edge pages to the neighbour with a elastic hint, a wheel edge-push advances once with a cooldown, and paging up lands at the previous section's bottom. After a transition lands, input is swallowed for a beat so residual momentum from the same swipe can never page twice. Gestures starting on .chart, input or textarea are ignored. The pager dispatches "facet:section" ({id, index}) on the .snap and exposes element.facetPager {go, toEl, index, settle}. data-pager="off" opts out; without JS, plain CSS snapping remains. Each paged section gets generous top/bottom padding (var(--space-section)) plus clearance (var(--nav-menu-clearance)) so its centered content never falls behind the fixed nav-menu on a short screen, and a hairline divider marks each boundary except the last — landing at the viewport's bottom edge as each section snaps (sections are 100dvh and border-box). ### View stack WHEN TO USE: an app with real screens and a working Back button — home, question, result. One per page, as the page's
. For a one-story scroll, use the snap shell instead. What it is: the app shell for apps with real screens — a
of .view sections where exactly one shows at a time, each scrolling on its own, content centered by auto margins when short. Navigation is plain hash links: any switches views, facet.views.go(id) and .back() do it from code, the URL always names the screen, and the browser's own Back walks the trail (back() falls home to the first view when there is nothing left to pop). Every view pads itself clear of the Dynamic Island (safe-area top) and of the home bar plus the nav menu (safe-area bottom + clearance); opt a view out with .bleed-top. What it is for: screen-to-screen apps — home → question → result → leaderboard — where the paged .snap shell (one long page you page through) is the wrong shape. When to use it: one view-stack per page, as the page's
, one .view per screen with a real id. facet.js upgrades it (.is-stacked, like .snap → .is-paged): html gets .is-app-shell, the page locks to the viewport, one view shows. With JS off the screens simply stack in reading order and the links jump between them — everything stays crawlable. Views keep their scroll positions while hidden, so Back returns exactly where the visitor left. Each arrival fires a bubbling "facet:view" CustomEvent ({view, from}) on the stack and moves focus to the view.
Play
### Button WHEN TO USE: any click that does something. At most one primary per screen; navigation dressed as a button is an . What it is: the action element, in three variants and three sizes, working identically on Variants: .btn-primary, .btn-secondary (default for bare .btn), .btn-ghost Shapes: .btn-pill rounds the button into a capsule. Because the base .btn is already an inline-flex row, an inline icon plus a text label sit side by side with no extra class — that is the icon-pill the nav menu is built from. Sizes: .btn-small, default, .btn-large States built in: hover, focus ring, pressed, :disabled ### Icon button WHEN TO USE: compact, secondary actions where an icon plus its tooltip is enough — close, clear, share. Always with an aria-label. What it is: a square button holding a single icon. What it is for: compact actions whose meaning the icon plus its tooltip carries — the clear cross in a number field, a close button, a share arrow. When to use it: when space is tight and the action is secondary. The icon alone says nothing, so every icon button carries an aria-label for screen readers and a data-tip tooltip for everyone else. Combines with any button variant and size. Icons are inline SVG with stroke="currentColor" so they take the button's text color in every theme. ### Number input WHEN TO USE: every numeric entry in a calculator or money product — it groups digits for the visitor's locale as they type and reads the number back in words. What it is: a labelled field wrapping the calculator-grade number input, with a clear button inside the field and a words helper under it. What it is for: money and other large numbers, entered the way the visitor says them. The input ships prefilled with an example value, clears fully on the first tap — and never again once the visitor has typed — groups digits for the locale as they are typed (25,00,000 where lakh and crore are the norm, 2,500,000 elsewhere), and reads the number back in words underneath, in the active language: 25 lakh, 2.5 million, 25 लाख. The locale follows facet.location and the browser, overridable with data-numbers="indian|western"; the language with data-language. All behaviours come from facet.js the moment the input carries data-number. When to use it: every numeric entry in a calculator. The field shows the grouped text; read the plain number in app code by stripping separators — parseFloat(input.value.replace(/[^0-9.]/g, "")). For the error state, set data-status="error" on the field and swap the hint for a .field-msg line (.field-invalid + .field-error stay as aliases). Monthly investment What you invest every month ### Slider WHEN TO USE: a bounded pick where the feel of more-and-less matters more than the exact digit — years, percentages. Pair it with the number input when precision matters. What it is: a real, themed range input — hairline track, accent-3 thumb — paired with a live value readout. What it is for: picking a number inside a known range where the feel of more-and-less matters more than exact digits: years, percentages, counts. When to use it: bounded inputs with sensible ends. When the exact number matters, pair it with or swap it for the number input. Put an in the field label and facet.js keeps it showing the live value. Full keyboard support comes free with the native element. ### Detailed slider WHEN TO USE: a range too wide for a plain slider to land exactly — a price, a duration in seconds, a token in pixels. Fast coarse drag plus precise fine entry, always in sync. What it is: a slider for a wide range, paired with a precise entry. The track drags fast in coarse jumps (data-coarse) so you cross a big span in one swipe; the −/+ steppers and the number box reach the exact value between the jumps (data-fine). All three stay in sync. What it is for: values with many gradations where you want both speed and precision — a price, a duration in seconds, a token in pixels. When to use it: ranges too wide for a plain slider to land precisely. For a small bounded range, the plain .slider is enough. facet.js wires it and fires a bubbling facet:slide {value} on any change.
### Choice grid WHEN TO USE: one exclusive pick from up to about ten compact, symbolic options — currencies, units, categories — too many for a segmented control, too visual for a select. What it is: a grid of small square choice buttons (.choice-grid of .choice-btn — a .choice-sym symbol above a .choice-cap caption), five per row on phones. aria-pressed carries the state and facet.js keeps the pick exclusive; on change the grid fires a bubbling CustomEvent "facet:choice" {value, button} (value = data-value ?? the button text), and facet.choiceSelect(grid, value) sets a pick from code (URL restore, reset). With JS off the buttons still render and submit nothing — pair with a hidden input when the form must post.
### Result block WHEN TO USE: whenever a calculation produces one headline answer — a verdict a person could repeat at dinner, then the number, then the supporting figures. What it is: a verdict-first result card: one plain-language sentence a person can repeat in conversation, one large number, then the supporting figures. What it is for: the centrepiece of a results screen — readable at a glance, and announced by screen readers on every recalculation when the block carries aria-live="polite". When to use it: whenever a calculation produces one headline answer. Leave out the figures list when there is nothing to support the verdict. For the empty state before any input, show a muted verdict line alone.

Your money halves in value every 10 years

Rs 46,319

Total invested
Rs 12,00,000
Real return
4.1% a year
### Description tooltip WHEN TO USE: on every interactive element — one data-tip attribute explains it in place. This is the library's everything-explained-in-place promise; write one for every control you ship. What it is: a one-attribute explanation bubble: put data-tip on any element and the text appears above it on hover and on keyboard focus. On touch screens a tap opens it instead — placed above, below or beside the element, wherever there is room, always inside the viewport, with a tail pointing at it — and it stays until the page scrolls, a tap lands anywhere else, or the same element is tapped again. A control that opens something else (a sheet, a menu, a dialog) acts without pinning a bubble over what it opened. A tablet's trackpad cursor shows it on hover too. The tap still performs the element's own action; the bubble explains, it never blocks. What it is for: explaining every interactive element in place — one of the library's core principles. The description doubles as documentation anyone can read by pointing at the thing. When to use it: every interactive element ships with one. Add data-tip-below on elements near the top of the viewport so the hover bubble drops under them instead of clipping off the screen. ## JavaScript API WHEN TO USE: the few moments an app really needs a call — everything else wires itself from data attributes in the markup. facet.js wires itself to data attributes on DOMContentLoaded; pages opt in by writing HTML, never JS. A tiny public API is exposed as window.facet for calculator apps: facet.set({...}) apply configuration live: theme, mode, density, language, or any --token override facet.version the library version string, e.g. "0.2.1" facet.scheme three-way appearance: .get() / .set("auto" |"light"|"dark") / .cycle(); auto follows the OS live, remembered in the URL + sessionStorage facet.location null until resolved, then { city, region, country, countryCode, latitude, longitude, source } from an IP lookup. OFF by default — opt in with data-location on the script tag (or data-location-endpoint for a custom one), so a page that needs no geo makes no network call. facet.locate() is the promise. Silent on failure; never throws. Precise GPS is not requested — that is the app's call on a real user gesture (navigator.geolocation). facet.groupNumber(n, {system}) grouped for the active locale, or force {system:"indian"|"western"} per call facet.numberWords(n, {system}) "7.43 crore" / "74.25 million" / "7.43 करोड़" per locale, with a per-call override facet.numberSystem() "indian" or "western", however resolved facet.choiceSelect(grid, value) set a .choice-grid pick; fires facet:choice facet.strings the translation table; extend in place facet.install() show the install prompt when available facet.go(url) navigate with the page transition facet.feedback the feedback engine (created at load): .tap/.tick/.snap/.success play sound + haptics; .sound.enabled / .haptic.enabled switch them facet.sheet(el, scrim) the settings-sheet engine; returns { open, close, toggle, isOpen } (wired sheets expose it as element.facetSheet) facet.scrollGauge(el) attach the slim scroll thumb to a scroller; returns { update } — call it after content changes facet.tabIndicator(bar) attach the sliding pill to a .tab-bar; returns { moveTo } facet.installNudge(el) the add-to-home-screen flow; returns { standalone, installed, addNow, never } facet.views the view-stack app shell, when the page has one: .go(id), .back(), .current — hash-routed, so plain
links navigate too facet.motion the ambient motion engine, one vector, two channels: .register(selector, {xMax, yMax}) moves elements (parallax), .register(selector, {light: true}) steers their --shine-x/--shine-y light (shine). Modes off | idle | responsive: .setMode(m) / .set(m, fromGesture) / .cycle() switch and persist them, .get() and .mode read, .label() gives the UI word (Off/Idle and the resolved Cursor/Tilt for responsive); every change fires facet:motion on document. Legacy "cursor"/"tilt" still accepted as aliases of responsive. register() and setMode() boot the engine on demand, and registering an element twice is safe (the second is ignored) facet.sliderScale(wrap) rebuild a .slider-wrap's tick scale after a range change facet.mapStyle() the active theme as a Google Maps styles array facet.toast(msg, kind) transient pill notice, gone in four seconds; kinds success|warning|error|info facet.chart(el, opts) draw the line chart: {points, projectFrom, events, format} facet.icons the glyph table; extend it, then re-run facet.icon() facet.icon() (re)fill every svg[data-icon] on the page facet.navMenu() (re)wire floating .nav-menu overlays facet.translate() (re)apply content-language variants after inserting markup ### Copy buttons WHEN TO USE: any button that copies text on click — a snippet, a share link, a result. One attribute, no JS to write. What it is: a button carrying data-copy="#id" copies that element's text to the clipboard on click and confirms with the library's own toast — never by relabelling the button, which would shift layout and lose the button's name to assistive tech mid-press. What it is for: "Copy" actions beside code blocks, generated links and results. When to use it: wherever a reader takes text away with one tap.
the text to take away
## Page transitions WHEN TO USE: multi-page sites and apps that should feel continuous — one attribute and every same-origin navigation animates. Put data-transition="page" on the html tag (or the facet.js script tag, or facet.set({transition: "page"})) and every same-origin navigation between Facet pages transitions: the old page eases up and away, the new one springs in per the motion mode, and the URL changes normally. A link with data-no-transition snaps out instead. facet.go(url) navigates programmatically with the same transition. Built on cross-document view transitions; browsers without support simply navigate. data-motion="off" and prefers-reduced-motion make pages snap. ## Make a page installable (PWA) — offline, install and updates WHEN TO USE: any page that should open from a home screen and work offline — three small files at the root and the library does the rest. Three small files at your project ROOT turn any Facet page into an installable, offline-capable app. Facet ships the caching engine (/lib/facet-sw.js); you provide these three because a manifest and a service worker must be served from your own site root: 1. sw.js — one line that switches Facet's engine on at your root: importScripts("https://facet-kappa.vercel.app/lib/facet-sw.js"); A service worker can only control pages at or below its own url, so this file must sit at the ROOT (not a subfolder). It is a pointer, not a copy — the strategy lives in the library and updates centrally. 2. manifest.json — the install "info card" (name, colors, icons). Copy /templates/manifest.json and edit name + colors to match your theme's --background. References icons at 192, 512, and a 512 maskable. 3. icons/ — the PNG app icons (192, 512, 512-maskable). A default Facet set ships at /icons/; replace them with your own. Then wire them on the page: put data-service-worker="/sw.js" on the facet.js script tag (this registers the worker with updateViaCache: "none", so the engine your one-line sw.js imports is re-checked on every load and engine updates reach installed apps without you versioning anything), and add plus the apple-mobile-web-app metas in the head — copy the exact head block from /templates/app.html. Install experience: desktop browsers that support installation show an install control in the address bar; Android offers Add-to-Home-Screen; iOS Safari installs via Share -> Add to Home Screen (iOS never auto-prompts). Give any button data-install and it appears only when the browser actually offers installation; facet.install() triggers the prompt in code. Caching (the caching law): pages and the unversioned /lib/ files are network-first — every refresh loads the current deploy, the cache answers only offline — while other static assets serve from cache instantly and revalidate behind. Updates ship by redeploying. Until v1 the library is always the latest from the web; versioned freezes start at /lib/v1/. ## Backgrounds WHEN TO USE: give one big quiet surface a material — a hero, a landing band, a section divider zone, an empty state, or the whole page of a document-like product. Use variant GRID when the product should feel precise, technical or editorial (notes, tools, calculators; the custom glyph makes it a brand mark); use variant FLUID when it should feel alive and expressive (landings, launch pages, playful apps). One background per surface, never nested and never behind dense data; lower --bg-strength before reaching for none. Very faint page and section backgrounds, in two variants that are built differently and tuned separately. Put one class or attribute on a section or body; content sits straight on top. Everything fades to nothing under prefers-contrast: more and never prints. VARIANT GRID — a repeating symbol on a grid: - The looks: .bg-grid (technical plus marks), .bg-dots (dot grid), .bg-circles (circle grid: outlined rings on the dots' half-cell rhythm, noticeably bigger than a dot; --bg-ring sets the ring diameter, default 15px), .bg-ruled (writing lines), .bg-graph (graph paper, deliberately the lightest) — or the custom grid: data-bg-glyph="✦" with any character or emoji, a facet icon name, or several tokens separated by spaces for an alternating grid; facet.js draws the tile in the pattern ink and redraws it live whenever the attribute, the theme, the mode or the element's style changes. facet.glyphBackground(el) draws a surface inserted after load. - The knobs, on any grid: --bg-tint (ink color, default the muted text token), --bg-strength (ink opacity, default 12% — whisper-quiet), --bg-cell (spacing, default 2.5rem). The custom grid adds data-bg-glyph-size (spacing override in px) and data-bg-glyph-scale (glyph size as a fraction of the cell, default 0.42). - The scatter: data-bg-scatter="✦ ❋" on .bg-grid, .bg-dots or .bg-circles replaces roughly one lattice point in ten with one of your characters (any character or emoji, or a facet icon name); data-bg-scatter-rate tunes the frequency — one character per N points, default 10 — and data-bg-scatter-scale multiplies the character size (0.1–2, default 1). The arrangement is seeded fresh on every page load — each visit scatters differently — but holds within the load, so themes and modes re-ink the same arrangement instead of reshuffling; facet.scatterBackground(el) draws a surface inserted after load. VARIANT FLUID — data-bg-fluid: a soft, slowly-breathing field of out-of-focus color pools and small drifting bubbles, drawn from the theme's accent and vivid tokens, so every theme and mode recolors it with no redraw. facet.js mounts three GPU-composited layers whose desynced clocks keep the field from visibly repeating; facet.fluidBackground(el) mounts a late-added surface. Still under data-motion="off" and prefers-reduced-motion; plain background with JS off. FIXED TO THE SCREEN — two opt-ins pin a background to the viewport: - .bg-fixed on any grid look makes it the page's one still backdrop —
as the body's first child, content floats over it with no wrapper, the pattern holds still while the page scrolls. It raises its own --bg-strength default to 22% (whole-page fixed use is where the 12% whisper disappears on dim phone panels); tune as usual. Never prints. - data-bg-fluid-fixed beside data-bg-fluid pins the POOLS to the screen while the surface scrolls over them like a window — a hero card gliding over a still wash. The surface clips its window with a clip-path rounded to the card radius; set --bg-fluid-radius to match a different rounding.

A quiet dotted surface

## Blocks Assemblies of the components, documented as copyable patterns in the Library's Blocks layer — a block is the arrangement, not a new thing to learn. The set: grid of cards; horizontal card row (.card-row: momentum side-scroll, per-card proximity snap — safe, the pager law is about full pages); promo rail (.promo-scroll of .promo-card links: full-bleed, tinted from the vivid palette, mandatory snap — full entry below); filterable card list (an input with data-filter="#target" live-hides non-matching children — the markup is the data); list cards; product grid; product detail; cart line items with a stepper and the result total; the side index panel (.side-index: the sticky grouped-links aside the docs themselves use); full top menu (.top-menu: wordmark, .top-menu-links, actions; on phones the header stays one line — wordmark left, a native details.top-menu-fold "Menu" right, whose summary takes .when-closed/.when-open labels so Menu becomes Close. Opening does not float a card: the page behind gradually darkens and blurs, the header's own surface extends downward — its bottom border rides the reveal — and the links arrive one by one, all progressive enhancement on a plain
. Row actions hide on phones, so author a copy of them inside the fold's links, the same way the links are authored twice. .when-closed/.when-open are generic: inside ANY details, .when-closed shows only while shut and .when-open only while open); sidebar navigation (sectioned nav-links); footer (link columns + legal row); hero (.hero: display type, one line, one primary); feature grid; pricing table (the recommended plan carries the page's one primary); FAQ list (named accordions); CTA band (.cta-band: ink-filled closing ask); logo row + testimonial (.pullquote + .avatar); data table with toolbar (search data-filter + chips + actions over .table); stat row (.stat KPI cards, tabular digits); app identity (.app-identity: centered icon, name, phrase, optional badge row; .app-identity-stats centers .stat cards under it — full entry below); settings section (check-rows with switches + fields); form section (grouped fields, one submit row); article set (header, prose on the narrow container, .media-figure with caption, .pullquote). ### Grid of cards WHEN TO USE: a set of same-shaped cards that should pack themselves — features, products, stats. The grid decides the column count; you never write a breakpoint. What it is: cards in the auto-fitting grid — as many columns as fit, one on phones, no breakpoints. What it is for: any set of same-shaped things. When to use it: the default way to lay out more than two cards. ### Horizontal card row WHEN TO USE: more cards than fit, kept on one line — recent items, suggestions — side-scrolling with momentum and a gentle per-card snap. What it is: cards that scroll sideways with momentum and a gentle per-card snap — proximity snap is safe here; the pager law is about full-page sections. What it is for: browsable shelves: recents, suggestions, categories. When to use it: when the list is long but the screen is not. ### Filterable card list WHEN TO USE: a long set the visitor narrows by typing or by tapping filter chips — the markup is the data; no app code. What it is: a search field over a list — the input carries data-filter pointing at the container and facet.js live-hides children that do not match. The markup is the data. What it is for: lists a reader scans by name: projects, contacts, settings. When to use it: from about ten items up. ### List cards WHEN TO USE: rows that are richer than a list but lighter than cards — each row a bordered card with a leading icon, text and a trailing action. What it is: stacked full-width rows — an outlined card per record with the fields in a row inside. What it is for: records that read left to right: name, meta, amount, action. When to use it: when each row carries more than a table cell should, but less than a full card grid deserves. ### Product grid WHEN TO USE: a storefront's shelf — image, name, price per card, the whole card one link. What it is: clickable cards of image, title, price, action — the whole card is the link, the action is the one button. What it is for: shop shelves and catalogues. When to use it: anything bought or picked. The image slot is a quiet background box until a real image fills it. ### Product detail WHEN TO USE: one product's page — gallery beside the facts, price, the one buy action. What it is: gallery beside the info panel — image column and a stack of name, price, chooser and the one primary action. What it is for: the page one product gets. When to use it: after a product grid click. On phones the columns stack. ### Cart line items WHEN TO USE: a cart or order summary — line items with steppers, then the total row; the result block carries the verdict. What it is: the order as honest rows — item, stepper for quantity, line total — with the totals block closing it. What it is for: carts and order summaries. When to use it: any purchase review. The verdict line (the grand total) is the biggest thing on it, per the result-block rule. ### Full top menu WHEN TO USE: the first row of a website or document — wordmark, links, one action; on phones it stays one line and the links open under a darkening blur. Pages, not installed apps (apps get the tab bar). What it is: the site header — wordmark, nav links, actions. Add .top-menu-sticky on a wrapper around the header to pin it to the viewport top as a translucent, blurred, full-bleed strip (the blur lives on the wrapper, so the strip goes opaque while the fold is open and the header never darkens). Fill the wordmark slot with a .brand-lockup to put a logo chip beside the name. Nav links may carry a leading icon — an inside the .nav-link sits beside the label on its own. On phones the header stays ONE line — brand left, an icon-only details fold right (summary.fold-trigger with a data-icon="menu"/"close" pair on .when-closed/.when-open). Tapping it grows the panel down from the header's bottom edge in one clean motion (about 0.4s) while the page behind fades and blurs; tapping the trigger again, tapping anywhere outside, or Escape shrinks it back (about 0.3s) and the scrim leaves with it — the scrim never outlives the fold. Interrupting mid-motion continues from the current height. The engine is facet.js progressive enhancement on a plain
: with JS off or reduced motion the fold opens and closes instantly, no scrim. Row actions (a direct-child button like Sign up) hide on phones — author a copy of them inside the fold's links, the same way the links themselves are authored twice. What it is for: every site's first row. When to use it: pages, not installed apps (apps get the tab bar). Resize this page to see the fold.
### Sidebar navigation WHEN TO USE: app and docs shells with more destinations than a top row holds — three sections or more; fewer fit the top menu. What it is: sectioned nav-link groups in an aside — exactly the pattern this page's own index uses. What it is for: app and docs shells with more destinations than a top row holds. When to use it: three sections or more; fewer fit the top menu. ### Footer WHEN TO USE: every page's last block — link columns and the small print. What it is: link columns over a legal row. What it is for: the bottom of every site page. When to use it: always — a page that ends abruptly reads as broken. ### Hero WHEN TO USE: the opening statement of a landing page — display type, one line under it, one primary action. What it is: the opening statement — display type, one line under it, one primary action (and at most one quiet second). What it is for: the top of a landing page. When to use it: once. A page has one hero the way a screen has one primary. ### Feature grid WHEN TO USE: the what-you-get section — three to six cards, each an icon, a phrase and a line. What it is: icon, claim, one sentence — repeated in the auto-fitting grid. What it is for: the "what you get" section of a landing page. When to use it: three to six features; more belong in docs. ### Pricing table WHEN TO USE: two or three plans side by side, one marked recommended; every plan's action is a real link. What it is: plan cards side by side — name, price, the differences as a short list, one action each; the recommended plan carries the page's one primary. What it is for: choosing a plan without a spreadsheet. When to use it: two or three plans. More than three is a decision problem, not a layout problem. ### FAQ list WHEN TO USE: the questions people actually ask, folded behind native details — no JS, printable, crawlable. What it is: accordions with a shared name — the browser keeps one answer open, no JS. What it is for: the questions people actually ask, at the bottom of a landing or checkout page. When to use it: five to ten questions; more means the page above failed to explain. ### CTA band WHEN TO USE: the last push before the footer — one line, one button, on the accent wash. What it is: the closing ask — an ink-filled strip that flips to the on-accent voice, one line and one button. What it is for: the end of a landing page, after the case is made. When to use it: once, last. ### Logo row & testimonial WHEN TO USE: borrowed trust — a muted logo row, or one strong quote with a name and face; never a carousel of both. What it is: the proof pair — a muted row of names that trust you, and one quote with a face and a name. What it is for: landing pages between the hero and the features. When to use it: only with real logos and real quotes; invented proof reads as invented. ### Data table with toolbar WHEN TO USE: a dashboard's working table — a toolbar of filters above, the themed table below, states from the grammar. What it is: search, filter chips and actions in a row above the data table — the search input filters rows live via data-filter. What it is for: the working screen of any records app. When to use it: whenever a table grows past one screenful. ### Stat row WHEN TO USE: the numbers that matter, first — three to five stats in a row, each a number over a caption. What it is: KPI cards — one number that matters with its label under it, tabular digits, in the grid. What it is for: the top of a dashboard. When to use it: three or four numbers; a wall of KPIs is a report, not a dashboard. ### Settings section WHEN TO USE: an app's settings screen — grouped rows of switches and pickers, each row a label with its control. What it is: labelled rows with their controls — switches, selects, steppers — in a quiet stack under a section caption. What it is for: preference screens. When to use it: every setting a product has, in one predictable shape. ### Form section WHEN TO USE: any real form — stacked labelled fields, hints under, one primary submit at the end. What it is: grouped fields with one submit row — a fieldset per group, the primary action last and alone. What it is for: anything the reader fills in. When to use it: always this shape; forms that scatter their buttons scatter their completion rates. ### Article header, prose, media WHEN TO USE: editorial pages — the header (kicker, title, byline, time), reading-measure prose, media figures with captions. What it is: the editorial set — a header of title, byline and meta; long-form prose that is just semantic tags on the narrow container (the base styles are the design); a media figure with its caption attached; and the pull quote for the sentence worth stealing. What it is for: articles, guides, changelogs. When to use it: any page someone reads top to bottom. ### Promo rail WHEN TO USE: featured content at the top of a launcher or home screen — two to six promos; a single promo is just a card. What it is: the promo rail — a full-bleed carousel of wide feature cards, each washed with a different vivid-palette tint, snapping card by card with the next card peeking in from the edge. What it is for: featured content at the top of a launcher or home screen: announcements, highlights, seasonal picks. When to use it: two to six promos; a single promo is just a card. Each card is one real — a .promo-kicker (small icon and muted name), a .promo-line in the heading face, and a .promo-cta with an arrow. The rail's negative side margins mirror the container's responsive padding exactly (space-stack, then space-page from 640px) — change one and you change both, or the page grows a sideways scrollbar. Tints cycle through the vivid palette by position, mixed into the surface so they stay soft in light and dark; the scrollbar hides and the whole card is the tap target. ### Side index WHEN TO USE: docs, references and settings hubs — one long or paged content column with a persistent map beside it. What it is: the index panel the library's own docs use — a sticky aside on a surface fill with a hairline and rounded corners, holding grouped link lists. Each .side-index-group opens with an uppercase name (make it an to link the group's own page); groups divide with hairlines; .side-index-sub labels caption runs of links; the current link marks itself with aria-current="page" or .is-current and reads as a filled pill. The panel pins to the viewport — exactly 100dvh minus the --space-pin inset top and bottom — and scrolls its own content, never the page. When the marked link changes, scroll the panel so that link sits at the panel's own middle (links near the ends settle as close as the panel allows) — the panel scrolls, never the page. What it is for: docs, references, settings hubs — any page with one long or paged content column and a persistent map beside it. When to use it: put the aside and the content in a two-column grid (the grid wrapper is the page's layout job); on narrow screens stack it or hide the groups and keep a search box. ### App grid WHEN TO USE: the front door of a suite of mini apps, a portal, a directory of tools — every tile one real link. What it is: a home-screen launcher — an auto-fill grid of app tiles, each tile one real link: a rounded-square icon, the app's name, a one-line caption, and an optional metric line. What it is for: the front door of a suite of mini apps, a portal, a directory of tools — many small destinations presented as equals. When to use it: when the destinations are apps rather than content — content belongs in the card grid or the list. The grid packs as many tile columns as fit, still two across on a phone, with no breakpoints written. A tile is an : the whole tile is the hit target, it fills on hover and carries the keyboard focus ring. Put an open count or any live number in .app-count — tabular digits so tiles line up. Reuses the neutral ramp and type tokens, so it restyles with every theme. ### App identity WHEN TO USE: once, at the top of an app's about, release-notes or install page — which app this is, at a glance. What it is: the identity header of an app's about page — a centered rounded-square icon, the app's name, a muted one-line phrase, and an optional badge row; .app-identity-stats centers a row of .stat blocks under it. What it is for: the App-Store-style front matter of an about, release-notes or install page — which app this is, at a glance, before any detail. When to use it: once, at the top of the page, with the name as the page's one h1. The icon wears a hairline border so light icons keep an edge on light surfaces. Pairs with the stat row — opens, version, size — numbers first, prose after.

Inflation calculator

Time-travel your money

In development

4,120OPENS
v0.9VERSION
## Themed map WHEN TO USE: a Google map that must look like the page — facet.mapStyle() returns the active theme as a Maps styles array, so the map re-skins when the theme flips. A Google Map styled with the active theme — ground, water, parks and roads derived from the base family, labels from the text family, boundaries from the quiet accent, recomputed live when the theme or mode switches. Bring your own key (the library never ships one): put data-map on a div, the key in data-map-key (or data-maps-key on the facet.js script tag), optional data-lat / data-lng / data-zoom. Without a key, or offline, the div stays a quiet themed placeholder — never a broken embed. facet.mapStyle() returns the styles array for anything else. Styles derive from tokens by formula, never hand-tuned per theme, so every future theme gets its map for free.
### Sound & haptics WHEN TO USE: any app-shaped product — leave it on and the components click, tick and snap by themselves; the settings sheet's switches are the user's mute. What it is: synthesized UI sound and vibration with no audio files — facet.feedback plays short Web Audio blips (tap, tick, snap, toggle, success) and fires navigator.vibrate haptics, both ON by default. What it is for: the physical feel of an app — a tick as a stepper bumps, a soft snap as a pager settles, a tap under every control. When to use it: any app-shaped product; leave it on and it wires itself to the components. There is almost no HTML to write: the user's switches are the settings sheet's Sounds and Haptics toggles (data-control="sounds" / "haptics", self-wired), and the components already call it. To fire it yourself: facet.feedback.tap() / .tick() / .snap() / .success(); mute a channel with facet.feedback.sound.enabled = false or .haptic.enabled = false. Reduced-motion and a muted switch silence it. A mute holds for the session (sessionStorage, like every setting): it sticks across page navigations and resets when the tab closes. ### Tab bar WHEN TO USE: the main navigation of an app-shaped page — a floating pill of three to five sections with the menu on its left and settings on its right; phones get it at the bottom, wide screens can move it to the top. What it is: the app's floating command bar, in the style of Apple's Photos pill. Three detached pieces sit in one row: a round .tab-menu button on the left that opens the navigation sheet from the LEFT edge, the long glass .tab-set pill of text segments with one raised highlight sliding behind the active one, and a round .tab-settings button on the right that opens the settings sheet from the RIGHT edge. Each piece is optional — write only the pill for a simple app, drop the menu or the settings button when there is nothing behind it; the markup IS the variant, nothing to configure. A segment is a
acme Start free
### Sheet & menu WHEN TO USE: the app's edge panels, each behind one button — settings on the right (the default), navigation on the left (.sheet-left). What it is: the app's edge panel — a slab floating clear of the screen over a dark scrim, sliding in as one rigid block on a spring that overshoots slightly and eases back. The right edge is the settings side (the default); .sheet-left is the same panel sliding from the LEFT edge — the navigation side — and opening either closes the other first — never two panels moving at once. Add .sheet-rise and on phones the panel rises out of the tab bar instead: the bar's own frosted glass, condensing into and rising out of its own corner button as a narrow card — the menu slimmer on the left, settings wider on the right — standing only as tall as its options need — capped at half the screen so nothing sits out of thumb reach, taller content scrolling inside — mostly opaque so the options read, options left-aligned under eyebrow group names, a slim position thumb riding the panel's OUTER edge (left on the left panel, so that corner reads as the page's corner), and a foot of small labeled pill actions that never scrolls away. On wide screens the same panel becomes a corner DROPDOWN instead: it hangs from the top bar with its outer edge lined up with the bar's own corner button — a slim panel for the menu on the left, a wider one for settings on the right — options left-aligned, in the same glass, and the page does not dim (a click anywhere still closes it). Inside, menu groups of thin rows (.menu-item under a .menu-label) and a foot row of square icon toggles (.menu-icon-btn) whose ON state stands raised while OFF sinks into a darker well. What it is for: navigation, actions and app-level controls behind one trigger — any button that aria-controls the sheet opens it; scrim tap and Esc close; the page behind locks still while a panel is open; the trigger's aria-expanded is managed, a floating trigger stays visible above the open sheet as its close toggle, and a sheet left open never survives the browser's back-forward cache — it closes itself on return. When to use it: every app-shaped product's settings. Press a row: it sinks INTO the panel with a true bevel, never a dark smear. State changes ease — toggles must never jump. ### Float button WHEN TO USE: the one floating control a screen needs — usually the menu or settings trigger the sheet wires itself to. What it is: a lone floating round button pinned to the bottom-left of the page — the tab bar's trailing icon without the bar. What it is for: pages that need the settings sheet (or any panel) but not section navigation: give it aria-controls pointing at a .sheet and facet.js wires the open/close, the aria-expanded state and the press feedback exactly like the tab bar's settings button. When to use it: whenever a page has one floating action; .float-btn-right pins it bottom-right instead. Always an aria-label and a data-tip: the icon alone says nothing. On a real page it floats fixed; here it sits in the flow — this one opens the sheet from the entry above. ### Nav menu WHEN TO USE: whole-site navigation on app-like pages and PWAs — the thumb-reachable Menu and Settings pills in a bottom corner. What it is: a floating navigation cluster pinned bottom-left — a "Menu" pill that opens a stack of page links, and a same-size round Settings button beside it that opens a stack of setting toggles. Both stacks rise above their own trigger; opening one closes the other, and neither trigger moves when a stack opens. Every link and the Menu pill are the base .btn.btn-pill (the Settings button is .btn-pill.btn-icon, canonical icon data-icon="sliders"); each setting is a .btn.btn-pill.nav-set whose on/off state shows in the pill itself: the pill stays solid in both states and only the trailing .menu-value chip changes (filled accent = on, outlined muted = off). What it is for: whole-site navigation on any Facet page, plus the app's own settings — theme, appearance, motion, sound, haptics, language — behind two thumb-reachable floating controls. When to use it: the primary nav for a content site or app. The Menu is a real
wrapping real links, so it works with JavaScript off — the summary opens the links inline — and the links sit in the DOM for crawlers and AI. Every item is one line, left-aligned, sized to its own content and capped near 300px; wrap the label in .nav-label and a long one truncates with an ellipsis while the icon and any trailing .menu-value stay whole. facet.js upgrades both stacks to a floating overlay: a dimming scrim, body scroll-lock, a staggered reveal, Escape to close, and focus moved into the open stack and back. The setting pills self-wire through data-control (theme cycles Default/Velvet/Aero/Elegant; appearance is the separate Light/Dark/Auto and the two compose) and data-language-cycle. Clicking a nav link or action closes the panel; a .nav-set toggle leaves it open so several settings can be changed in a row. Reduced motion drops the stagger. Add .nav-menu-right to anchor the cluster bottom-right with right-aligned stacks (left stays the default); an optional .nav-menu-back round pill — a real link with a chevron-left — sits on the cluster's outer edge, in the canonical order [back] [Menu] [settings]. On a real page it floats fixed bottom-left or bottom-right; here it sits in the flow. ### Scroll gauge WHEN TO USE: long scrollers inside app screens — a slim thumb that shows how much is left and drags to scrub. What it is: a pill thumb inset in an embossed groove beside a scroller — thumb height is the visible share of the content, position is progress, and overscroll momentum slides it out of the groove where the clip swallows it. What it is for: a themed, unobtrusive scrollbar look-alike for panels and whole pages; facet.scrollGauge(scroller) injects it, a metric option reports composite scrolls (the pager's sections laid end to end), and onScrub makes the lane a drag handle; risen sheet panels (.sheet-rise) inject their own automatically, riding the panel's outer edge. When to use it: sheets and app pages where the native scrollbar is hidden. Auto-hides when nothing overflows. Scroll the box: ### Installable (PWA) WHEN TO USE: any page that should install to a home screen — three small files at the root and the library's caching engine do the rest. What it is: the setup that turns any Facet page into an installable, offline-capable app. What it is for: products you want on a phone's home screen — opening full-screen, working offline. When to use it: any app-shaped page. Facet ships the caching engine (/lib/facet-sw.js) plus a working reference set — /sw.js, /manifest.json and /icons/ — that you copy into your own project root. The full step-by-step recipe lives in llms.txt; the app template ships the exact head block below. ## App-kit self-wiring (apps are markup + data only) WHEN TO USE: any app-shaped page — write the markup, the library wires the behaviour; there is no per-app glue code to write. The app kit wires the whole app shell from markup, so a calculator is markup + its own data/logic with no library-shaped glue: Settings controls: any element carrying data-control self-wires (a .menu-icon-btn, a .menu-item row, or a .nav-set pill) — "sounds" and "haptics" flip facet.feedback.sound/haptic.enabled (the haptics row hides itself where the Vibration API doesn't exist, e.g. iPhones), "motion" cycles facet.motion Off → Idle → Responsive and shows the resolved flavour word (Cursor or Tilt), repainting live when the mode changes anywhere else, "appearance" cycles facet.scheme (Light/Dark/Auto), and "theme" cycles the shipped themes (Default/Velvet/ Aero/Elegant). Each writes its .menu-value word and dims via data-off. Zero app JS. Appearance is three-way (facet.scheme): auto follows the OS live, light/dark are held, remembered in the URL and sessionStorage — every setting is session-scoped: it holds across pages, resets with the tab. data-mode-toggle still flips light/dark for back-compat. Install nudge: put a .nudge-scrim (and optional .overlay-guide) on the page and the whole Add-to-Home-Screen flow self-wires — data-nudge-key, data-nudge-delay, and [data-nudge="add"|"later"|"never"] buttons; add runs addNow() and reveals the guide when iOS needs a how-to; an "Add to Home Screen" menu row ([data-nudge="add"]) opens the same guide. No app JS. Sheet menu rows navigate: a .menu-item[data-section] pages the snap pager (snap.facetPager.toEl) and closes its sheet. The sheet instance is exposed as sheetEl.facetSheet ({open,close,toggle,isOpen}), like snap.facetPager. Choice grid emits: on change a .choice-grid fires a bubbling CustomEvent "facet:choice" {value, button} (value = data-value ?? text); facet.choiceSelect(grid, value) sets a pick programmatically (URL restore, reset). Page gauge: a .snap[data-gauge] mounts a draggable whole-page .scroll-gauge-page whose thumb reflects true position across all sections; drag scrolls the page. Pager landing accounts for a section's top border, so paging lands exactly on the content top with a 1px divider present. Chart: facet.chart staggers colliding event labels across two rows in the top band and draws a faint "projection" divider where projectFrom begins. Icons added for app UIs: chart, grid, sliders, refresh, undo, volume, vibrate, motion, contrast, plus-square, download-cloud, and the icon badge's status glyphs sparkle and wrench, and the faceted gem (48 -> 62). Tooltip on touch: the [data-tip] bubble gains a triangle tail, and on touch screens a tap opens a floating bubble instead — placed above, below or beside the element by available room, staying until the page scrolls, a tap lands elsewhere, or the same element is tapped again (a control that opens a sheet, menu or dialog acts without pinning a bubble over it) — automatic, nothing to wire. Pinch zoom is blocked at the gesture level. Choice grid: a grid of small square choice buttons (.choice-grid of .choice-btn — a .choice-sym above a .choice-cap), five per row on phones. For one exclusive pick from up to about ten options where the choices are compact and symbolic — currencies, units, categories; aria-pressed carries the state and facet.js keeps the pick exclusive. Use it for any single pick too wide for a segmented control. Slider tick scale: wrap a .slider and a .slider-scale in a .slider-wrap; facet.js builds minor ticks (data-minor, default 10), sparse labels (data-label, default 40) and a highlighted data-now value from the slider's own min/max. facet.sliderScale(wrap) rebuilds after a range change. Install nudge card (.nudge-scrim > .nudge-card) and the full-screen instruction overlay (.overlay-guide): the add-to-home-screen prompt and the dark how-to screen, driven by facet.installNudge — addNow() returns "native" (Android prompt shown) or "guide" (show the overlay). The overlay's dark palette is deliberately fixed across themes. Nav menu (.nav-menu): a floating navigation cluster pinned bottom-left — a "Menu" pill that opens a stack of page links, and a same-size round Settings button beside it that opens a stack of setting toggles. Both stacks rise ABOVE their own trigger; opening one closes the other, and neither trigger moves when a stack opens (the stacks are out of flow). Every link and the Menu pill are the base .btn.btn-pill (the Settings button is .btn-pill.btn-icon, canonical icon data-icon="sliders"); each setting is a .btn.btn-pill.nav-set whose on/off state shows in the pill itself: the pill stays solid in both states and only the trailing .menu-value chip changes (filled accent = on, outlined muted = off, keyed on aria-pressed). The Menu is a real
wrapping real links, so it works with JavaScript off (the summary opens the links inline) and the links sit in the DOM for crawlers and AI; facet.js (initNavMenu, re-runnable via facet.navMenu) upgrades both stacks to a floating overlay — a dimming scrim, body scroll-lock, a staggered bottom-up reveal, Escape to close, and focus moved into the open stack and back. Every item is one line, left-aligned, sized to its own content and capped near 300px (never wider than the viewport minus the menu's side margins); wrap the label in .nav-label and a long one truncates with an ellipsis while the leading icon and any trailing .menu-value stay whole. Clicking a nav link or an action button closes the panel; a .nav-set toggle leaves it open so several settings can be changed in a row (scrim tap and Escape still close). The setting pills self-wire through data-control (theme/appearance/motion/sounds/haptics — theme cycles Default/Velvet/Aero/Elegant and composes with the separate appearance Light/Dark/Auto) and data-language-cycle, exactly like the settings sheet. Reduced motion drops the stagger. It is an overlay control, not content: author it at the end of , it is user-select:none, and it prints off. Variants and slots: .nav-menu-right anchors the whole cluster bottom-right (safe-area aware) and every stack opens right-aligned; left stays the default. An optional .nav-menu-back round pill — a real with a chevron-left — sits on the cluster's outer edge, in the canonical order [back] [Menu] [settings]. Components: .tab-bar (bottom pill of .tab-seg segments + .tab-divider + trailing .tab-settings button; a .tab-indicator pill is injected and moves on per-edge analytic springs), .sheet-scrim + .sheet (right-edge settings panel on a rigid spring; scrim tap and Esc close), .menu-item/.menu-label/.menu-icon-row/.menu-icon-btn (thin menu rows and square icon toggles), .scroll-gauge (floating scrollbar look-alike; .scroll-gauge-page is the draggable whole-page lane), and .float-btn — a lone floating round trigger pinned bottom-left (.float-btn-right for bottom-right): give it aria-controls pointing at a .sheet and it opens it, aria-expanded managed, press feedback included, and .nav-menu (the floating Menu-pill + Settings-button cluster above). Markup contracts: the wall entries on the Library page — the copy button under each demo gives the exact HTML. Modules on window.facet: feedback (synthesized UI sounds + haptics, ON by default; feedback.sound.enabled / feedback.haptic.enabled are the switches), sheet(el, scrim, {onChange}), scrollGauge(scroller, {...}), tabIndicator(bar), installNudge(el, {...}), motion (the motion engine behind parallax and shine: register/init/setMode/cycle, modes off|cursor|tilt|idle, reduced-motion off). Auto-wiring: .tab-bar finds the page's pager and its sheet; any button[aria-controls] on a .sheet opens it; a .nav-menu upgrades its
into the floating overlay (facet.navMenu re-runs it). Velvet law: elements with velvet lift/press transforms must not register with facet.motion's move channel (the light channel — shine — is exempt: it writes only custom properties, never transform). ## Templates WHEN TO USE: starting any new page — pick the shape that matches the product and edit the words; never start from an empty file. Whole pages ready to rename, all consuming the library by URL and all carrying the SEO head pack (title, description, OG tags, canonical, favicon slot). On /library.html the three flagship shapes (landing, saas, social) render live in an iframe device preview you can flip between desktop, tablet and phone widths to see the genuine responsive layout at each breakpoint: /templates/landing.html — marketing page assembled from the block layer: top menu, hero, social proof, features, pricing, FAQ, CTA band, footer; JSON-LD organization slot. /templates/saas.html — SaaS analytics dashboard (Northstar): folding sidebar, top bar with search, four-KPI stat row, twelve-point revenue chart, filterable signups table beside an activity panel. /templates/social.html — social app (Ripple): three-column feed on desktop that folds to one column with the app-kit bottom tab bar on phones; compose box, feed of post cards, trends and who-to-follow, and the settings sheet behind the tab bar's settings button. The feed is live: likes, comments and view counts are shared by every visitor, stored in a small Supabase backend the page calls directly (the template's script shows the pattern — app logic lives in the project, never in the library); offline, the static numbers stand. /templates/app.html — app shell (sidebar + top bar) with a working dashboard: stat row, live chart, data table with toolbar; installable: manifest link, apple metas, the one-line sw stub wired. /templates/article.html — editorial page: header, prose on the narrow container, media figure, pull quote; JSON-LD article slot. /templates/deck.html — 1920x1080 pitch deck in five layouts (title, content, two-column, chart, closing); arrow keys / space to present, F for fullscreen, scales to any screen; print to PDF gives one slide per page at exact size. /templates/document.html — A4 pages: letterhead, invoice (table + result total), one-pager; true print margins, one .page per sheet. /templates/card.html — 3.5x2in business card, front and back, plus a print sheet: five duplex-aligned pairs per A4 at exact size. /templates/manifest.json — the PWA manifest template with the icon checklist. robots.txt and sitemap.xml ship at the site root as the copyable pattern. ### Landing page WHEN TO USE: start a marketing site here — hero, proof, features, pricing, FAQ, CTA, footer, assembled from the block layer. Rename and fill. A marketing site from the block layer: top menu, hero, social proof, feature grid, pricing, FAQ, CTA band and footer. Swap the words, keep the shape. ### SaaS dashboard WHEN TO USE: start a dashboard product here — sidebar shell, stat row, chart, working table. An analytics product — Northstar — with a folding sidebar, a top bar, a KPI stat row, a live chart, and a filterable table beside an activity panel. The everyday shape of a web app. ### Social app WHEN TO USE: start a feed-shaped app here — three columns on desktop, the tab bar taking over on phones. A three-column feed — Ripple — that folds to one column with a bottom tab bar on phones, showing off the app-kit tab bar and the settings sheet. Compose box, a live feed, trends and who-to-follow. ## Build advice — guidance, not components Some patterns matter but are too thin or context-bound to wrap as a component: a few imperative lines beat a class. Those ship here as guidance entries — read them like instructions. If advice is not written down here, do not assume it; if a pattern below repeats enough to earn markup and behaviour, it gets promoted to a real component. Reserved class names: every class facet.css ships is reserved — the short generic ones are the silent-collision traps: .card, .stack, .row, .grid, .list, .chip, .badge, .avatar, .field, .fold, .snap, .empty, .shine, .glyph (the inline-svg fix). Before inventing a class in your own project, search this file for the name; if it exists, pick another or prefix yours. Choosing a shell (one per page): - A document, a marketing page, an article: no shell. Normal flow, normal scrolling, a .container for width. - One story told one screen at a time (a calculator: intro → inputs → results):
— the visitor pages through a single flow, top to bottom, and every screen is part of one narrative. - Real screen-to-screen navigation (an app: home → question → result → leaderboard, with back):
— one screen visible, hash-routed, the browser's Back works. Never nest one shell inside another. Choosing navigation (one primary navigation per page): the four navigation pieces are different components on purpose — they share the same pill and token primitives and the same data-control self-wiring, but their markup, position and behaviour differ, so they are not variants of each other. Pick by the page's shape: - A website or document (landing, article, docs): the .top-menu block — the classic in-flow header, wordmark left, links right, folding to a disclosure on phones. Works with no JS. - An app-like page or installed PWA: the floating .nav-menu cluster — thumb-reachable Menu and Settings pills in a bottom corner, stacks opening upward as an overlay. - An app with three to five top-level sections: the .tab-bar, with the settings sheet on its trailing icon. - The .sheet is not navigation itself: it is the right-edge panel any trigger opens — the home of settings rows (data-control) wherever a tab bar or a custom button wants them. Never stack two primary navigations on one page (a .top-menu plus a .nav-menu): pick the one that matches the page's shape and let it carry the settings too. Safe areas (standalone PWAs, black-translucent status bar): - Ship the app-shell metas: viewport-fit=cover on the viewport meta, apple-mobile-web-app-capable and black-translucent status bar (the PWA section below has the exact head block). The page then runs under the Dynamic Island and home bar, so padding must handle both. - Facet surfaces already do: they pad with --safe-top / --safe-bottom. When you build your own full-viewport or fixed surface, pad it the same way: padding-top: calc(your-padding + var(--safe-top)) and padding-bottom: calc(your-padding + var(--safe-bottom)); add var(--nav-menu-clearance) to the bottom when a fixed nav menu or tab bar is on the page. - Bleeding under the island is a deliberate choice, never a default: put .bleed-top on the one surface that wants it (a full-bleed map, an immersive hero) and keep its controls padded clear. Standalone PWA head pack (copy this whole set together — each line fixes a real shipped bug): - — cover is what lets safe-area insets exist. - and — the installed app runs edge to edge. - manifest theme_color/background_color must match the shipped theme, or the installed app's chrome flashes the wrong color. - The full install recipe (manifest, sw.js, icons) is in the PWA section below; this pack is the part pages get wrong on their own. Text inputs on iOS: - Keep input font-size at 16px or larger or iOS zooms the page on focus (Facet's base inputs already are; do not shrink them). - Say what the keyboard should be: inputmode="decimal" for numbers, "email"/"url"/"search" where true, autocapitalize="none" for usernames and codes, enterkeyhint for the return key's word. - Never rely on placeholder as the label; the label element is what readers and autofill use. Where app state lives (URL first): - Any state a person could want to share, restore or crawl belongs in the URL: inputs read and write the query string, screens ride the hash (the view stack does this for you), and facet.js already carries theme/mode/language there. An agent should be able to construct any state from a link alone. - sessionStorage is for device-private preferences that make no sense in a link (sound off, a dismissed panel) — session-scoped, so nothing follows a reader forever; every setting facet.js stores lives there. Reserve localStorage for durable device facts (the installed-app stamp). Never put content or answers in either store. ## Rules for building on Facet Core principles: readability is the product. One-glance HTML. Not one extra wrapper. Least code that stays readable. Pure HTML, CSS and JS, no build step, never minified. One-step theme change. Everything explained in place. Fully commented. Accessible, SEO-ready and AI-crawlable by default. Markup rules: semantic tags over div (header, nav, main, section, article, aside, footer, figure). The wrapper law: an element exists only to hold content, create layout, or carry behavior; anything else is deleted. Container inside a container only to enable a real feature: scroll areas, sticky regions, overflow clipping. Buttons are button, links are a, form fields have real label elements. One h1 per page; heading levels never skip. Adding a new component, the compliance checklist: semantic tokens only. Works in every theme, light and dark, with zero extra code. All states covered. Keyboard operable with a visible focus ring. Interactive elements ship with a description tooltip. 44px touch targets, no hover-only interactions. Minimal semantic markup. Fully commented in the file. Names follow the naming rules, nothing minified. data-event analytics hook on the key action. Wall entry on library.html (the Library page): live demo, variant and state chips, exact snippet with copy. Docs description — what it is, what it is for, when to use it — same text as the file comment and this file. Committed to Git with a clear message. Analytics: no vendor is baked in. Every key action carries a stable, documented data-event name — the same hooks agents target. Five lines wire any tool to all of them at once: Naming: tokens by role, not value. One class prefix and pattern per component (.btn, .btn-primary). JS: one small named function per behavior. Maintaining: docs are demos — a component is not done until the Library wall (library.html) shows it live with its code. One component, one commented section in the CSS. All changes go through Git commits. Growth by extraction: build a new pattern inside a project first, promote it once it repeats. App logic, data fetching and state management live in projects, never in the library. This file is updated whenever a component or rule changes. Distribution: consume by URL only. Releases are Git tags. When a project ships, freeze a copy of the current files to /lib/v1/ and leave them untouched; work continues in /lib/. Old projects point at their frozen folder forever. ## iOS rules — fixes for real iPhone bugs These are fixes for specific bugs that show up on iPhone (mostly mobile Safari) and not on desktop. The library already obeys every one, so following them costs nothing — they are written down so nobody hits the same bug the hard way. Pager law: full-page snap sections that can outgrow the viewport are never built with CSS scroll-snap — mandatory re-snaps a tall section back to its top after a flick and blocks the next gesture, proximity behaves like no snap, and a free-scrolling outer with a JS settle-snap is imprecise and works against momentum. The model that works is the pager .snap ships: the outer never free-scrolls, JS springs its scrollTop between section tops, each section is 100dvh and scrolls natively inside, and gestures hand off at section edges with an elastic hint. Gesture law: tame iOS with touch-action — pan-y on scrollers (vertical scroll only, also kills double-tap zoom and pinch), manipulation on fixed interface controls, none on drag surfaces such as scrubbing charts and the page gauge. Pair with a gesturestart preventDefault for older iOS pinch (facet.js does this globally). Never rely on viewport user-scalable=no or maximum-scale; iOS ignores both. App shell law: for a standalone PWA, viewport-fit=cover alone is not enough on iOS — without apple-mobile-web-app-capable and apple-mobile-web-app-status-bar-style set to black-translucent, the installed app gets an opaque strip cutting the page at the status bar. Every app page carries both metas, keeps safe-area env() insets in its section padding, and the manifest's background_color and theme_color match the shipped theme. Caching law: HTML documents and, until versioned freezes exist, the /lib/ files themselves are never cache-first — every refresh loads from the network and caches answer only offline. A worker that serves pages from cache and revalidates behind makes every deploy invisible until a second refresh; that bug shipped once and is now law. facet-sw.js implements this: network-first for pages and /lib/, cache-first with background revalidation for other static assets. Parallax exclusion: elements carrying their own transform physics — velvet lift and press, the pager's elastic translate — never register with facetMotion. Two writers on one inline transform collide, and the losing one flickers. Font-list law: never put inherit inside a font-family list (ui-rounded, "SF Pro Rounded", inherit is silently invalid and drops the whole declaration). A stack ends in a generic family, always. ## iOS, honestly: what a website can and cannot do What a website CAN do on iOS: tint Safari's UI with theme-color (facet.js keeps the meta synced to the active theme's background, live); ship apple-touch-icon and install standalone (manifest + the apple metas from the app shell law); send web push and set an app badge — but only after the visitor adds the app to their home screen (iOS 16.4+), never from a plain Safari tab, and only from a user gesture. What it CANNOT do: see or join Screen Time, read a visitor's age, or apply parental controls — OS territory; do not design around it. Child safety has no web API: the real levers are self-labeling (adult content declares itself with the RTA rating meta, , so OS filters catch it; family content states isFamilyFriendly in JSON-LD) and restraint — nothing Facet ships autoplays, hooks, or dark-patterns anyone, children included. The head pack every Facet app page carries: