# 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 as a placement model, base to top: the base family plus accent-3 (the BASE layer — washes, pills, the selected tab, focus rings and text highlights), accent-2 (the MIDDLE layer — eyebrows, callouts, captions) and accent-1 (the TOP layer — main headings, links, the one primary action and input check marks), each with hover, pressed, on-colors and derived -tint / -text / -wash 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 four styles (primary, secondary, ghost, danger) and three sizes working on
### Fields: input, textarea, select, search WHEN TO USE: any time a control needs a name — which is always. One pattern wraps every kind of control. What it is: the labelled field family — one pattern (label.field > .field-label + control + .field-hint) wrapped around every kind of control: text input, textarea, select (with a token-colored chevron, no image files), and a capsule search field. What it is for: every form. The label is real, so clicking it focuses the control and readers announce it; for validation set data-status="error|warning|success" on the field and swap the hint for a .field-msg line (.field-invalid + .field-error stay as aliases — see Component states). The input's type sets its mobile keyboard, autofill and native check (email, tel, url, number, password); inputmode fine-tunes the keyboard without changing the type (numeric on a text field for a one-time code) and autocomplete names the autofill (email, tel, one-time-code, current-password). A password field takes data-reveal for a show/hide eye; any text field takes data-field-actions for an in-field Clear and a Copy / Paste / Select all / Undo / Redo menu (see Field actions). When to use it: any time a control needs a name — which is always. ### Field actions WHEN TO USE: a field a reader edits by hand and often copies, pastes into, clears or fixes — an API key, a share link, a coupon, a search. Opt in per field; a plain form field does not need it. What it is: an opt-in pair of affordances tucked at a field's trailing edge — a Clear × that wipes the value in one tap, and a ⋯ menu of Copy (the value to the clipboard), Paste (the clipboard into the field at the cursor), Select all, and Undo / Redo (stepping through the field's history). What it is for: fields worth acting on by hand. When to use it: add data-field-actions to the .field and facet.js wraps the control and wires everything — no per-field JS, no markup for the buttons. Each action gives feedback and a data-event hook, the menu is a real dropdown and the × a real button with an aria-label, all keyboard operable. The Clear hides itself while the field is empty. ### File upload & date input WHEN TO USE: always these before any custom uploader or calendar — native means drag-drop, keyboard, the picker and the form value all work for free, in every theme. What it is: the native file and date controls, themed and kept native — the file input's button is styled as a secondary action while drag-drop, keyboard and the form value stay the browser's (add multiple for many files, accept to filter, capture to open the camera on a phone); and the whole date-and-time family — date, time, month, week and datetime-local — each keeping its native picker with the accent coloring the highlight and color-scheme flipping it in dark mode. When to use it: always these before any custom uploader or calendar, wrapped in the same label.field pattern as every other control. ### Card WHEN TO USE: one thing per card — a product, a stat, a setting group. A clickable card is one
and carries at most that one action; specialised jobs (feature, option, inline) are the same primitive with one modifier. What it is: the grouped-surface primitive — a padded, rounded panel one step above the page. What it is for: one thing per card: a product, a stat, a setting group, a summary. When to use it: .card for most grouping; .card-outlined when many cards sit close and shadows would pile up; a clickable card is an — the whole surface is the link, it lifts on hover and presses on click. Never put more than one primary action inside a card that is itself clickable. Variants, each one modifier on .card: .card-feature displays a feature (a .card-feature-icon badge, a .card-feature-title, a .card-feature-text); .card-option is a selectable choice for quizzes and pickers — a real Open the report →
Card as a stack

Children spaced by the stack's gap.

Fast by default

No runtime between you and the platform.

Ada Lovelace Signed up today
### Feed post WHEN TO USE: an authored social post in a feed — your own updates, shares and short articles, stacked newest first. One post per card; not a comment thread. What it is: a card for one authored post, built on .card. Its slots, top to bottom: a .feed-author row (a .feed-avatar, a .feed-author-meta with the .feed-author-name and a kind label, and a .feed-date pushed to the end); a .feed-post-body of text (.feed-comment); an optional media block — a plain .feed-media image, or a .feed-embed / .feed-link-preview that shows a thumbnail with a play glyph over a source and title; a row of #hashtag .feed-tags; and a .feed-actions bar of .feed-act buttons (like, comment) with a passive .feed-act-static count pushed to the end. The whole card can be one link: a stretched .feed-post-link overlay opens the post while the tags, actions and any embed link stay clickable above it (they carry z-index over the overlay). What it is for: a personal or product feed of authored posts. When to use it: stack them in a column; a post is any subset of the slots. The action buttons are presentational — wire likes and comments in your project (the library keeps that state out on purpose); each carries a data-event hook.
Ada Lovelace Posted
6 Jul · 8:53 pm

