You design at three widths. Users arrive at a thousand. r$ lets you declare what happens in between — and then measures whether the browser agreed.
Start with the measuring. One command, nothing installed, on a site you already have:
npx @responsivejs/cli analyze https://your-site.com
# every width judged · exit 0 pass, 1 violationsat 560px — r$ gives 24.4px, the ladder gives 18px · a 6.4px difference nobody designed
the same function, rendered — drag the slider
Before — a number you now own forever
@media (max-width: 843px) {
.nav { display: none; }
.burger { display: block; }
}After — the browser decides, by measurement
r$.geometry('.nav', {
wrapped: r$.whenWraps,
});Add a link, translate to German, change the font: still correct. There is no number left to maintain.
Before — three steps, two visible jumps
.card { padding: 12px }
@media (width >= 768px) {
.card { padding: 24px }
}
@media (width >= 1024px) {
.card { padding: 36px }
}After — one declaration, no jumps
r$('.card', {
padding: r$.fluid(12, 36),
});Linear values compile to a static clamp(): this ships as CSS and costs zero JavaScript at runtime.
Before — "looks fine on my screen"
a screenshot,
reviewed by a human,
at whatever width
their laptop happens to beAfter — a verdict your CI can fail on
rjs verify home.contract.json https://…Overflow, touch targets, contrast against the background actually painted, continuity of the curve — at every width, exit-code gated.
r$ ✗ fail — 2 errors, 1 warning (410 checks)
noOverflow (1 across 1 element)
.card[0] @320px — right=496 > viewport=320
↳ style at src/cards.ts:12 fluid(240 → 480)
touchTarget (1 across 1 element)
.cta[0] @320,375px — 40x22px < 24x24px
fix: .cta { min-height: 24px } (exact)Every finding carries where it was measured, by how much, and — when a runtime construct owns that element — which construct, and at which line. The fix goes to the cause instead of fighting the cascade.
Fixes are labelled: exact ones apply verbatim, heuristic ones are a direction, and runtime-patch means edit the declaration, not the CSS.
| Invoice | Client | Date | Status | Total |
|---|---|---|---|---|
INV-2051 | Northwind Traders | 12 Mar | Paid | € 1.240,00 |
INV-2052 | Contoso Ltd | 14 Mar | Overdue | € 380,50 |
INV-2053 | Fabrikam Industries | 19 Mar | Draft | € 9.120,00 |
whenOverflows('x') measures false — the columns fit, so it stays a table
Drag it. Five columns stay a table while they fit and become cards when they don't — and the switch is a measurement, so it survives a longer client name, a wider font, another column next quarter.
r$.geometry('.probe', {
crowded: r$.whenOverflows('x'),
});.wrap[data-crowded] tr {
display: grid;
grid-template-areas: 'code total' 'client client' 'date status';
}The subtlety worth stealing: the predicate measures a probe that keeps the table's natural width, never the table it restyles. Measure what you change and it oscillates forever.
Everything in Solo, plus shared contracts and CI verdicts.
import { useResponsive, useGeometry } from '@responsivejs/react';
import { fluid, whenWraps } from '@responsivejs/runtime';
function PriceCard() {
const card = useRef(null);
const tags = useRef(null);
useResponsive(card, {
padding: fluid(12, 28, { container: true, from: 240, to: 720 }),
borderRadius: fluid(8, 18, { container: true, from: 240, to: 720 }),
});
useGeometry(tags, { stacked: whenWraps });
return <article ref={card}>…</article>;
}Same declaration, this framework’s lifecycle. The panel runs the Vue one.