React Server Components

React 19
Box renders on the server with no 'use client', no provider and no stylesheet to import. Its CSS is part of the HTML.

How it works

A Server Component cannot inject styles: there is no effect to run and no document to write to. In this mode Box does not inject anything — each rule it needs comes back as a <style href precedence> element rendered next to the markup, and React 19 hoists those into <head> and dedupes them by href. Styling becomes rendering, which is why it also streams: nothing waits for a commit that Suspense may split.
Nothing to configure
The react-server export condition resolves to a build of Box that calls no hook, schedules no effect and never touches the DOM. Importing the package from a Server Component is the whole setup.
Deduped by content
Every href is a hash of the rule text, and class names are content hashes too — so the class the server resolved is the class the browser bundle resolves, and two Boxes with p={4} produce one style element.
A page with no 'use client'
JSX
// app/page.tsx — a Server Component.
import Flex from '@cronocode/react-box/components/flex';
import { H1, P } from '@cronocode/react-box/components/semantics';

export default function Page() {
  return (
    <Flex d="column" gap={2} p={6} bgColor="slate-50" borderRadius={2} theme={{ dark: { bgColor: 'slate-900' } }}>
      <H1 fontSize={24} fontWeight={600}>Rendered on the server</H1>
      <P color="slate-600" md={{ fontSize: 16 }} hover={{ color: 'sky-500' }}>
        Pseudo-classes, breakpoints and themes all work — they are just more rules.
      </P>
    </Flex>
  );
}
Client components: one line, once
JSX
// app/elementMode.ts — imported by every 'use client' module in the app.
'use client';
import Box from '@cronocode/react-box';

// The client bundle resolves the client Box, which inserts rules through the CSSOM after
// hydration — so an island's CSS would be missing from the server-rendered HTML. This puts
// it back in the markup, on the same emission path the server used.
Box.configure({ sink: 'element' });

Theming without a provider

Box.Theme needs state, storage and a media-query listener, so it stays client-only — but theme rules are ancestor-scoped (.dark .someClass), which means a server component only has to put the theme name on <html>. Switching it later is a DOM write, and createThemeController() is that state machine with no React in it.
The server decides the theme
JSX
// app/layout.tsx — a Server Component.
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className="dark" data-theme="dark">
      <body>{children}</body>
    </html>
  );
}
…and the browser can change it
JSX
// app/themeToggle.tsx — the island that changes it.
'use client';
import { createThemeController } from '@cronocode/react-box/core';

const controller = createThemeController({ storageKey: 'theme', theme: 'dark' });

export default function ThemeToggle() {
  return <button onClick={() => controller.set(controller.theme === 'dark' ? 'light' : 'dark')}>Theme</button>;
}

The cascade comes from @layer

React hoists elements in render order, and atomic rules are shared between Boxes — so element order can never be relied on to keep p losing against px. Every rule is therefore wrapped in a cascade layer, and the base element declares the full layer order once, up front. Layer order beats source order, so this mode is exactly as deterministic as the CSSOM one. Two consequences are worth knowing:
Your own unlayered CSS wins
Unlayered CSS beats every layer, so a plain stylesheet of yours overrides Box props here — which matches where the library puts its <style> element in the other modes.
Call Box.extend() before the first render
CSS appends a layer the first time it meets it, after every layer already named, so a prop registered mid-render sorts after the built-ins.

What stays on the client

  • Hover-callback children ({({ isHover }) => …}) — they need state. The server Box throws a message saying so.
  • Box.Theme, for the same reason. Theme styles are unaffected.
  • The pre-built components that hold state — Dropdown, Tooltip, DataGrid, Checkbox, Select, Form. Their chunks ship a 'use client' banner, so a Server Component can still import one — it just becomes a client boundary. The hook-free ones (Flex, Grid, Button, Textbox, H1, …) render on the server like Box does.
  • React 19 only. On React 18 the elements cannot be hoisted; keep the default sink there.

The example app

examples/next-app in the repository is a Next.js App Router app whose pages are Server Components, with a streamed Suspense boundary, a client island, the theme toggle above and a page of pre-built components rendered by the server. Its smoke test starts the production server and asserts on the HTML that the base element was hoisted, that every class in the markup has its rule in the response, and that the streamed section brought its own CSS with it.
Terminal
npm run build            # the library itself
npm run build:next-app   # packs dist/, installs it, builds the app
npm run smoke:next-app   # 13 assertions against the served HTML