---
title: "@systhemaui/core/client"
description: "Client tokens and the pixel readers (resolveTokenPx, tryResolveTokenPx, isPositiveTokenLength)."
requested_language: fr
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/fr/reference/core-client
version: unreleased (main)
docs_index: https://docs.systhema.app/fr/llms.txt
---
> This page isn't translated yet. Showing English.


`@systhemaui/core/client` is the entry `'use client'` code imports instead of `@systhemaui/core`. Why it exists and how it resolves is on [Client and server code](https://docs.systhema.app/fr/concepts/client-and-server.md); this page covers the client-token snapshot and the readers that turn token values into numbers.

## Client tokens

A handful of components need _resolved_ token scalars in the browser: the variant name lists behind `button-*` / `card-*` / `accordion-*` class names, the gap-token names the Payload block converters index into, the default theme and layout background a page falls back to, and the pixel gaps Swiper wants as JS numbers. Instead of shipping the dataset to compute them, the server resolves them once and forwards them.

`<SysthemaConfigServer />` (rendered by `<SysthemaProvider>`, so every Systhema layout already has it) calls `getSysthemaClientTokens()` and hands the result to `<SysthemaConfigClient />`, which calls `setSysthemaClientTokens()` before any component renders.

```ts
type SysthemaClientTokens = {
  breakpoints: Record<string, string> // { sm: '0px', md: '640px', … }
  scopes: string[] // ['sm', 'md', 'lg']
  values: Record<string, Record<string, string>> // values[tokenPath][scope]
  variants: Record<string, string[]> // variants.button = ['primary', 'secondary']
  themes: string[] // colorSystem names: ['default', 'dark']
  layoutBackgrounds: string[] // first theme's layout backgrounds
}
```

Read it with the helpers rather than the raw object:

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

const variants = clientVariants('button') // ['primary', 'secondary']
const gaps = clientVariants('gap') // ['none', 'xs', 'sm', …]
const gap = clientTokenValue('gallery.slidesGapX', 'md') // '--spacing(12 / 4)'
const bp = clientBreakpoints().lg // '1192px'
const theme = clientThemes()[0] // 'default' — the project default theme
const layoutBg = clientLayoutBackgrounds()[0] // 'main'
```

Read them **at render time, not at module scope** — module bodies run before the snapshot is seeded.

### What the snapshot carries

Only the paths and components declared in `CLIENT_TOKEN_PATHS` / `CLIENT_TOKEN_VARIANTS` are carried. Anything else resolves to `undefined` / `[]` in the browser while still resolving on the server — so it would render one value during SSR and another after hydration. Outside production the server logs a one-time warning naming the missing key; to fix it, add the path or component to those lists in `packages/core/src/clientTokens/types.ts` and refresh `DEFAULT_CLIENT_TOKENS` (a unit test pins the literal to the shipped tokens).

Mount `<SysthemaProvider>` — every Systhema layout and both templates already do. Without it the browser falls back to `DEFAULT_CLIENT_TOKENS`, i.e. Systhema's **built-in** defaults: a project that renames its `button-*`/`card-*`/`accordion-*` variants in the tokens would get the built-in variant names in the browser instead of its own.

## Reading a token value as a pixel number

`clientTokenValue()` returns the token as a **CSS string**, and that string is not reliably a plain length. Paths matched by a spacing rule (`gap.*`, `grid.*`, `container.*`, …) come back as Tailwind's spacing **function** — `'--spacing(12 / 4)'`, not `'12px'` — while a `%screen%`-based spacing config yields a viewport unit such as `'1.6779vw'`. So `parseInt`/`parseFloat` return `NaN` on the common case, and `??` will not catch that `NaN`.

Use `resolveTokenPx(value, fallback)` whenever you need a real number (Swiper's `spaceBetween` is the canonical case — it does its layout math in JS):

```ts
'use client'
import { clientTokenValue, resolveTokenPx } from '@systhemaui/core/client'

resolveTokenPx(clientTokenValue('gallery.slidesGapX', 'md'), 20) // 12
resolveTokenPx('--spacing(20 / 4)', 20) // 20  — core pins --spacing: 4px
resolveTokenPx('1rem', 20) // 16
resolveTokenPx(undefined, 20) // 20  — the fallback
```

It handles `--spacing(…)` calls (evaluating the numeric expression with a small hand-rolled parser — never `eval`), plain `px`/`rem`/`em` lengths, and `vw`/`vh`/`%` against the live viewport. Anything it cannot turn into a finite number — an unset token, an unknown form, or a viewport unit read during **server** rendering — returns the `fallback`, so always pass the design default rather than relying on `??`.

`resolveTokenPx` is exported from the **main `@systhemaui/core` entry as well**, because the same trap catches server-side code. Reach for it wherever a token value has to become a number.

## Picking the right reader

`resolveTokenPx` is deliberately lossy: it cannot tell "genuinely zero" from "could not resolve". That is fine for a layout number with a sane design default, and wrong for a decision. Two siblings cover the other cases:

| You need                                      | Use                               | Because                                                                      |
| --------------------------------------------- | --------------------------------- | ---------------------------------------------------------------------------- |
| A layout number, with a design default        | `resolveTokenPx(value, fallback)` | Ergonomic; the fallback is a legitimate answer.                              |
| A number where "unresolvable" must be handled | `tryResolveTokenPx(value)`        | Returns `number \| null` — a real `0` and an unresolved token stay distinct. |
| A yes/no question about the token's size      | `isPositiveTokenLength(value)`    | Needs no viewport at all, so it answers identically on server and client.    |

```ts
import { isPositiveTokenLength, tryResolveTokenPx } from '@systhemaui/core'

tryResolveTokenPx('0px') // 0     — genuinely zero
tryResolveTokenPx('11.09vw') // null  on the server; pixels in a browser
tryResolveTokenPx('var(--x)') // null

isPositiveTokenLength('--spacing(60 / 4)') // true
isPositiveTokenLength('11.0938vw') // true, with or without a window
isPositiveTokenLength('calc(60 / 1920 * 100vw)') // true — sign-determinable
isPositiveTokenLength('calc(100% - 20px)') // false — indeterminate without layout
isPositiveTokenLength('0vw') // false
```

`isPositiveTokenLength` never converts — every unit Systhema emits scales the token's numeric component by a positive factor, so the sign is the same in every unit and readable anywhere. **Use it for any gate**, and never `resolveTokenPx(value, 0) > 0`: server-side, a viewport-unit token has no pixel answer, collapses to the `0` fallback, and the gate silently reads `false` for a token that is plainly positive. `@systhemaui/payload`'s `hasArticleSidePadding()` — the shared "Container" width gate for six blocks — is built on it for exactly that reason.

All three readers understand the [`calc()` shape](https://docs.systhema.app/fr/concepts/responsive-sizing.md#swap) too, within one deliberate limit: a **pure `*`/`/` chain over a single unit** (`calc(60 / 1920 * 100vw)`) only scales one magnitude, so its sign follows from the numeric chain and it parses. An expression that **adds or subtracts across differing scales** (`calc(100% - 20px)`) needs a real layout to decide which side wins, so it stays unparseable — `null` from `tryResolveTokenPx`, `false` from `isPositiveTokenLength` — rather than being guessed at. A viewport-unit `calc()` behaves like any other `vw` value for pixels: a sign is knowable without a viewport, pixels are not.

## Documentation URLs

`docsUrl(route, anchor?)` builds a public documentation URL for core's installed version. A stable `1.7.5` installation uses `/v/1.7/`; a prerelease uses `/next/`. Pass the page route without `.md` or a trailing `/index`, and pass a heading slug as the optional second argument.

```ts
import { docsUrl } from '@systhemaui/core/client'

const help = docsUrl('cli/doctor', 'checks')
```

The helper is also exported from `@systhemaui/core` and `@systhemaui/core/utils`. Both client implementations share it, and it imports no token data or Node modules.

## Related

- [Client and server code](https://docs.systhema.app/fr/concepts/client-and-server.md)
- [Responsive sizing](https://docs.systhema.app/fr/concepts/responsive-sizing.md)
- [SysthemaProvider](https://docs.systhema.app/fr/components/provider.md)
