01 · The problem
Why packages need their own styling
FlairUp is a CSS-in-JS library for UI package authors. Applications can dictate their stack; packages cannot — a shared component must bring its styles along and behave in bundlers, frameworks, and server runtimes it has never seen. FlairUp is designed for exactly that job.
The package problem
- Manual style imports
- Consumers have to import CSS files or configure style loaders before a single component renders. Every extra setup step loses adopters.
- Bundler-specific configuration
- Webpack, Rollup, Vite and others each handle styles differently. A package that works everywhere needs styling with no build pipeline at all.
- Style conflicts
- Shared class names and CSS variables leak across packages. Two dependencies can silently override each other with no warning.
- SSR as an afterthought
- Server rendering needs the same styles as strings, in every framework, with no DOM available. Most solutions bolt this on late — or never.
How FlairUp answers
- Zero configuration
- Styles ship inside the JavaScript. Consumers install the package and render — nothing to import, nothing to configure.
- No build pipeline
- FlairUp computes plain CSS at runtime and injects it once. It works under any bundler, or none.
- Scoped atomic classes
- Every declaration becomes its own hashed, deduplicated class. Packages cannot collide, and repeated styles are inserted once.
- SSR from day one
- sheet.getStyle() returns the whole stylesheet as a string. Render it into a <style> tag on the server; the client picks it up.
02 · Concepts
Core concepts
One sheet per name and target
createSheet('name') gives a package its own stylesheet; calls with the same name and mount target share it. A null root always creates an isolated sheet, so server requests never share styles. Identical declarations deduplicate into a single class, so popular styles cost nothing extra no matter how many components use them.
Atomic classes, composed with cx()
Every scope returns a set of classes — one per declaration. Pass sets, strings, arrays, and { name: boolean } maps to cx() and get one class string back.
Conditions travel with the style
Pseudo selectors, parent markers, media queries, and CSS variables live inside the style object next to the declarations they modify. No separate files, no selector bookkeeping.
SSR is just a string
sheet.getStyle() returns the full stylesheet as text. Create the sheet with a null root on the server, inject the string into a <style> tag, and the client continues from the same CSS.
import { createSheet, cx } from 'flairup';const sheet = createSheet('my-package');const styles = sheet.create({button: {color: '#fff',backgroundColor: '#9c1a24',padding: '10px 20px',borderRadius: '8px','&:hover': {backgroundColor: '#7e1420',},},block: {display: 'block',width: '100%',},});function Button({ block, className }) {return (<button className={cx(styles.button, block && styles.block, className)}>Save changes</button>);}
03 · Features
Built for shipping
≈6 KB minified and gzipped, zero dependencies
Small enough to bundle into any package without a second thought.
TypeScript throughout
Style objects are typed; scopes come back as named sets of classes.
Scoped by construction
Hashed atomic classes mean two packages never fight over a name.
Framework-agnostic SSR
Styles render to a string anywhere JavaScript runs — no DOM required.
04 · Install
Installation
npm install flairup# oryarn add flairup
05 · API
API reference
createSheet(name, rootNode?)
Creates a named stylesheet and returns { create, keyframes, getStyle, isApplied }. Pass an element to mount into it, null to keep styles as strings only (the SSR pattern), or nothing to mount into <head>. An options object { rootNode, nonce } covers the rest.
const sheet = createSheet('my-package');// SSR only:const serverSheet = createSheet('my-package', null);
sheet.create(styles)
Defines named scopes of camelCase declarations. Each scope returns a set with one class per declaration. Nest :hover, ::before, .parent markers, &.compound selectors, @media queries, and a -- block of CSS variables.
const styles = sheet.create({card: {padding: '16px','--accent': '#9c1a24','&:hover': { borderColor: 'var(--accent)' },'@media (min-width: 700px)': { padding: '24px' },},});
cx(...args)
Combines class sets, strings, arrays, and { className: boolean } maps into a single class string for className. When two classes set the same declaration the later one wins and removes the earlier one from the output; non-conflicting and unknown classes are preserved. Atomic shorthands resolve as one unit, so give each visual state its own complete scope (see Variants) when overriding them.
cx(styles.card, isActive && styles.active, extraClass);
sheet.keyframes(frames)
Defines named keyframe animations. Returns animation names to reference from the animation property.
const { fadeIn } = sheet.keyframes({fadeIn: { from: { opacity: '0' }, to: { opacity: '1' } },});// animation: `${fadeIn} 300ms ease-out`
sheet.getStyle()
Returns the entire stylesheet as CSS text. Inject it into a <style> tag on the server; the client-side sheet continues from the same rules. See the SSR section below.
sheet.isApplied()
Reports whether the sheet has mounted a <style> tag. Tells a live browser sheet apart from a detached, strings-only server sheet.
if (!sheet.isApplied()) {// strings-only mode: ship sheet.getStyle() to the client}
06 · Examples
Basic usage
Basic usage
const styles = sheet.create({"button": {"font": "inherit","fontWeight": "600","color": "var(--btn-fg)","backgroundColor": "var(--btn-bg)","padding": "10px 20px","border": "none","borderRadius": "8px","cursor": "pointer","transition": "background-color 0.15s ease","&:hover": {"backgroundColor": "var(--accent-strong)"},"&:active": {"transform": "translateY(1px)"},"&:focus-visible": {"outline": "2px solid var(--focus)","outlineOffset": "2px"},"&:disabled": {"opacity": "0.5","cursor": "not-allowed"}}});
function SaveButton() {return (<button className={cx(styles.button)}>Save changes</button>);}
Variants and scopes
Variants and scopes
const styles = sheet.create({"variantPrimary": {"font": "inherit","fontWeight": "600","padding": "10px 20px","borderRadius": "8px","border": "1px solid transparent","cursor": "pointer","color": "var(--btn-fg)","backgroundColor": "var(--btn-bg)","transition": "background-color 0.15s ease","&:hover": {"backgroundColor": "var(--accent-strong)"}},"variantSecondary": {"font": "inherit","fontWeight": "600","padding": "10px 20px","borderRadius": "8px","border": "1px solid var(--field-border)","cursor": "pointer","color": "var(--ink)","backgroundColor": "transparent","transition": "background-color 0.15s ease","&:hover": {"backgroundColor": "var(--card)"}},"variantQuiet": {"font": "inherit","fontWeight": "600","padding": "10px 20px","borderRadius": "8px","border": "1px solid transparent","cursor": "pointer","color": "var(--accent)","backgroundColor": "transparent","transition": "background-color 0.15s ease","&:hover": {"backgroundColor": "var(--card)"}}});
function Actions({ kind }: { kind: 'primary' | 'secondary' | 'quiet' }) {const variants = {primary: styles.variantPrimary,secondary: styles.variantSecondary,quiet: styles.variantQuiet,};return (<div className={cx(styles.buttonGroup)}><button className={cx(variants[kind])}>Publish</button></div>);}
CSS variables
CSS variables
Payment failed
Your card was declined. Try another payment method.
Payment received
Thanks — a receipt is on its way to your inbox.
Trial ends in 3 days
Add a payment method to keep your workspace running.
const styles = sheet.create({"alert": {"--tone": "#9c1a24","backgroundColor": "var(--card)","borderLeft": "4px solid var(--tone)","borderRadius": "8px","padding": "12px 16px","marginBottom": "12px","@media (prefers-color-scheme: dark)": {"--tone": "#ec9aae"}},"alertTitle": {"fontWeight": "650","color": "var(--tone)","marginBottom": "0.25em"},"alertText": {"color": "var(--muted)","fontSize": "0.95rem"},"toneSuccess": {"--tone": "#237a4b","@media (prefers-color-scheme: dark)": {"--tone": "#7fc79b"}},"toneWarning": {"--tone": "#8a5a00","@media (prefers-color-scheme: dark)": {"--tone": "#d9a93f"}}});
function Notices() {return (<><div className={cx(styles.alert)}><p className={cx(styles.alertTitle)}>Payment failed</p><p className={cx(styles.alertText)}>Your card was declined.</p></div><div className={cx(styles.alert, styles.toneSuccess)}><p className={cx(styles.alertTitle)}>Payment received</p><p className={cx(styles.alertText)}>Receipt sent to your inbox.</p></div></>);}
Media queries
Media queries
Viewport: base · under 480px
New in 1.1
Deterministic cx()
Overrides resolve in cx() order, not creation order.
const styles = sheet.create({"badge": {"color": "var(--muted)","fontSize": "0.9rem","marginBottom": "1em"},"badgeValue": {"fontWeight": "650","color": "var(--ink)"},"card": {"backgroundColor": "var(--paper)","border": "1px solid var(--line)","borderRadius": "8px","padding": "16px","display": "flex","flexDirection": "column","gap": "8px","@media (min-width: 480px)": {"padding": "22px","gap": "10px"},"@media (min-width: 720px)": {"flexDirection": "row","gap": "16px","padding": "22px 26px"},"@media (min-width: 1024px)": {"maxWidth": "60rem","marginInline": "auto","padding": "28px"},"@media (min-width: 1280px)": {"borderLeft": "4px solid var(--accent)","padding": "28px 32px"}},"cardBody": {"minWidth": "0","@media (min-width: 720px)": {"flex": "1 1 auto"}},"cardEyebrow": {"fontSize": "0.8rem","fontWeight": "650","letterSpacing": "0.08em","textTransform": "uppercase","color": "var(--accent)"},"cardTitle": {"fontFamily": "var(--font-display)","fontSize": "1.25rem","fontWeight": "700","@media (min-width: 480px)": {"fontSize": "1.4rem"},"@media (min-width: 1280px)": {"fontSize": "2rem"}},"cardText": {"color": "var(--muted)","fontSize": "0.95rem"},"cardButton": {"font": "inherit","fontWeight": "600","color": "var(--btn-fg)","backgroundColor": "var(--btn-bg)","padding": "10px 20px","border": "none","borderRadius": "8px","cursor": "pointer","alignSelf": "flex-start","&:hover": {"backgroundColor": "var(--accent-strong)"},"@media (min-width: 720px)": {"marginLeft": "auto","alignSelf": "center"}}});
function Announcement() {return (<div className={cx(styles.card)}><div className={cx(styles.cardBody)}><p className={cx(styles.cardEyebrow)}>New in 1.1</p><p className={cx(styles.cardTitle)}>Deterministic cx()</p><p className={cx(styles.cardText)}>Overrides resolve in cx() order, not creation order.</p></div><button type="button" className={cx(styles.cardButton)}>Read the notes</button></div>);}
Pseudo selectors and elements
Pseudo selectors and elements
const styles = sheet.create({"invite": {"maxWidth": "24rem"},"field": {"display": "flex","flexDirection": "column","gap": "6px","marginBottom": "12px"},"label": {"fontWeight": "600","fontSize": "0.95rem"},"input": {"font": "inherit","color": "var(--ink)","backgroundColor": "var(--paper)","border": "1px solid var(--field-border)","borderRadius": "8px","padding": "10px 12px","&:hover": {"borderColor": "var(--muted)"},"&:focus-visible": {"outline": "2px solid var(--focus)","outlineOffset": "1px"},"&::placeholder": {"color": "var(--muted)","opacity": "1"},"&:disabled": {"opacity": "0.55","cursor": "not-allowed"}},"button": {"font": "inherit","fontWeight": "600","color": "var(--btn-fg)","backgroundColor": "var(--btn-bg)","padding": "10px 20px","border": "none","borderRadius": "8px","cursor": "pointer","&:hover": {"backgroundColor": "var(--accent-strong)"},"&:active": {"transform": "translateY(1px)"}}});
function Invite() {return (<div className={cx(styles.invite)}><div className={cx(styles.field)}><label className={cx(styles.label)} htmlFor="invite-email">Work email</label><inputid="invite-email"type="email"placeholder="teammate@company.com"className={cx(styles.input)}/></div><button type="button" className={cx(styles.button)}>Send invite</button></div>);}
Parent selectors
Parent selectors
Starter
$9
- Unlimited projects
- Export to any format
Pro
$29
- Unlimited projects
- Export to any format
const styles = sheet.create({".theme-dark": {"plan": {"backgroundColor": "#241c13","borderColor": "#4a3f30","color": "#f2eada"},"planPrice": {"color": "#f2eada"},"planFeatures": {"color": "#c9bda6"},"planButton": {"backgroundColor": "#ec9aae","color": "#2a1216","&:hover": {"backgroundColor": "#f6c2cf"}}},"plans": {"display": "grid","gridTemplateColumns": "1fr","gap": "12px","@media (min-width: 640px)": {"gridTemplateColumns": "1fr 1fr"}},"plan": {"backgroundColor": "var(--paper)","border": "1px solid var(--line)","borderRadius": "8px","padding": "20px","color": "var(--ink)"},"planName": {"fontWeight": "650","fontSize": "0.95rem","color": "var(--accent)","marginBottom": "0.25em"},"planPrice": {"fontFamily": "var(--font-display)","fontSize": "1.75rem","fontWeight": "700","color": "var(--ink)","marginBottom": "0.35em"},"planFeatures": {"color": "var(--muted)","fontSize": "0.95rem","marginTop": "0","marginBottom": "1em","paddingLeft": "1.2em"},"planButton": {"font": "inherit","fontWeight": "600","color": "var(--btn-fg)","backgroundColor": "var(--btn-bg)","padding": "10px 20px","border": "none","borderRadius": "8px","cursor": "pointer","&:hover": {"backgroundColor": "var(--accent-strong)"}}});
function Pricing() {return (<div className={cx(styles.plans)}><PlanCard name="Starter" price="$9" />{/* Same card, restyled by the host's theme class */}<div className="theme-dark"><PlanCard name="Pro" price="$29" /></div></div>);}
Keyframe animations
Keyframe animations
const keyframes = sheet.keyframes({spin: {from: { transform: 'rotate(0deg)' },to: { transform: 'rotate(360deg)' },},shimmer: {from: { opacity: '0.45' },to: { opacity: '1' },},bounce: {'0%': { transform: 'translateY(0)' },'50%': { transform: 'translateY(-4px)' },'100%': { transform: 'translateY(0)' },},});const styles = sheet.create({spinner: {width: '28px',height: '28px',borderRadius: '50%',border: '3px solid var(--line)',borderTopColor: 'var(--accent)',animation: `${keyframes.spin} 0.9s linear infinite`,},skeletonLine: {height: '12px',borderRadius: '6px',backgroundColor: 'var(--line)',animation: `${keyframes.shimmer} 1.2s ease-in-out infinite alternate`,},dot: {width: '10px',height: '10px',borderRadius: '50%',backgroundColor: 'var(--accent)',animation: `${keyframes.bounce} 1.2s ease-in-out infinite`,},dotDelayed: {animationDelay: '0.15s',},});
function Loading({ kind }: { kind: 'spinner' | 'skeleton' | 'typing' }) {if (kind === 'spinner') {return <div role="status" aria-label="Loading" className={cx(styles.spinner)} />;}if (kind === 'typing') {return (<div className={cx(styles.typing)} role="status" aria-label="Someone is typing"><span className={cx(styles.dot)} /><span className={cx(styles.dot, styles.dotDelayed)} /><span className={cx(styles.dot, styles.dotLate)} /></div>);}return (<div className={cx(styles.skeleton)} aria-hidden="true"><div className={cx(styles.skeletonLine, styles.skeletonLong)} /><div className={cx(styles.skeletonLine, styles.skeletonShort)} /></div>);}
07 · SSR
Server-side rendering
On the server there is no DOM to inject into, so create the sheet detached and read the CSS out as a string:
import { createSheet } from 'flairup';// null root: no <style> tag is touched, styles stay in memoryconst sheet = createSheet('my-package', null);renderMyComponents();// inline the result wherever the server renders <head>const css = sheet.getStyle();// <style>{css}</style>
The same calls run unchanged in the browser, where the sheet mounts a <style> tag and keeps it in sync. This very page renders its stylesheets this way.