The note itself…

youtube.comThe title
#AI
1.2k
### Entry WHEN TO USE: a dated listing entry — a job or degree on a résumé, a release in a changelog, a milestone on a roadmap, an award. Stack several under a heading. What it is: one dated entry — an .entry-head row (an .entry-logo holding an emoji or a logo image, a strong title, and an .entry-date pushed to the far end), an optional .entry-org line (company, place, tags), a summary paragraph, and an optional bullet list. What it is for: work history, education, changelogs, timelines and awards — any list of dated items with a source and details. When to use it: stack them in a .stack; every part is optional, so a bare entry is just a head and a line.
Staff Engineer

Orbital Systems · Remote

Led the ground-station rewrite and mentored the platform team.

Use an image logo instead of an emoji with . Drop the .entry-date, the .entry-org, the paragraph or the list freely — any subset is valid. ### Testimonial WHEN TO USE: one quote of praise with its author — a customer, a colleague, a review. A few in a row or grid; never invented. What it is: a quote card — a .testimonial-head (an .testimonial-avatar with an image or initials, a .testimonial-who with the .testimonial-name and .testimonial-role, and a decorative .testimonial-mark quote glyph pushed to the end) over the .testimonial-quote itself, set in the quote face. What it is for: borrowed trust on landing and about pages. When to use it: with real quotes and real people, a few in a card row or grid — never a carousel of invented praise.
AL Ada Lovelace CTO, Analytical Engines
It shipped in a weekend and looked designed. Nothing else comes close.
Swap the initials for a photo with . Place a few in a .card-row or a .grid. ### List WHEN TO USE: any repeated row of items — settings, menus, feeds, search results. It is ONE component: every look is a composition of its slots, never a sibling component. What it is: a list of rows — each with an optional leading icon or avatar (.list-icon), a title (.list-title), an optional description line under it (.list-desc), and an optional trailing value or action (.list-trail). What it is for: settings, menus, feeds, search results, contact and file lists — any repeated row of items. When to use it: .list for rows on the page separated by hairlines; wrap the same markup in .list-boxed for a bordered card list. A row is one line (icon + title + trailing) or two (add .list-desc, which top-aligns the icon on its own). Make a row actionable by writing it as or ### Modal WHEN TO USE: something that must be dealt with before the page continues — a destructive confirm, an alert to acknowledge, a short form, or a panel of content. Built on , so focus trapping, Esc and the inert page behind come free. What it is: a centered card on the platform's own , composed from three slots — .dialog-head (an optional .dialog-icon status chip, the title, an optional close button), .dialog-body (the ONE region that scrolls), and .dialog-actions (the footer buttons). A short modal needs only head and actions and stays a simple padded card; add a .dialog-body and it becomes a head / scrolling-body / actions column with the head and actions held still and hairlines marking them off. Three sizes: .modal-sm (a terse confirm), the default (a form), .modal-lg (content). The title wears the interface voice, not the display serif. What it is for: one card, four jobs — a destructive confirm (the danger action is .btn-danger, there is no close button since Cancel is the way out, and Cancel carries autofocus so Enter is safe); an alert (a .dialog-icon with data-status colours the icon, one action to acknowledge); a form (put autofocus on the first field, keep the corner close); and scrolling content (a .dialog-body between a sticky head and footer). When to use it: open it from a button whose aria-controls names the dialog (facet.js calls showModal and moves focus into the dialog — the dialog itself unless you set autofocus, so nothing is falsely ring-highlighted); close it with a form method="dialog" button, Esc, or a backdrop click. On phones the actions go full width and stack. The app-kit .sheet is the settings panel for app pages; the modal is the document-world decision.

Delete this report?

This cannot be undone.

Terms of service

Long content scrolls here…

### Drawer WHEN TO USE: a side task that keeps the page's context in view — filters, details, a navigation list. One panel, any edge. What it is: a that slides in from an edge; the direction is a variant — the default right, .drawer-left, .drawer-top or .drawer-bottom. The side drawers stand full height; top and bottom are full-width strips capped at 85% of the height; all of them scroll their own content when it overflows. What it is for: filters, details and navigation that sit beside the work instead of on top of it. When to use it: same open and close as the modal — a button's aria-controls names it (facet.js calls showModal), Esc, backdrop and form method="dialog" all close it; the optional .dialog-head and .dialog-actions give it the same tidy header and footer, and in a column drawer the actions sink to the bottom edge. The document-world twin of the app-kit .sheet.

Filters

### Popover WHEN TO USE: a small anchored bite — a hint, a definition, a short menu — that should close the moment attention moves on. What it is: a [popover] element toggled straight from a button's popovertarget — the platform's top layer, no JS to open. What it is for: small floating bits attached to a control: a hint, a note, a few actions. When to use it: point a button's popovertarget at the element's id and it toggles, no wiring. It light-dismisses on a click outside or Esc, and facet.js also closes it the moment the page scrolls, so a popover never floats detached from the thing it explains. For actions that change the page rather than explain it, reach for the dropdown menu instead.
Quick note

