@responsivejs/runtime
v1.0.0-alpha.0
Published
The authoring runtime of r$ — reactive value = f(width), viewport and container aware, CSS-first: emits clamp() where CSS suffices, JS only where it doesn't.
Downloads
97
Maintainers
Readme
@responsivejs/runtime
The authoring half of
r$: reactivevalue = f(width)— viewport and container aware, CSS-first.
npm install @responsivejs/runtimeThe rule that shapes everything: where CSS suffices, emit CSS. A linear r$.fluid()
compiles to a clamp() (the Utopia formula); breakpoint switches compile to @media blocks.
JavaScript drives only what CSS cannot express — non-linear curves, colors, geometry state,
cross-element relations, measurement-driven logic.
60 seconds
One import; type r$. and autocomplete the whole surface:
import { r$ } from '@responsivejs/runtime';
const bp = r$.breakpoints({ mobile: 320, tablet: 768, desktop: 1280 } as const);
r$.tokens({
'--space-m': r$.fluid(16, 24), // → static clamp() on :root, zero JS
'--font-hero': r$.fluid(28, 64, { curve: 'exponential' }), // → JS-driven variable
});
r$.geometry('.site-nav', { wrapped: r$.whenWraps }); // CSS: .site-nav[data-wrapped] { … }
r$('.cards', {
gridTemplateColumns: bp.below('tablet', '1fr', 'repeat(3, 1fr)'), // → static @media
padding: r$.fluid(8, 32, { curve: 'ease-in' }), // → JS (non-linear)
});r$(target, map) splits the map automatically: static parts land in one injected stylesheet,
the rest updates via a single shared resize listener, coalesced to one style write per frame.
What's on r$.
- Values —
fluid(min, max, unit? | opts)(linear→clamp; curves; colors via OKLab; per-breakpoint arrays; structural strings) ·custom(fn)·combine([...])·when(pred, a, b)·whenInRange(min, max, v)·breakpoint.below/above/between/match. - Typed breakpoints —
r$.breakpoints({mobile: 320, …} as const)returns an API typed on your names:bp.below('tablet', …)autocompletes, a typo is a compile error. - Tokens —
r$.tokens({'--space-m': r$.fluid(16, 24)}): the design scale as fluid custom properties on:root;.cssfor SSR,.toDTCG()for design tooling. - Geometry ("JS detects, CSS styles") —
geometry(target, states)keeps data-attributes in sync with measured facts:whenWraps,whenOverflows,whenTruncated,whenStuck,linesOf,whenCollides. Style them from the stylesheet:.nav[data-wrapped] { … }. - Cross-element —
fromElement(sel)as a fluid domain (a value driven by another element's width) ·sync(sel, 'height')(equal sizes across containers) ·ratio(a, b, {min, max})(an enforced layout invariant). - Utilities —
static(sel, map)(CSS-only, throws if JS needed) ·dynamic·lazy·memo·batch·debug·flush()· the tagged-template and utility micro-grammars.
Every value accepts { container: true } to bind to the nearest container width instead of
the viewport (shared ResizeObserver; static emission switches vw → cqi).
Named exports of every namespace member exist for tree-shaking-sensitive code (import {
fluid, geometry } …) — they are the same objects. Two are renamed to stay unambiguous:
r$.apply is applyUtilities, and r$.batch (signal batch plus style flush) is
batchWrites, so it never collides with the pure batch of /signals. Subpaths:
/signals (the TC39-shaped reactive engine), /curves, /layout, /typography,
/geometry.
Contracts worth knowing
- Disposal & ownership: every construct returns a handle;
dispose()removes exactly what it did — effects, observers, injected CSS, data-attributes — and RESTORES inline values that pre-existed. Handles are isolated (unique stylesheets, refcounted shared side effects likecontainer-type). - Geometry's one rule: never
display: nonethe element a predicate measures; collapse keeping layout (visibility: hidden; height: 0; overflow: hidden). - SSR: no
windowaccess at module level; shiphandle.css,r$.tokens(...).css, orr$.renderStatic()(every emission at once). Strict CSP?r$.configure({ nonce }). - SPA:
r$.observe(selector, map)keeps a selector bound as elements mount and unmount;r$.scope()groups a component's handles so onedispose()releases them all. - No build step:
dist/global.jsis the whole runtime as an IIFE (~15.5 kB gzip) —<script>it and usewindow.r$. Same API, same CSS-first split. - Cost: one resize listener, one ResizeObserver, one scroll listener — refcounted, total. ~11 kB gzipped, zero dependencies.
Documentation
The runtime guide (start here) · case studies · API reference · live example
Licensed under MPL-2.0.
