Skip to main content

Style

@exodra/style turns a plain object into a valid CSS style string, so you don't hand-write "color:red;font-size:12px". It is pure and dependency-free — the output is just a string, so it goes straight into static.style, bindable.style (a derived string), or anywhere a style attribute is accepted, and works identically in the DOM and in SSR.

npm install @exodra/style

Exports: style, and the StyleArg, StyleObject, StyleScalar types.

style(...args)

type StyleScalar = string | number | null | undefined | false;
type StyleObject = Record<string, StyleScalar | readonly (string | number)[]>;
type StyleArg = StyleObject | string | null | undefined | false;

function style(...args: StyleArg[]): string;

Each argument is either a style object, a raw CSS string (appended verbatim), or a falsy value (skipped — handy for conditionals). The results are joined with ;; later arguments come after earlier ones, so they win in the browser.

import { style } from '@exodra/style';

style({ color: 'red', fontSize: 12, '--gap': 8 });
// "color:red;font-size:12px;--gap:8"

Normalization rules

InputOutputRule
backgroundColor: 'red'background-color:redcamelCase → kebab-case
fontSize: 12font-size:12pxnumbers get px
margin: 0margin:0…except 0
opacity: 0.5, zIndex: 10opacity:0.5;z-index:10…and unitless props (opacity, zIndex, lineHeight, fontWeight, flexGrow, grid spans, …)
'--gap': 8--gap:8custom properties pass through untouched (no kebab, no px)
padding: [4, 8]padding:4px 8pxarrays expand with per-item formatting
color: null / undefined / false(omitted)falsy values are dropped
WebkitTransform: 'scale(2)'-webkit-transform:scale(2)explicit vendor prefixes serialize correctly (msFlexAlign-ms-flex-align)

There is no autoprefixing@exodra/style serializes what you write, it doesn't invent prefixes. Modern targets rarely need them; when you do need one, write it explicitly (WebkitBackdropFilter, msFlexAlign, …) and it is emitted verbatim.

Conditionals and merging

Falsy arguments and falsy values are skipped, so conditional styles read cleanly:

style(
base, // an object of shared declarations
isActive && { color: 'blue' }, // dropped entirely when isActive is false
{ opacity: hidden ? 0 : 1 }
);

Usage

The output is a string, so it drops into any bucket that takes a style attribute.

import { style } from '@exodra/style';
import { bindable, derive } from '@exodra/reactivity';

function Card() {
const active = bindable(false);
// A derived string is reactive — the DOM `style` updates when `active` changes.
const css = derive(active, on =>
style({ padding: 16, borderRadius: 8, background: on ? '#eef' : '#fff' })
);

return <div bindable={{ style: css }}></div>;
}

For a value that never changes, put it in static:

<div static={{ style: style({ display: 'flex', gap: 8 }) }} />

Compile-time folding (with the babel plugin)

@exodra/babel-plugin-jsx optimizes static.style objects for you — this is the Exodra path, and the reason style objects are first-class in JSX even though the runtime helper works with a plain string.

When every value in a static.style object is a static literal, the compiler folds it to a CSS string at build time — zero runtime, and the string is then hoistable/clone-cacheable like any other static attribute:

// You write:
<div static={{ style: { color: 'red', fontSize: 12 } }} />

// The plugin emits (no @exodra/style import, no runtime work):
h('div', { static: { style: 'color:red;font-size:12px' } });

When any value is dynamic, the compiler wraps the object in a runtime style(...) call and injects import { style } from '@exodra/style' automatically — so the exact same normalization happens, and you never write the import yourself:

// You write:
<div static={{ style: { color: theme.fg, fontSize: 12 } }} />

// The plugin emits:
import { style } from '@exodra/style';
h('div', { static: { style: style({ color: theme.fg, fontSize: 12 }) } });

The folded string is guaranteed identical to what style() produces at runtime (a parity test in the plugin pins the two normalizers together). Because a static object costs nothing at runtime, prefer writing static.style as an object and let the compiler decide.