Light-dismisses on any outside click, Esc, or a scroll.

### Accordion, toast, dropdown menu WHEN TO USE: the accordion folds long optional reading; the toast confirms an outcome that needs no reply; the dropdown holds a few quiet actions behind one button. What it is: the accordion is native
styled (hairline rows, rotating chevron; give siblings the same name attribute and the browser keeps exactly one open, and add open to start one expanded); the toast is facet.toast(message, kind) — a transient pill that says one thing and leaves after four seconds, announced politely (kinds success / warning / error / info color the dot); the dropdown is details.dropdown whose summary is styled as a button and whose .dropdown-menu holds real buttons and links — each can lead with an svg icon, an aria-disabled item greys out, and .dropdown-danger marks a destructive one — closed by outside clicks, Esc, or picking an item. When to use it: never a toast for a question (dialog) and never a dropdown for navigation (links).
How is the rate calculated?

From the last twelve months.

facet.toast("Report saved", "success") ### Tabs, breadcrumb, pagination WHEN TO USE: tabs switch views in place; the breadcrumb says where you are in a hierarchy; pagination walks a long set page by page. What it is: the wayfinding trio. Tabs are real ARIA tabs (role= tablist/tab/tabpanel; click or arrow keys; accent underline; facet.js wires panels and roving tabindex) for two-to-five in-page views. Breadcrumb is the path to here — nav.breadcrumb > ol of links with quiet separators, current page unlinked with aria-current="page". Pagination is numbered page links in 44px squares, current page filled in accent-1, edges disabled with aria-disabled on a span. When to use which: tabs switch views in place; the tab bar navigates an app; the segmented control is a form value; breadcrumb and pagination are links, never buttons.
### Nav link WHEN TO USE: the one style for links inside navs, sidebars and rails; aria-current="page" marks where you are. What it is: the navigation link with its three states designed — quiet by default, named on hover, plainly marked when it is the page you are on (aria-current="page"). What it is for: headers, sidebars, footers. When to use it: every navigational link in navigation and controls; in content, plain underlined links stay the rule. ### Footer WHEN TO USE: the last band of any real page — the standing navigation and identity that closes it, the counterpart to the top menu or tab bar. Every content or marketing page; not one-page apps, which end at their last snap section. What it is: the page's closing band — a full-bleed footer that reads as the site's plinth. Every piece is optional and the markup is the variant: a .footer-top holding the brand column (.footer-brand with a .footer-wordmark, a one-line .footer-tagline, an optional mono .footer-meta status line and a row of round .footer-social buttons) beside an optional .subscribe well; then up to four .footer-cols link columns (each a nav.footer-col with a mono .footer-col-label over a .footer-links list, links may carry a leading icon); then a .footer-bottom line for copyright and colophon. It carries its own --footer-* token block so it can stand apart from page surface, and by default it inherits the theme, toning to the surface with the page accent bleeding faintly through its top edge. Add data-plinth and it becomes the dark base of the whole site in BOTH light and dark modes — the deliberately-inverted always-dark case — with the accent still bleeding through the top. What it is for: the standing navigation and identity that closes every page. When to use it: on any real page; one-page apps end at their last section instead. It clears the floating bottom tab bar on phones on its own, and never prints (page furniture).
### Subscribe WHEN TO USE: any inline email capture — the footer's mailing-list well, an end-of-article sign-up, a hero or modal opt-in. The layout and the validation are the library's; the actual send stays the project's. What it is: a compact email-capture well — a .subscribe-label and .subscribe-blurb over one input and one button in a .subscribe-row, with a .subscribe-msg status line beneath. What it is for: collecting an email inline without a whole form. When to use it: wherever a single email opt-in belongs. Give the form data-subscribe and facet.js validates the address on submit, sets data-ok on the .subscribe-msg line (true green, false red), and dispatches a facet:subscribe CustomEvent (detail: { email, form }) so the project does the network call — the library never owns an endpoint. The event is cancelable: call event.preventDefault() in your listener to take over the status line yourself; leave it and the well shows a default confirmation and resets. With JS off the form posts natively, so give it a real action if you have no listener. It reads --footer-well inside a footer so it tones to the plinth, plain surface anywhere else. ### Table WHEN TO USE: real tabular data, where rows and columns mean something — comparisons, statements, breakdowns. Never for layout. What it is: the data table — muted header row, zebra striping, a hover that names the row you are reading, right-aligned tabular numbers via .num. Add data-sortable and every column header becomes a click- and keyboard-sort control (facet.js reorders the rows, numeric columns as numbers, a th carrying data-sort-skip stays inert). Rows take the state grammar: aria-selected marks a picked row, and data-status="success|warning|error|info" tints a row and draws a reading-edge bar in the status color (pair it with a badge for the words). The empty state is one full-width cell holding an .empty block; the loading state is skeleton rows (.skeleton bars in the cells, or facet.busy on the region). What it is for: real records: transactions, users, line items. When to use it: data with columns; one-field lists go in list markup. Wrap wide tables in .table-scroll so they scroll horizontally on phones (the wrapper law allows it: it enables scrolling).
MonthAmount
April12,480
July48,000
### Chart WHEN TO USE: one series over time where the shape tells the story — prices, balances, rates. Keep the numbers in a table nearby for readers. What it is: a line chart drawn by facet.chart(el, {points, projectFrom, events, format}) into a .chart div — data line in accent-1 (2.5px, dashed from projectFrom on), dots ringed in the page background, grid on the border token, muted axis text, event markers as faint dashed verticals labelled in a band above the plot, and a drag crosshair that reads the nearest value, dropped on pointer lift. points is [{x, y, label?}]. Colors are all tokens, so theme and mode switches restyle a live chart with no redraw; velvet turns the line gold via accent-1 automatically. The SVG draws at ~1 unit per pixel so type reads true; the surface is touch-action: none (gesture law). Put the chart in a .chart-card — a well in velvet, a quiet panel elsewhere. update(next) redraws with new data.
### Badge, chip, avatar, progress WHEN TO USE: badges state, they never act; chips toggle; an avatar names a person; progress shows completeness with words nearby. What it is: four tiny signals. Badge: a word in a pill — the plain .badge rides the base accent (accent-3: a very pale fill with a normal-colour outline), plus .badge-success / -warning / -error / -info for status. Chip: a small toggleable pill button; when pressed it takes the same pale-fill-plus-coloured-outline base treatment; aria-pressed carries its state and facet.js toggles a bare chip on click (chips wired to data attributes keep their own handlers); an svg glyph inside a chip sits beside the label on its own; add a .chip-remove × to make it a removable filter token (facet.js deletes the chip and fires a bubbling facet:chipremove {value, chip}). Avatar: a round face — an image or initials on the quiet fill; .avatar-small and .avatar-large resize it, and an .avatar-group overlaps several into one ringed cluster. Progress: the native themed, accent-1 fill on a quiet track, or a sliding indeterminate bar when it has no value. Badges state, they never act; avatars carry the person's name in alt or aria-label; determinate progress needs value and max with words nearby. Paid Travel TS AL GH +3 ### 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: over a hundred smooth, round-capped 24x24 line glyphs from Lucide (lucide.dev, ISC-licensed, a fork of Feather), carried as markup inside facet.js — no image files, no sprite fetch, no build step. 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 message globe image file folder sun moon info play pause chart grid sliders refresh undo redo clipboard volume vibrate motion contrast plus-square download-cloud sparkle gem wrench phone camera video mic map-pin map credit-card cart bag tag gift send paperclip list log-out log-in key shield thumbs-up thumbs-down smile flag zap cloud wifi book award target trending-up pie-chart bar-chart code terminal hash at-sign percent wallet package briefcase coffee rocket warning select help.

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, working identically on 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 — an accent-3 thumb over a track that fills with accent up to the thumb and fades to a hairline past it — 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 and paints the fill; a long range can grow a tick scale — wrap the slider in a .slider-wrap with a .slider-scale (data-minor, data-label and data-now set the ticks). 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 — the symbol can be a glyph or an SVG icon) that wraps to fit its width, about five per row on phones. A disabled button greys out and takes no pick. 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. ### 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. /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. Mapping a brand onto the accent ranks (do this first when a product has brand colours): the ranks are LAYERS, not an importance ladder — read them base to top. - One brand colour (single-colour product): set --accent-1, --accent-2 and --accent-3 to the same hue. Difference comes from placement — a solid fill (top), a small label (middle), a faint tint (base) — not from hue. Nothing looks flat because each rank lands somewhere else. - Two brand colours (a base and a highlight, the common case): the base colour → --accent-3 (it becomes the page and section washes, the pills, the selected tab, focus and selected states — the ground the product sits in). The highlight → --accent-1 (it becomes the main headings and the one primary button — the loud thing on top). Leave --accent-2 alone: the middle layer defaults to the neutral muted ink and quietly breaks information down (eyebrows, callouts, captions). - Three brand colours: also override --accent-2 (and --accent-2-text for the readable version) with the support hue. Set them with facet.set({...}) or a [data-theme] block; do NOT paint components with raw hex. The rank tokens (and their derived -tint, -text, -wash) reach every component and every theme for free. 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: