A fast, 2Kb alternative to StyleX, offering the same API without requiring a bundler plugin
- đź”® A drop-in replacement for StyleX with react-strict-dom support
- 🚀 Native inline style generation with no build step required
- 🎨 Intuitive and predictable style composition with "last style wins" approach
- 🖥️ RSC and SSG support out of the box, compiled away to zero runtime overhead
- 🔧 Compatible with latest build tools like Turbopack, SWC, Expo Web and Expo
DOM Components (
use dom
) - 🏎️ Optimized for performance in RSC, SSR and CSR environments
- 📦 Tiny runtime footprint in just 2Kb
npm install @nanocss/nanocss
import * as nanocss from '@/lib/nanocss'
const styles = nanocss.create({
root: {
padding: 16,
},
element: {
backgroundColor: {
default: 'red',
':hover': 'blue',
},
},
})
export default function App() {
return (
<div {...nanocss.props(styles.root, styles.element)}>Hello, World!</div>
)
}
Create src/lib/nanocss.ts
with the following contents:
import { nanocss } from 'nanocss'
export const {
props,
create,
inline,
defineVars,
createTheme,
keyframes,
styleSheet,
} = nanocss({
// Hooks defined here can be used inside `create` function.
hooks: [':hover', '@media (max-width: 800px)'],
debug: process.env.NODE_ENV !== 'production',
})
Optional: You can use nanocss
CLI to scan source code to generate hooks for
quick scaffolding or automatic scanning.
nanocss -i "./src/**/*.{ts,tsx}" -o "./src/lib/nanocss.ts"
--watch
option can be used to dynamically generate hooks as well.
Modify src/app/layout.tsx
and add the stylesheet:
export function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<style dangerouslySetInnerHTML={{ __html: styleSheet() }} />
</head>
<body>{children}</body>
</html>
)
}
Create src/app/page.tsx
with the following contents:
import * as nanocss from '@/lib/nanocss'
export default function App() {
return (
<main {...nanocss.props(styles.root)}>
<h1>Title</h1>
<div {...nanocss.props(styles.container)}>Hello, World!</div>
</main>
)
}
const styles = nanocss.create({
root: {
padding: 16,
},
container: {
color: 'blue',
},
})
NanoCSS offers the same API as StyleX and react-strict-dom.
You can configure module aliases in your bundler to resolve @stylexjs/stylex
to NanoCSS and get all of the StyleX ecosystem benefits automatically including
StyleX ESLint
Plugin and react-strict-dom.
Next.js
Add a file named src/lib/stylex.ts
in your project.
import { nanocss } from '@nanocss/nanocss'
const { styleSheet, ...css } = nanocss({
hooks: [
// Add your hooks here
],
debug: process.env.NODE_ENV !== 'production',
})
// For ESM imports
export const { create, props, defineVars, createTheme, keyframes } = css
export { styleSheet } // Add styleSheet() in your layout.ts
export default css
Modify src/app/layout.tsx
and add the stylesheet:
import { styleSheet } from '@/lib/stylex'
export function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<style dangerouslySetInnerHTML={{ __html: styleSheet() }} />
</head>
<body>{children}</body>
</html>
)
}
In your next.config.mjs
, add a module alias resolving @stylexjs/stylex
to src/lib/stylex.ts
const __dirname = new URL('.', import.meta.url).pathname
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['react-strict-dom'], // If you use react-strict-dom, add this file
webpack: (config, { dev, isServer }) => {
config.resolve.alias = {
...(config.resolve.alias || {}),
'@stylexjs/stylex': path.resolve(__dirname, 'src/lib/stylex'),
}
},
}
export default nextConfig
Now, you can author styles using StyleX imports:
import stylex from '@stylexjs/stylex' // This will resolve to NanoCSS
const styles = stylex.create({
root: {
color: 'green',
},
})
Expo Web
Similar to Next.js, NanoCSS can be configured to replace react-strict-dom
and @stylexjs/stylex
in Expo Web with full support for Static
Rendering, Async Routes and
Experimental DOM Components(use dom
) features. Please see the example app for how to configure module aliases in metro.config.js
.
NanoCSS uses an object syntax to represent pseudo-states and media queries per property:
const styles = nanocss.create({
text: {
color: {
default: 'black',
':hover': 'blue',
':focus': 'green',
},
},
})
This flattened structure allows for predictable style merging and overriding.
Compose styles easily using the props function:
<div
{...nanocss.props(
styles.base,
styles.variant,
condition && styles.conditionalStyle
)}
/>
Styles are merged in order, with later styles always taking precedence.
When merging multiple styles with the same property, the last value overrides all previous values including conditions.
This is modeled after Object.assign
behavior.
All styles are generated at runtime, eliminating the need for complex build processes and allowing for flexibility.
NanoCSS works out of the box with SSR and RSC setups. The generated styles are included inline with your HTML, improving First Contentful Paint times.
Create dynamic styles using the function syntax in create
:
const styles = nanocss.create({
color: (color: string) => ({
color,
}),
})
For highly dynamic styles, nanocss.inline
can be used:
function Component() {
return (
<div
{...nanocss.props(
nanocss.inline({
color: {
default: 'red',
':hover': 'blue',
},
})
)}
>
Hello, World!
</div>
)
}
nanocss.inline
must compute and resolve nested conditions during rendering so
it should be used sparingly.
NanoCSS supports defineVars
and createTheme
APIs to define CSS variables
declaratively and override CSS variables for specific UI sub-trees.
The APIs are compatible with StyleX and supports nested conditions for both in
defineVars
and createTheme
.
To understand how these APIs work together, you can refer to StyleX Themes Docs.
Defining CSS Variables
export const colors = nanocss.defineVars({
primary: 'green',
})
Unlike StyleX, variables do not need to be defined in separate .stylex.ts
files and can
be co-located with files defining styles and components. The same restriction
about create
API still apply, and variables must be defined in the top scope
of the files rather than inside of render functions.
Using variables
const styles = nanocss.create({
container: {
color: colors.primary,
},
})
Each CSS variable defined is converted to a unique variable name, and can be
used inside create
functions.
Create themes
const theme = nanocss.createTheme(colors, {
primary: 'red',
})
Themes allow you to override CSS variables defined with defineVars
. All
variables must be overridden.
Using themes
function Component() {
return (
<main {...nanocss.props(theme)}>
<p {...nanocss.props(styles.container)}>Text</p>
</main>
)
}
Themes can be applied via props
and overrides CSS variables of the same
element and its descendant UI sub-trees.
NanoCSS offers built-in support for StyleX keyframes
API for writing CSS
keyframe animations directly in JS, allowing you to create dynamic and complex
animations.
We recommend centralizing your keyframe definitions in a separate file, such as src/styles/animations.ts
:
// src/styles/animations.ts
// File name should be `animations.stylex.ts` in StyleX-compatible mode
import * as nanocss from '@/lib/nanocss'
const { keyframes, defineVars } = nanocss()
export const fadeIn = keyframes({
'0%': { opacity: 0 },
'100%': { opacity: 1 },
})
export const pulse = keyframes({
'0%, 100%': { transform: 'scale(1)' },
'50%': { transform: 'scale(1.1)' },
})
// Define custom variables for your animations
export const animations = defineVars({
fadeIn,
pulse,
})
Modify src/app/layout.tsx
and specify keyframe names to include in the stylesheet:
import { fadeIn, pulse } from '@/styles/animations'
export function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<style
dangerouslySetInnerHTML={{
__html: styleSheet({
keyframes: [fadeIn, pulse],
}),
}}
/>
</head>
<body>{children}</body>
</html>
)
}
To use keyframe animations, simply specify the keyframe name in animationName
prop:
import * as nanocss from '@/lib/nanocss'
import { animations } from '@/styles/animations'
const { create, props, styleSheet } = nanocss()
const styles = create({
root: {
animationName: animations.fadeIn,
animationDuration: '1s',
animationIterationCount: 'infinite',
},
})
export function Component() {
return <div {...props(styles.root)}>Fade in content</div>
}
NanoCSS is designed with performance in mind:
- The
create
function does most of the heavy lifting outside of the render cycle. - The
props
function is optimized for fast merging during rendering. - CSS variables are used to create a small initial stylesheet, reducing the payload size.
- Define styles outside of your components using
create
for better performance. - Leverage style composition for creating variant styles.
- For dynamic styles, prefer conditional styles over style functions in
create
. inline
function should be used sparingly to hoist computation outside rendering.
NanoCSS is built on top of CSS Hooks, which leverages the fallback trick of CSS custom properties to enable pseudo-classes and media queries in inline styles.
Implementation Details
Given the following style definitions:const { create, styleSheet } = nanocss({
hooks: [':hover'],
})
const styles = nanocss.create({
root: {
color: {
default: 'black',
':hover': 'red',
},
},
})
NanoCSS generates the following styleSheet
and styles
:
const styleSheet = () => `
* {
--_hover-0: initial;
--_hover-1: ;
}
*:hover {
--_hover-0: ;
--_hover-1: initial;
}
`
const styles = {
root: {
style: {
color: 'var(--_hover-1, red) var(--_hover-0, black)',
},
},
}
The styleSheet
function generates a small stylesheet with CSS custom properties for each defined hook (e.g., :hover).
The create
function transforms style definitions into flat inline style objects that utilizes the generated CSS custom properties.
Then, the props
function merges multiple nested styles into a single flat style object by iterating over the provided styles and flattening them in the specified order. When encountering conflicting properties, the last style takes precedence, ensuring a predictable "last style wins" approach.
function props(...styles: StyleProp[]): {
className?: string
style?: React.CSSProperties
} {
const style: Record<string, any> = {}
for (const flatStyle of (styles as any[]).flat(Infinity)) {
if (!flatStyle) continue
Object.keys(flatStyle).forEach((key) => {
delete style[key]
style[key] = flatStyle[key]
})
}
return { style }
}
The combination of CSS Hooks and a StyleX-compatible API allows NanoCSS to provide a simple and intuitive way to define and compose styles while ensuring predictable behavior and optimal performance across various rendering scenarios.
NanoCSS offers a unique combination of performance, simplicity, and compatibility with StyleX:
- No build step required, making it easy to integrate into any project
- A drop-in compatibility with StyleX without modifying existing code
- Predictable style composition with a "last style wins" approach
- Optimized for various rendering scenarios (RSC, SSR, CSR)
- Tiny runtime footprint, minimizing impact on your bundle size
NanoCSS is written in TypeScript and provides full type definitions out of the box. This ensures type safety when defining and using styles, catching potential errors at compile-time.