Docs
Systhema Design (opens in new tab)
Unreleased

Client and server code

The client-safe entry points and the import rules that keep tokens and the CMS out of visitor bundles.

On this page

Systhema's tokens exist to generate CSS at build time; the browser never needs the token dataset, only the CSS and a few resolved values. This page explains which entry points a 'use client' module may import so the dataset, and in a Payload project the CMS, stay out of visitor bundles.

The rules at a glance:

  • In a 'use client' module, import runtime values from @systhemaui/core/client or @systhemaui/core/utils, never from @systhemaui/core. Type-only imports are fine from either.
  • Read token values in client components from the forwarded snapshot (clientVariants, clientTokenValue), at render time.
  • Keep SysthemaProvider at the root of every layout; it forwards that snapshot.
  • In a Payload project, import client code from the dedicated client entries, never the package root.

The client-safe entryLink to this section

Never import @systhemaui/core from a 'use client' module. The main entry statically reaches handleTokens → the generated token map → all 14 design-token JSON files. JSON modules are whole-module (there is nothing for a bundler to tree-shake), so a single line like

// Wrong: ships the entire design-token dataset to the browser
import { getSysthemaConfigSync } from '@systhemaui/core'

in one client component puts the whole dataset — over a megabyte in a typical project — into the browser bundle. Those tokens exist to generate CSS at build time; they have no runtime job in the browser.

Import from @systhemaui/core/client instead:

// Right: a few KB, no token data
import {
  getSysthemaConfig,
  getSysthemaConfigSync,
  setSysthemaConfigSync,
  getSysthemaCookieConsentConfig,
  getSysthemaClientTokens,
  setSysthemaClientTokens,
  clientBreakpoints,
  clientScopes,
  clientTokenValue,
  clientVariants,
  clientThemes,
  clientLayoutBackgrounds,
  resolveTokenPx,
  // plus the locale helpers: defineSysthemaLocales, isSysthemaLocale,
  // isSysthemaLocalePrefixedOnHost, getSysthemaLocalizedPathname,
  // getSysthemaInternalLocalePathname, getSysthemaPublicHref,
  // getSysthemaLocaleOrigin, getSysthemaLocaleDirection,
  // getSysthemaLocaleMissingBehavior, getSysthemaAlternates
  // plus the pure utils: getFirstKey, toKebabCase, toUrlCase, toSnakeCase,
  // toTitleCase, deepMerge, deepMergeReplace, isObject, isArray
} from '@systhemaui/core/client'

The locale helpers are pure path and host math over a resolved SysthemaLocales contract — no token data, no filesystem — so both implementations share the same module.

Everything the main entry exports as a type (SysthemaConfig, ColorSystem, ButtonVariant, AspectRatio, …) is re-exported here too, so type-only imports can point at either.

How it resolvesLink to this section

./client is a conditional export with two implementations behind one specifier:

ConditionResolves toBacked by
browser (client bundles — Turbopack, webpack, Vite)dist/client.browser.jsThe in-memory config store and the forwarded client-token snapshot
default (Node, RSC, SSR, any other bundler)dist/client.jsThe real systhema.config.* loader and the real token data

So server rendering is byte-identical to reading the tokens directly, while the browser never sees them. If a bundler doesn't understand the browser condition, it falls back to the full implementation — slower, never wrong.

Utilities are client-safeLink to this section

The pure helpers under @systhemaui/core/utils do not reach the token data, so importing from /utils in a client component is fine.

Your own client componentsLink to this section

The same rule applies to your own components: import @systhemaui/core/client (or @systhemaui/core/utils) from them, never the main entry.

If a client component needs a resolved token value, read it from the forwarded snapshot rather than the tokens:

'use client'
import { clientVariants, clientTokenValue } from '@systhemaui/core/client'

export function MyThing({ variant }: { variant?: string }) {
  // Read at render time — module bodies run before the snapshot is seeded.
  const variants = clientVariants('button')
  const resolved = variant ?? variants[0]
  return <button className={`button-${resolved}`} />
}

See Client tokens for the snapshot and its readers.

Swiper is loaded on demandLink to this section

<Carousel> and <GalleryWrapper> keep Swiper (~120 KB) behind lazy(() => import(…)), so a page with no carousel or gallery never fetches it.

While the chunk resolves, a <Suspense> fallback renders the same .swiper / .swiper-wrapper / .swiper-slide structure Swiper produces — Systhema owns that CSS, so the layout is unchanged. The tradeoff: for that moment the slides are a static, non-draggable row (no inline spaceBetween gaps, no transforms). Under SSR the dynamic import resolves on the server, so the published HTML still contains the real markup and there is no content gap for crawlers.

Payload code in client componentsLink to this section

In a Payload project, a 'use client' file imports from the dedicated client entries, never from the package root. See Imports in 'use client' files and Payload import paths.