CSS-in-JS for component packages

Styles that ship with your components.

FlairUp is a lightweight CSS-in-JS library for UI package authors. No CSS files to import, no bundler plugins to configure — styles travel inside the JavaScript and work in any app, any bundler, and on the server.

npm install flairupRead the API reference

Used in production by Emoji-Picker-React.

Live demo — pick a variant

styles.demoPrimary

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
# or
yarn 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

The whole loop: define scopes with sheet.create, get a set of classes back per scope, and combine them with cx() where the element renders.

Variants and scopes

Variants and scopes

Each variant is a complete scope with everything the button needs. Variants never layer, so there is no question which rule wins — pick one per state and let cx() handle the rest.

CSS variables

CSS variables

A scope can set variables instead of declarations, and one class carries all of them. Each tone is a literal hex value, with its dark-mode counterpart nested in a media query — override --tone per instance and every declaration that reads it follows.

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.

Media queries

Media queries

Nest @media blocks inside the scope they modify. One card, five breakpoints: spacing grows at 480px, it turns horizontal at 720px, settles into a centered measure at 1024px, and gains an accent edge with larger type at 1280px. The badge names the active breakpoint as you resize. Each step uses distinct values: under flairup 1.1.0, identical declarations share one class, so a repeated value would apply at every width.

Viewport: base · under 480px

New in 1.1

Deterministic cx()

Overrides resolve in cx() order, not creation order.

Pseudo selectors and elements

Pseudo selectors and elements

States live next to the declarations they change: :hover and :active on the button, :focus-visible and ::placeholder on the input. No separate selectors to keep in sync.

Parent selectors

Parent selectors

Scope a whole subtree under a host class like .theme-dark. The same card renders light or dark depending on where it lands — handy when your component has to respect theming it doesn't own. The deeper idea is a contract: name the host classes your package responds to — .theme-dark, .density-compact — and document them. Consumers opt in by wrapping your components, no configuration objects or prop drilling required.

Starter

$9

  • Unlimited projects
  • Export to any format

Pro

$29

  • Unlimited projects
  • Export to any format

Keyframe animations

Keyframe animations

Define keyframes once, reference the returned names from animation. Pick an animation above — the typing dots add staggered delays to the same pattern. This site disables animation under prefers-reduced-motion, so all three go still for users who ask.

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 memory
const 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.