Skip to content

Your layout is a function.
Treat it like one.

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:

bash
npx @responsivejs/cli analyze https://your-site.com
# every width judged · exit 0 pass, 1 violations
4818320px1440px@media ladderfluid(18, 48)

at 560px — r$ gives 24.4px, the ladder gives 18px · a 6.4px difference nobody designed

Between your breakpointsthis is what a user actually sees.

the same function, rendered — drag the slider

What you stop doing

Before — a number you now own forever

css
@media (max-width: 843px) {
    .nav { display: none; }
    .burger { display: block; }
}

After — the browser decides, by measurement

ts
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

css
.card { padding: 12px }

@media (width >= 768px) {
    .card { padding: 24px }
}
@media (width >= 1024px) {
    .card { padding: 36px }
}

After — one declaration, no jumps

ts
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"

txt
a screenshot,
reviewed by a human,
at whatever width
their laptop happens to be

After — a verdict your CI can fail on

bash
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.

It reads the page back to you

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.

Built for agents, too →

This reacts to the panel, not to your window

InvoiceClientDateStatusTotal
INV-2051Northwind Traders12 MarPaid€ 1.240,00
INV-2052Contoso Ltd14 MarOverdue€ 380,50
INV-2053Fabrikam Industries19 MarDraft€ 9.120,00
720px — drag the handle (or focus it and use ←/→)

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.

ts
r$.geometry('.probe', {
    crowded: r$.whenOverflows('x'),
});
css
.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.

Six more live demos →

One declaration, whichever framework you're in

Team€ 29/mo

Everything in Solo, plus shared contracts and CI verdicts.

ContractsCI gateSARIFProvenance
440px — drag the handle (or focus it and use ←/→)
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.