Skip to content

A fast, 2Kb alternative to StyleX, offering the same API without requiring a bundler plugin

Notifications You must be signed in to change notification settings

javascripter/nanocss

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

32 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NanoCSS

A fast, 2Kb alternative to StyleX, offering the same API without requiring a bundler plugin

Open in StackBlitz

Features

  • đź”® 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

Installation

npm install @nanocss/nanocss

Quick Start

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>
  )
}

Usage

As a standalone library

1. Set up NanoCSS

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.

2. Add stylesheet

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>
  )
}

3. Define styles

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',
  },
})

As a drop-in replacement of StyleX

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

1. Create src/lib/stylex.ts as a compatibility layer

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

2. Add stylesheet

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>
  )
}

3. Configure module aliases

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.

Core Concepts

Flattened Conditional Styles

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.

Style Composition

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.

Runtime Generation

All styles are generated at runtime, eliminating the need for complex build processes and allowing for flexibility.

Server-Side Rendering

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.

Dynamic Styles

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.

Themes

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.

Keyframes

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.

1. Defining keyframes

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,
})

2. Add styleSheet

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>
  )
}

3. Defining styles

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>
}

Performance Considerations

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.

Best Practices

  1. Define styles outside of your components using create for better performance.
  2. Leverage style composition for creating variant styles.
  3. For dynamic styles, prefer conditional styles over style functions in create.
  4. inline function should be used sparingly to hoist computation outside rendering.

How does it work?

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.

Why NanoCSS?

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

TypeScript Support

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.

Acknowledgements

  • CSS Hooks for bringing advanced CSS capabilities including pseudo classes and media queries to native inline styles.
  • StyleX for the simple and powerful API design for predictable and deterministic styling.

About

A fast, 2Kb alternative to StyleX, offering the same API without requiring a bundler plugin

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published