cssfun
v0.1.0
Published
Near-zero runtime CSS-in-JS library
Maintainers
Readme
Write modular CSS within your JavaScript code with built-in themes and SSR support.
Key Features
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.
Getting Started
Installing via npm
$ npm install cssfunimport { css } from 'cssfun';Using ES modules
import { css } from 'https://esm.run/cssfun';Using <script> tag
<script src="https://cdn.jsdelivr.net/npm/cssfun"></script>const { css } = CSSFUN;Create your styles
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'
}
});Apply the styles to your components:
const Button = () => <button className={classes.button}>Click me</button>;See Class Name Generation for how the names are built.
Class Name Generation
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.
Which selectors get a class
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.
Stable across environments
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.
Name format
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
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:
Camelized keys will be transformed to dashed keys
css({
root : {
backgroundColor : 'black',
fontSize : '16px',
paddingTop : '10px'
}
}).toString();Renders to:
<style data-fun-uid="uwitok">
.fun-uwitok-root {
background-color: black;
font-size: 16px;
padding-top: 10px;
}
</style>Nested selectors will be expanded
Use
&to reference the selector of the parent rulecss({ button : { backgroundColor : 'white', '&:hover' : { backgroundColor : 'black' }, '& span' : { color : 'blue' } } }).toString();Renders to:
<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();Renders to:
<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>
Class references will be replaced by the generated class name
Use
$to reference a local class within the sameStyleSheetinstancecss({ button : { backgroundColor : 'white' }, '$button:hover' : { backgroundColor : 'black' }, '$button span' : { color : 'blue' } }).toString();Renders to:
<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 selectors will be rendered as global styles
Global block
css({ '@global' : { body : { backgroundColor : 'black' } } }).toString();Renders to:
<style data-fun-uid="ml03n3"> body { background-color: black; } </style>Nested global block
css({ root : { '@global' : { a : { color : 'black' } } } }).toString();Renders to:
<style data-fun-uid="1eia2eq"> .fun-1eia2eq-root a { color: black; } </style>Global prefix
css({ '@global body' : { backgroundColor : 'black' } }).toString();Renders to:
<style data-fun-uid="1p1av20"> body { background-color: black; } </style>Nested global prefix
css({ root : { '@global a' : { color : 'black' } } }).toString();Renders to:
<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 stringCustom Renderers
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.
Subclassing & dynamic values
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.
Themes
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.
Creating a Theme
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'
}
});Customizing the Theme
Color Scheme
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.
CSS Variables Prefix
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.
Applying the Theme Class
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);Using Theme Variables in Styles
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>;Server-Side Rendering (SSR)
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.
Content Security Policy (CSP)
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="…">.
Set it on the prototype per request
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.)
Scoped alternative: a subclass with preinitialize
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.
TypeScript
CSSFUN ships with TypeScript declarations out of the box. The types are bundled in the package and resolved automatically.
Requires TypeScript 4.1+ (the
classesinference relies on key remapping and template literal types).
Class name inference
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 existOnly top-level keys that are valid class-name identifiers (letters, digits and underscores — i.e. matching /^\w+$/) get a generated class at runtime, and the type mirrors that: at-rule keys (@global, @keyframes …, @media …, @supports …), class reference keys ($name) and keys containing selector syntax (dashes, spaces, &, :, etc.) are filtered out of classes automatically — they don't produce class names at runtime, so they don't appear in the type either.
CSS property autocomplete
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)' },
},
});Exported types
The following types are exported from the package root for use in your own code:
import type {
CSSValue,
CSSProperties,
StyleRule,
Styles,
StyleSheetOptions,
RendererFn,
Resolvable,
ThemeDefinition,
ThemeVars,
CreateThemeOptions
} from 'cssfun';API Documentation
Complete API documentation can be found here.
Working with LLMs
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.
Examples
The example 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.
Available Examples
- 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.
License
CSSFUN is open-source and available under the MIT License.
Contributing
Contributions are welcome! Share feature ideas or report bugs on our GitHub Issues page.
