Near-zero runtime CSS-in-JS library
Write modular CSS within your JavaScript code with built-in themes and SSR support.
-
Near-Zero Runtime ⚡
Styles are generated when the module is initialized, rather than during component rendering. This eliminates runtime style generation, improving performance and reducing complexity. -
Component-Scoped Styles ✨
CSSFUN scopes styles to the component, preventing style leakage and promoting modularity. It keeps both logic and styling in the same file for easier management. -
Framework-Agnostic and Lightweight 🌐
CSSFUN is compatible with any environment. At just 1.8KB, it adds minimal overhead to your projects. -
No Build Tools Required 🛠️
CSSFUN can be used directly in the browser, eliminating the need for complex build tools or configurations. -
Server-Side Rendering (SSR) Support 🚀
CSSFUN supports server-side rendering out of the box, optimizing initial load times without duplicating styles. -
Built-in Theme Management 🎨
With built-in theme support, CSSFUN uses CSS variables to manage light and dark color schemes. Themes update automatically based on user preferences, with no re-renders needed. -
TypeScript Support 🔷
Ships with type definitions. Theclassesobject is fully typed based on your styles, so you get autocomplete for class names and a type error on typos. CSS properties autocomplete via csstype.
$ npm install cssfunimport { css } from 'cssfun';import { css } from 'https://esm.run/cssfun';<script src="https://cdn.jsdelivr.net/npm/cssfun"></script>const { css } = CSSFUN;css() injects the styles and returns a StyleSheet instance. Its classes property holds the generated class names, one per top-level selector:
const { classes } = css({
button : {
backgroundColor : 'blue',
color : 'white',
padding : '10px',
borderRadius : '5px'
}
});const Button = () => <button className={classes.button}>Click me</button>;See Class Name Generation for how the names are built.
css(styles) returns a StyleSheet instance. Its classes property is an object that maps each top-level selector in your styles to a unique, scoped class name:
const sheet = css({
button: { color: 'red' },
link: { color: 'blue' }
});
sheet.classes; // { button: "fun-9qkk9s-button", link: "fun-9qkk9s-link" }
sheet.classes.button; // "fun-9qkk9s-button"Destructure classes to use the generated names as className:
const { classes } = css({ button: { color: 'red' } });
const Button = () => <button className={classes.button}>Click me</button>;Generation happens once, when the instance is created — never during rendering — so there is no per-render overhead.
Only top-level keys that are plain identifiers (matching /^\w+$/ — letters, digits and underscores) get a generated class. At-rules (@global, @media …, @keyframes …), $ references and keys with dashes, spaces or commas are left untouched and don't appear in classes.
Each class name embeds a uid hashed from the styles' content, so identical styles always produce identical class names — across server and client, hot reloads, and repeated calls. This is what makes SSR hydration seamless.
The format depends on the build:
| Build | Format | Example |
|---|---|---|
| Development | {prefix}-{uid}-{name} |
fun-9qkk9s-button |
| Production | {prefix[0]}-{uid}-{index} |
f-9qkk9s-1 |
In production, the original name is replaced by a 1-based index to keep the output small. The default prefix is fun.
Note: Examples in this documentation use the development format for readability. You can override generation via
options.generateClassNameor by extendingStyleSheet.
Renderers are functions that transform style objects into CSS strings. They are applied in sequence, with each renderer receiving the output of the previous one.
CSSFUN uses two built-in renderers by default:
parseStyles: Transforms the style object (expands nested selectors, replaces class references, converts camelCase to dashed-case, handles global styles)renderStyles: Converts the processed object into a CSS string
The final renderer in the chain outputs the CSS string that gets injected into the DOM.
These are the built-in renderer transformations:
css({
root : {
backgroundColor : 'black',
fontSize : '16px',
paddingTop : '10px'
}
}).toString();<style data-fun-uid="uwitok">
.fun-uwitok-root {
background-color: black;
font-size: 16px;
padding-top: 10px;
}
</style>-
Use
&to reference the selector of the parent rulecss({ button : { backgroundColor : 'white', '&:hover' : { backgroundColor : 'black' }, '& span' : { color : 'blue' } } }).toString();
<style data-fun-uid="1pxyvx7"> .fun-1pxyvx7-button { background-color: white; } .fun-1pxyvx7-button:hover { background-color: black; } .fun-1pxyvx7-button span { color: blue; } </style>
-
Deep nesting
css({ button : { backgroundColor : 'white', '&:active' : { backgroundColor : 'black', '&:hover' : { backgroundColor : 'blue' } } } }).toString();
<style data-fun-uid="169vukw"> .fun-169vukw-button { background-color: white; } .fun-169vukw-button:active { background-color: black; } .fun-169vukw-button:active:hover { background-color: blue; } </style>
-
Use
$to reference a local class within the sameStyleSheetinstancecss({ button : { backgroundColor : 'white' }, '$button:hover' : { backgroundColor : 'black' }, '$button span' : { color : 'blue' } }).toString();
<style data-fun-uid="2xfpy0"> .fun-2xfpy0-button { background-color: white; } .fun-2xfpy0-button:hover { background-color: black; } .fun-2xfpy0-button span { color: blue; } </style>
-
Global block
css({ '@global' : { body : { backgroundColor : 'black' } } }).toString();
<style data-fun-uid="ml03n3"> body { background-color: black; } </style>
-
Nested global block
css({ root : { '@global' : { a : { color : 'black' } } } }).toString();
<style data-fun-uid="1eia2eq"> .fun-1eia2eq-root a { color: black; } </style>
-
Global prefix
css({ '@global body' : { backgroundColor : 'black' } }).toString();
<style data-fun-uid="1p1av20"> body { background-color: black; } </style>
-
Nested global prefix
css({ root : { '@global a' : { color : 'black' } } }).toString();
<style data-fun-uid="xvd6jj"> .fun-xvd6jj-root a { color: black; } </style>
Example flow:
Input styles object
↓
[parseStyles] → Transforms object (expands nested, replaces references, converts camelCase)
↓
[renderStyles] → Converts object to CSS string
↓
Output CSS string
You can customize the renderers via options.renderers (or by setting renderers on a StyleSheet subclass).
Elements in the renderers array can be either functions or strings that reference methods of the StyleSheet instance. Method-name strings are resolved to methods when the instance is created, and every renderer is called with the instance as this.
Renderers are applied in array order, each receiving the previous one's output. By default, StyleSheet instances use [this.parseStyles, this.renderStyles]: parseStyles runs first (transforms the object), then renderStyles (converts it to the CSS string).
renderers may also be given as a function that returns the array, resolved when the instance is created.
The prefix, attributes and renderers options also accept a function that returns the value, so you can compute it at runtime instead of passing a static value. The timing differs: prefix and renderers are resolved once, when the instance is created, while attributes is resolved lazily every time the styles are rendered — handy for per-request values such as a CSP nonce.
For subclasses, the preinitialize method runs at the very start of the constructor — before the options are applied and before the class names are generated. Override it to run setup logic or define prefix, attributes or renderers (matching options still take precedence).
See Content Security Policy (CSP) for a real-world example that combines preinitialize with a function-valued attributes.
A theme is a StyleSheet that provides access to CSS variables
for consistent styling across your application. It supports multiple color schemes,
including light, dark, light dark (default, adapts to system preferences), and normal.
Themes allow your components to automatically adapt to changes in the user's system preferences
or use a fixed color scheme.
The createTheme function generates a theme StyleSheet instance.
It accepts a themes object, which defines variables for the specified color schemes, and an
options object to customize the theme generation.
Each key in the themes object corresponds to a color scheme (light, dark, normal),
and its value is an object of key-value pairs that will be converted into CSS variables.
Define styles for light and dark color schemes using the createTheme function.
const theme = createTheme({
light : {
colorPrimary : 'black',
backgroundLevel1 : 'white'
},
dark : {
colorPrimary : 'white',
backgroundLevel1 : 'black'
}
});The options.colorScheme parameter specifies which color scheme(s) to use. Possible values are:
light: Uses thelighttheme only.dark: Uses thedarktheme only.light dark(default): Supports bothlightanddarkthemes, adapting to system preferences. You can override the system preference by setting thedata-color-schemeattribute tolightordarkon a parent element.normal: Uses thenormaltheme only.
The options.cssVarsPrefix parameter allows you to customize the prefix for the generated CSS variables.
By default, the prefix is fun. For example, a key colorPrimary in the theme will generate a CSS variable
like --fun-colorPrimary.
The generated theme includes a root class, which exposes all the theme's CSS variables to any element
that uses this class and its descendants. You can apply this class to the body element to style the
entire application, or to the root element of a specific component to apply the theme to just part of your UI.
// Add theme class to the body
document.body.classList.add(theme.classes.root);The themes object is automatically converted into CSS variables. For example:
{ backgroundLevel1 : 'black' }is converted into the CSS variable --fun-backgroundLevel1.
Nested structures like:
{
palette : {
common : {
black : '#000'
}
}
}are converted into --fun-palette-common-black.
You can use these variables in your component styles, even before the theme is applied. Your components will automatically update when the theme or system color scheme changes.
const { classes } = css({
button : {
color : 'var(--fun-colorPrimary)',
backgroundColor : 'var(--fun-backgroundLevel1)'
},
});
const Button = ({ label }) => <button className={classes.button}>{label}</button>;Easily add your styles to the server-rendered HTML by embedding the StyleSheets as a
string within the <head> of your page.
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StyleSheet, createTheme } from 'cssfun';
import App from './App.js';
// Create a theme with light and dark modes
const theme = createTheme({
light : {
bg : '#fff',
color : '#000'
},
dark : {
bg : '#000',
color : '#fff'
}
});
const app = express();
app.get('*', (req, res) => {
// Render the app
const html = renderToString(<App />);
// Get generated styles as string
const styles = StyleSheet.toString();
// Get theme root class
const cls = theme.classes.root;
const template = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SSR App</title>
${styles}
</head>
<body class="${cls}">
<div id="root">${html}</div>
<script src="/bundle.js"></script>
</body>
</html>
`;
res.send(template);
});When the app is hydrated on the client, the styles are preserved and will not be recreated.
If your site uses a strict Content Security Policy with a per-request nonce (style-src 'nonce-…'), add that nonce to the generated <style> tags through the attributes option/property. StyleSheet.toString() then emits <style nonce="…" data-fun-uid="…">.
Instances fall back to StyleSheet.prototype.attributes, which is read fresh on every render, so set the nonce there before rendering:
app.get('*', (req, res) => {
StyleSheet.prototype.attributes = { nonce : res.locals.cspNonce };
const html = renderToString(<App />);
const styles = StyleSheet.toString(); // <style nonce="…" data-fun-uid="…">…</style>
// …embed `styles` in the <head> as in the SSR example above.
});Simple and effective for synchronous SSR: the styles are rendered on the server and reused on the client during hydration without being recreated, so the client needs no nonce handling. (For streaming/async SSR, resolve the nonce per request from context — e.g. AsyncLocalStorage — via the function form, since the prototype is shared across concurrent requests.)
To avoid mutating the global prototype, scope it to your own subclass and css helper. preinitialize runs once per instance, so use the function form to read the nonce lazily at render time:
import { StyleSheet } from 'cssfun';
class CSPStyleSheet extends StyleSheet {
preinitialize() {
this.attributes = () => ({ nonce : getRequestNonce() });
}
}
const css = (styles, options) => new CSPStyleSheet(styles, options).attach();Pass CSPStyleSheet to createTheme via options.createStyleSheet so theme styles carry the nonce too.
CSSFUN ships with TypeScript declarations out of the box — no @types/cssfun needed. The types are bundled in the package and resolved automatically via the types field in package.json.
Requires TypeScript 4.1+ (the
classesinference relies on key remapping and template literal types).
The css() function and StyleSheet constructor are generic over the styles object. The generated classes map is inferred from the keys you pass in, so typos and missing keys are caught at compile time:
import { css } from 'cssfun';
const sheet = css({
link : { color : 'blue' },
button : { padding : 10 }
});
sheet.classes.link; // string
sheet.classes.button; // string
sheet.classes.typo; // ❌ Property 'typo' does not existAt-rule keys (@global, @keyframes …, @media …, @supports …) and class reference keys ($name) are filtered out of classes automatically — they don't produce class names at runtime, so they don't appear in the type either.
Only top-level keys that are valid class-name identifiers (letters, digits and underscores — i.e. matching /^\w+$/) get a generated class at runtime. The type can't fully express that pattern, so keys with dashes, spaces or commas (e.g. 'my-card') appear in classes as string but resolve to undefined at runtime. Stick to simple identifiers for top-level class keys.
Style rules use csstype under the hood, so you get autocomplete on standard CSS properties, with null/undefined accepted (and filtered at runtime). Note that values are intentionally permissive (any string is accepted) so that var(...), custom values and arbitrary nested selectors keep working — so this is autocomplete, not strict validation:
css({
card : {
color : 'red',
backgroundColor : null, // ok — filtered at runtime
margin : undefined, // ok
padding : 10, // numbers accepted for length props
},
root : {
color : 'black',
'&:hover' : { color : 'blue' },
'& span' : { fontSize : 14 },
},
'@global' : { body : { margin : 0 } },
'@keyframes wave' : {
'0%, 100%' : { transform : 'rotate(10deg)' },
'50%' : { transform : 'rotate(-10deg)' },
},
});The following types are exported from the package root for use in your own code:
import type {
CSSValue,
CSSProperties,
StyleRule,
Styles,
StyleSheetOptions,
ThemeDefinition,
ThemeVars,
CreateThemeOptions
} from 'cssfun';Complete API documentation can be found here.
For those working with LLMs, there is an AI Agents reference guide that provides API patterns, style syntax, theme management, and best practices, optimized for LLM context. You can share this guide with AI assistants to help them understand CSSFUN's architecture and styling APIs.
The examples folder contains various sample projects demonstrating how to use CSSFUN in
different environments and frameworks. Each example is a standalone project that you can run locally
to see CSSFUN in action.
- React Example: A basic React application demonstrating the use of CSSFUN for styling React components. Try it.
- Rasti Example: A simple Rasti application illustrating how to apply CSSFUN to style Rasti components. Try it.
- Vanilla JS Example: A straightforward JavaScript example showing how to use CSSFUN for styling HTML components. Try it.
CSSFUN is open-source and available under the MIT License.
Contributions are welcome! Share feature ideas or report bugs on our GitHub Issues page.