# ResponsiveJS — complete documentation # Getting started ResponsiveJS (`r$`) is one model — **`value = f(width)`** — with two halves: *author* responsive behavior CSS can't express, *verify* the rendered result with measurements. **Start with the second half.** It costs one command, changes nothing in your codebase, and tells you something true about a site you already have: ```bash npx @responsivejs/cli analyze https://your-site.com -w 320,375,768,1024,1280,1920 ``` Every width judged: what overflows, which targets miss the WCAG 24px floor, where contrast fails against the background actually painted, whether the page scrolls sideways on a phone. Exit `0` pass, `1` violations — so it is a CI gate the moment you want one. Nothing installed, nothing imported, no decision made. Liked what it found? [Pin it as a contract](adopting) — still without writing r$ code. Only then is the authoring half worth its install: it is for the cases CSS genuinely cannot express, not for replacing CSS you already have. - [**Adopting r$ in a site you already have**](adopting) — the five-step path, each step useful on its own - [**The tutorial**](tutorial) — the other direction: build a page from nothing, ~30 minutes - [**Why r$**](why) — "I can write `clamp()` myself", "we have visual regression testing", and when *not* to use it - [**Troubleshooting**](troubleshooting) — by symptom ## What it replaces | The hack you write today | The r$ construct | What you gain | | --- | --- | --- | | A `@media` ladder for every size | `r$.tokens({ '--space-m': r$.fluid(16, 24) })` | Smooth scaling, static `clamp()`, zero JS | | The burger breakpoint that rots | `r$.geometry('.nav', { wrapped: r$.whenWraps })` | Adapts by *measurement* — add a link, still correct | | IntersectionObserver sticky-sentinel | `r$.whenStuck()` | One line, no sentinel DOM | | Resize listeners + manual measuring | `r$.sync`, `r$.ratio`, `r$.fromElement` | Cross-element relations, cleanup included | | `'mobile'` strings that typo at runtime | `r$.breakpoints({...} as const)` | Names the compiler checks | | Squinting at three screen sizes | `rjs analyze ` | Measured verdict at every width, exit-code gated | | Screenshot diffing for regressions | `rjs record` / `verify` contracts | The layout's rules as reviewable JSON | Pick your entry: | I want to… | Install | Start here | | ------------------------------------------------------ | -------------------------------------- | ---------- | | Audit a URL right now, zero setup | nothing — `npx @responsivejs/cli` | [§ Audit](#audit-cli) | | Author responsive behavior (fluid, geometry, tokens) | `npm i @responsivejs/runtime` | [§ Authoring](#authoring) | | Validate/score a page in CI | `npm i -D @responsivejs/design @playwright/test` | [§ Validation](#validation) | | Score a live DOM without any driver | `npm i @responsivejs/design` | [§ Zero-driver](#zero-driver) | | Pin a layout down as a verifiable contract | `npm i -D @responsivejs/contract` | [§ Contracts](#contracts) | | Use it in React / Vue / Angular | `npm i @responsivejs/react` · `/vue` · `/angular` | [§ Adapters](#adapters) | | Just the math (curves, geometry, WCAG, aesthetics) | `npm i @responsivejs/core` | [API: core](api/core) | | Drive r$ as an AI agent | — | [agents docs](agents/validation-reference) | No bundler? `` gives you the same `r$` on `window` (~15.5 kB gzip) — CMS pages, plain HTML, live demos. All packages are ESM-only, zero runtime dependencies (Playwright and axe-core are optional peers of `design`), Node ≥ 20.19, MPL-2.0. ## Audit (CLI) ```bash npx @responsivejs/cli analyze https://example.com -w 320,768,1280 # constraints + a11y · exit 0 pass / 1 violations · -f json|sarif · --score adds the heuristic npx @responsivejs/cli audit https://example.com --vs https://competitor.com # → one self-contained HTML report: screenshots with violation overlays, side-by-side ``` Driver-pluggable (Playwright, or [agent-browser](https://github.com/vercel-labs/agent-browser) for any live URL with nothing installed). `verify`/`record` run the contract flow. → [CLI reference](api/cli) · [the design guide](guides/validation) ## Authoring One import, the whole surface behind your editor's autocomplete: ```typescript import { r$ } from '@responsivejs/runtime'; const bp = r$.breakpoints({ mobile: 320, tablet: 768, desktop: 1024 } as const); r$.tokens({ '--space-m': r$.fluid(16, 24), '--font-hero': r$.fluid(28, 64) }); // clamp() on :root r$.geometry('.site-nav', { wrapped: r$.whenWraps }); // CSS: .site-nav[data-wrapped] { … } r$('.cards', { gridTemplateColumns: bp.below('tablet', '1fr', 'repeat(3, 1fr)') }); ``` r$ is CSS-first: everything expressible as `clamp()`/`@media` becomes one injected stylesheet; JS drives only what CSS cannot — non-linear curves, **geometry state** (wrap, overflow, sticky, truncation), **cross-element dependencies** (`fromElement`, `sync`, `ratio`). Add `{ container: true, from, to }` to bind a value to the nearest container instead of the viewport — `from`/`to` are the container's own range, and are required. → **[the runtime guide](guides/runtime)** (purposes, gradual examples, the mental model) · [case studies](guides/case-studies) · [API: runtime](api/runtime) · [live example](https://github.com/AleSaiani/ResponsiveJS/blob/main/examples/landing) ## Adapters The constructs are framework-agnostic; the adapters own the **lifecycle**. ```tsx // React const ref = useRef(null); useResponsive(ref, { padding: r$.fluid(12, 24) }); // applied on mount, disposed on unmount const isDesktop = useBreakpoint('desktop'); ``` ```vue ``` Changing a declaration calls `update()` on the live handle instead of recreating it; React's StrictMode double-invocation is handled. Angular ships decorator-free helpers (`injectResponsive`, `injectViewportWidth`, …) that need no compilation step. → **[adapters reference](api/adapters)** ## Validation ```typescript import { test, expect } from '@playwright/test'; import { r$ } from '@responsivejs/design'; test('layout holds at all widths', async ({ page }) => { const r = r$(page); await r.sweep({ url: 'http://localhost:3000', widths: [320, 768, 1280], selectors: ['h1', '.btn'] }); r.assert.noOverflow().minSize('.btn', { height: 44 }).monotonic('h1', 'fontSize', 'up'); expect(r.report().pass).toBe(true); }); ``` Or run the whole oracle in one call — constraints + aesthetic score + a11y (axe): ```typescript const report = await r$(page).sweep({ ... }).then((r) => r.analyze()); // UnifiedReport: { pass, violations, fixes, scores, summary, … } ``` → **[the design guide](guides/validation)** (measure → model → judge) · [API: design](api/design) · [guide: CI](guides/ci) ## Zero-driver The browser subpath runs anywhere a DOM exists — no Playwright: ```typescript import { scoreDOM, analyzeStore, collectStore } from '@responsivejs/design/browser'; const { average, suggestions } = scoreDOM(['main', '.card']); // 17-metric aesthetic score const report = analyzeStore(collectStore(['main', '.card'])); // full constraint report ``` Inject it into any page via a driver's `eval` (CDP, agent-browser) — see the [agents guide](guides/agents). ## Contracts ```typescript import { contract } from '@responsivejs/contract'; import { verifyContract } from '@responsivejs/design'; const home = contract('home') .select('sidebar', '.app-sidebar') .assert('noOverflow', undefined, { id: 'no-bleed' }) .below(768).assert('hidden', { selector: '$sidebar' }, { id: 'sidebar-mobile' }) .from(768).assert('visible', { selector: '$sidebar' }, { id: 'sidebar-desktop' }) .build(); const report = await verifyContract(home, page); // sweep derived from the contract ``` Contracts serialize to JSON (published [schema](https://github.com/AleSaiani/ResponsiveJS/blob/main/packages/contract/schema/design-contract.v1.json)), travel with the repo, and double as the machine-readable spec agents enforce. → [API: contract](api/contract) · [guide: CI](guides/ci) ## Development setup (this repo) ```bash git clone https://github.com/AleSaiani/ResponsiveJS.git && cd ResponsiveJS pnpm install pnpm test # unit tests, plain Node pnpm typecheck # no build needed (source-resolved) pnpm build # tsc, topological pnpm test:e2e # needs: pnpm --filter @responsivejs/design exec playwright install chromium ``` --- # Adopting r$ in a site you already have The [tutorial](tutorial) builds a page from nothing. This is the other path: you have a codebase, media queries you did not write, and no appetite for a rewrite. Nothing here asks you to change a line of CSS until step 4 — and step 4 is optional. Each step is useful on its own. Stop wherever the value stops. ## Step 1 — Measure what you have (about a minute, nothing installed) ```bash npx @responsivejs/cli analyze https://your-site.com -w 320,375,768,1024,1280,1920 ``` You get every width judged: what overflows, which targets are under the WCAG 24px floor, where contrast fails against the background actually painted, and — if the page scrolls sideways at any width — the document's own reach. Exit `0` pass, `1` violations. If nothing is found, you have learned something real for the price of a minute. If plenty is found, do not fix it yet: pin it first. **No browser driver?** `npx @responsivejs/cli doctor` tells you what is available and the exact install command for what is not. ## Step 2 — Pin today's reality as a contract ```bash npx @responsivejs/cli init https://your-site.com -o site.contract.json ``` This works on a page that has never heard of r$: the rules that carry most of the value need neither a construct nor a selector you have to invent. You get a JSON file with the page-wide rules, plus baselines for the headings and body text it found. Read it — it is meant to be reviewed, not trusted blindly. Delete rules you disagree with, tighten the ones you care about. ```bash npx @responsivejs/cli record site.contract.json https://your-site.com # pin today's curves ``` `record` measures the current type scale and writes it into the contract. From now on, a change to those curves is a **diff in a reviewed file** rather than a surprise. ## Step 3 — Make it a gate ```yaml - run: npx @responsivejs/cli verify site.contract.json http://localhost:4173/ -d playwright ``` Run it against the built site, not the dev server. Two rules for keeping the gate trusted: - **Start from green.** If the audit found twenty violations, do not gate on all twenty on day one. Fix, or narrow the contract with `when: { min, max }` ranges, until it passes — a gate that is red on arrival gets ignored within a week. - **Warnings are not failures.** Only errors fail. `✓ 794/799 checks (5 warnings — no errors)` is a pass, and the five are worth a look, not a build break. At this point you have regression protection and have written no r$ code at all. For a lot of teams this is the whole adoption. ## Step 4 — Replace the breakpoints that actually hurt Now, and only now, is the runtime worth installing — and even then, one construct at a time. Start with the breakpoints that keep rotting, not with the ones that work: ```bash npm i @responsivejs/runtime ``` **The burger that is wrong in German.** A hand-picked `@media (max-width: 843px)` breaks when a link is added or the site is translated. Replace the number with the measurement: ```typescript r$.geometry('.site-nav', { wrapped: r$.whenWraps }); ``` ```css .site-nav[data-wrapped] { visibility: hidden; height: 0; overflow: hidden; } .site-nav[data-wrapped] ~ .burger { display: block; } ``` Your CSS keeps owning the appearance; JS only states the fact. And delete the media query — leaving both means two sources of truth. **The spacing ladder with three visible jumps.** Three rules become one declaration that compiles to a `clamp()` and ships as CSS: ```typescript r$('.card', { padding: r$.fluid(12, 36) }); ``` **A component that must answer to its container**, not the window — say how wide that container gets, which is required and not optional: ```typescript const panel = { container: true, from: 240, to: 820 }; r$('.card', { fontSize: r$.fluid(15, 26, panel) }); ``` Re-run `verify` after each replacement. The contract you pinned in step 2 is now doing the job it was written for. ## Step 5 — Let the constructs write the rules Once the page runs the runtime, `init` reads more than the page: ```bash npx @responsivejs/cli init https://your-site.com -o site.contract.json ``` Every fluid value you declared becomes a `monotonic` + `continuous` rule and a baseline: what you *declared* becomes what CI *verifies*. Anything not yet expressible as a rule is printed, never dropped silently. ## What not to do - **Do not convert every media query.** Most of them are fine. The ones worth replacing are the ones that encode a guess about content — how many links fit, whether the text was cut. - **Do not gate on a contract you have not read.** A generated file is a starting point. - **Do not chase the aesthetic score.** It is a heuristic, off by default, and no substitute for the measurements. → [Troubleshooting](troubleshooting) when something does not behave · [Why r$](why) when someone asks you to justify it. --- # Tutorial — from empty page to validated, fluid landing Build a real page with every core construct, one step at a time. Each step: what you're about to gain, the code, what actually happened, and a checkpoint you can see. At the end you'll have written the [landing example](https://github.com/AleSaiani/ResponsiveJS/blob/main/examples/landing) yourself — *and* pinned it with a contract so it can never silently regress. Time: ~30 minutes. Prereqs: Node ≥ 20.19, any bundler (we use vite). ```bash npm create vite@latest fluid-landing -- --template vanilla-ts cd fluid-landing && npm i @responsivejs/runtime ``` The page we'll build: a header with logo + nav + (hidden) burger button, a hero, a row of three cards, a sidebar. Plain HTML — grab the markup from the [example](https://github.com/AleSaiani/ResponsiveJS/blob/main/examples/landing/index.html) or write your own with the same class names. --- ## Step 1 — Kill your breakpoints for sizes: fluid tokens **What you gain:** spacing and type that scale smoothly with the viewport — no `@media` ladder, no magic numbers, zero runtime JavaScript for the linear cases. ```typescript // main.ts import { r$ } from '@responsivejs/runtime'; r$.tokens({ '--space-s': r$.fluid(8, 12), '--space-m': r$.fluid(16, 24), '--space-l': r$.fluid(32, 56), '--font-body': r$.fluid(15, 18), '--font-hero': r$.fluid(28, 64, { curve: 'exponential' }), }); ``` ```css body { font-size: var(--font-body); } .hero h1 { font-size: var(--font-hero); } .card { padding: var(--space-m); } main { gap: var(--space-l); } ``` **What happened:** every *linear* token compiled to a static `clamp()` on `:root` — open devtools, look at ``: there's a `