DEV Community

Cover image for I built a lightweight runtime for FSCSS property shorthands (with a JS API)
FSCSS tutorial for FSCSS tutorial

Posted on

I built a lightweight runtime for FSCSS property shorthands (with a JS API)

Full FSCSS can compile @define, arrays, pattern(), imports, and more. That’s powerful — and heavier than you need when all you want is size, mx / px, rounded, bg, stack, or inset: ratio-fit(16/9) in the browser.

So I shipped fscss-props: a properties-only runtime. It expands those shorthands at runtime and exposes a small DOM API. It does not parse advanced FSCSS features, so it stays fast even when the DOM is large.

Repo: github.com/Figsh/fscss-props

Shorthand map (same names as 1.2.5): fscss.devtem.org/shorthands

<script src="https://cdn.jsdelivr.net/npm/fscss-props@1.0.0/fscss-props.min.js"></script>
Enter fullscreen mode Exit fullscreen mode

Load it early in <head> (no defer) if the first paint should already see expanded CSS.


What it does

In CSS In JS
<style> blocks rewritten in place el.fscssProp.mx = '1rem'
Inline style="size: 80px; …" el.fscssProp.size = null removes owned longhands
link[data-fscss] stylesheets el.fscssPropText = 'stack: 12px; p: 16px'
.box {
  mx: auto;
  size: 200px 100px;
  rounded-t: 12px;
  center: true;
}
.vid {
  inset: ratio-fit(16 / 9); /* parent: container-type: size */
}
Enter fullscreen mode Exit fullscreen mode

Not included on purpose: @define, @arr, pattern(), module @import. For those, use the full FSCSS CLI/runtime and ship plain CSS.


Why this matters when you use JS

Utilities in pure CSS are fine until state drives the UI: theme, density, layout mode, open/closed. Then you either:

  • toggle classes and keep a big CSS file, or
  • set element.style with longhands and remember every property you touched.

fscss-props sits in between:

  1. Same names as design CSS — px, rounded, stack, not only paddingLeft.
  2. Ownership — setting a shorthand applies longhands; setting it to null clears those longhands, not random inline noise.
  3. Bulk modes — fscssPropText replaces the whole FSCSS-owned set (great for stack vs cols vs hstack without leftover display/gap).
  4. Small surface — no stylesheet AST for the whole language; property table + observers, so it’s aimed at large DOMs and frequent updates.
el.fscssProp.mx = '1rem';
el.fscssProp['ratio-fit'] = '16/9';
el.fscssProp.color = 'red';     // normal CSS works too
el.fscssProp.mx = null;         // remove

console.log(el.fscssPropText);
Enter fullscreen mode Exit fullscreen mode

Example: vanilla store → styles

A tiny subscribe store writes one fscssPropText blob whenever padding, radius, or theme changes:

<script src="https://cdn.jsdelivr.net/npm/fscss-props@1.0.0/fscss-props.min.js"></script>
<style>
  body {
    m: 0;
    min-h: 100vh;
    center: both;
    p: 24px;
    bg: #0b1020;
    color: #e6edf7;
    ff: system-ui, sans-serif;
  }
  .panel {
    w: 100%;
    max-w: 420px;
    stack: 16px;
    p: 24px;
    bg: #121a30;
    border: 1px solid #22304f;
    rounded: 16px;
  }
  .preview {
    min-h: 120px;
    center: both;
    fw: 600;
    trans: all 0.15s ease;
  }
  /* …labels, range inputs, buttons… */
</style>

<div class="panel">
  <div class="preview" id="box">Preview</div>
  <label>Padding <input id="pad" type="range" min="8" max="40" value="16"></label>
  <label>Radius <input id="rad" type="range" min="0" max="32" value="12"></label>
  <div class="bar" id="themes">
    <button type="button" data-bg="#6366f1" aria-pressed="true">Indigo</button>
    <button type="button" data-bg="#0ea5e9">Sky</button>
    <button type="button" data-bg="#10b981">Emerald</button>
  </div>
  <pre id="out"></pre>
</div>

<script>
  function createStore(initial) {
    let state = { ...initial };
    const subs = new Set();
    return {
      get: () => state,
      set(patch) {
        state = { ...state, ...patch };
        subs.forEach((fn) => fn(state));
      },
      subscribe(fn) {
        subs.add(fn);
        fn(state);
        return () => subs.delete(fn);
      },
    };
  }

  const box = document.getElementById('box');
  const out = document.getElementById('out');
  const store = createStore({ pad: 16, radius: 12, bg: '#6366f1' });

  store.subscribe((s) => {
    box.fscssPropText = [
      `p: ${s.pad}px`,
      `rounded: ${s.radius}px`,
      `bg: ${s.bg}`,
      'color: #fff',
      'center: true',
    ].join('; ');
    out.textContent = box.fscssPropText.replace(/; /g, ';\n');
  });

  document.getElementById('pad').oninput = (e) =>
    store.set({ pad: +e.target.value });
  document.getElementById('rad').oninput = (e) =>
    store.set({ radius: +e.target.value });

  document.getElementById('themes').onclick = (e) => {
    const btn = e.target.closest('button');
    if (!btn) return;
    [...e.currentTarget.children].forEach((b) =>
      b.setAttribute('aria-pressed', b === btn));
    store.set({ bg: btn.dataset.bg });
  };
</script>
Enter fullscreen mode Exit fullscreen mode

Full demos (shape lab, ratio-fit, reactive list): examples/ in the repo.


When to use which

Need Tool
Author modules, arrays, patterns; ship static CSS FSCSS CLI / full runtime
Live property shorthands + JS control fscss-props
Max performance in production, no live expand Compile with CLI; omit the script

Links

MIT

Top comments (0)