Skip to content
cennge-ui
GitHubv0.3.1, changelog

Provider

CenngeProvider is optional. Wrap the app in it once to set the default size, radius and colour, the locale for numbers and dates, and how much moves.

Without a provider#

Every component works without one. Each then uses its own defaults, the ones in its props table, and a Tooltip mounts the Radix provider it needs by itself. The theme is not a provider prop: dark mode is the dark class and the accent is data-accent, both read by CSS, as Dark mode and Theming describe.

Where it goes#

Put it in the root layout, around the pages and the Toaster. The layout can stay a Server Component: CenngeProvider is a client component, and its props are plain values.

// app/layout.tsx
import type { ReactNode } from "react";
import { CenngeProvider, ThemeScript, Toaster } from "cennge-ui";

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="de" suppressHydrationWarning>
      <head>
        <ThemeScript defaultTheme="dark" />
      </head>
      <body>
        <CenngeProvider locale="de-DE" radius="lg">
          {children}
          <Toaster />
        </CenngeProvider>
      </body>
    </html>
  );
}

Size, radius and colour#

A prop on the component always wins; the provider fills in the ones you leave out. Change the defaults and the whole card follows, though none of its components sets a size, a radius or a colour.

size
radius
Invite to payload-cache
defaults.tsx
import { useState } from "react";
import {
  Button,
  Card,
  CardBody,
  CardFooter,
  CardHeader,
  CenngeProvider,
  Checkbox,
  Input,
  Select,
  SelectItem,
  ToggleGroup,
  ToggleGroupItem,
  type CenngeColor,
  type CenngeRadius,
  type CenngeSize,
} from "cennge-ui";

const sizes: CenngeSize[] = ["sm", "md", "lg"];
const radii: CenngeRadius[] = ["none", "sm", "md", "lg", "full"];
const colors: CenngeColor[] = ["default", "primary", "secondary", "success", "warning", "danger", "info"];

export default function ProviderDefaults() {
  const [size, setSize] = useState<CenngeSize>("md");
  const [radius, setRadius] = useState<CenngeRadius>("md");
  const [color, setColor] = useState<CenngeColor>("primary");

  return (
    <div className="@container w-full">
      <div className="grid gap-6 @xl:grid-cols-[minmax(0,13rem)_minmax(0,1fr)]">
        <div className="flex flex-col gap-4">
          <div className="flex flex-col gap-1.5">
            <span className="text-sm font-medium">size</span>
            <ToggleGroup
              aria-label="size"
              size="sm"
              value={size}
              onValueChange={(value) => value && setSize(value as CenngeSize)}
            >
              {sizes.map((item) => (
                <ToggleGroupItem key={item} value={item}>
                  {item}
                </ToggleGroupItem>
              ))}
            </ToggleGroup>
          </div>
          <div className="flex flex-col gap-1.5">
            <span className="text-sm font-medium">radius</span>
            <ToggleGroup
              aria-label="radius"
              size="sm"
              value={radius}
              onValueChange={(value) => value && setRadius(value as CenngeRadius)}
            >
              {radii.map((item) => (
                <ToggleGroupItem key={item} value={item}>
                  {item}
                </ToggleGroupItem>
              ))}
            </ToggleGroup>
          </div>
          <Select
            label="color"
            size="sm"
            value={color}
            onValueChange={(value) => value && setColor(value as CenngeColor)}
          >
            {colors.map((item) => (
              <SelectItem key={item} value={item}>
                {item}
              </SelectItem>
            ))}
          </Select>
        </div>

        <CenngeProvider size={size} radius={radius} color={color}>
          <Card variant="bordered" className="bg-surface-1">
            <CardHeader className="font-semibold">Invite to payload-cache</CardHeader>
            <CardBody className="flex flex-col gap-4">
              <Input label="Email" type="email" defaultValue="dev@example.com" />
              <Select label="Role" defaultValue="maintain">
                <SelectItem value="read">Read</SelectItem>
                <SelectItem value="write">Write</SelectItem>
                <SelectItem value="maintain">Maintain</SelectItem>
                <SelectItem value="admin">Admin</SelectItem>
              </Select>
              <Checkbox defaultSelected>Can publish to npm</Checkbox>
            </CardBody>
            <CardFooter className="justify-end gap-2">
              <Button variant="light">Cancel</Button>
              <Button variant="solid">Send invite</Button>
            </CardFooter>
          </Card>
        </CenngeProvider>
      </div>
    </div>
  );
}
  • size reaches Button, Badge, ToggleGroup, every field that has a size, Calendar, Table, Tree, Pagination, Snippet and Prose. Tabs, Avatar, Kbd, Spinner, Progress, Stepper, Timeline and the overlays keep their own.
  • radius reaches Button, Card, Pagination, Snippet, the text fields, Select, Combobox and the date and time fields.
  • color is the colour of Button and Badge. The text fields, Select, Combobox, the date and time fields, Checkbox, Radio, Switch, Slider, Calendar and Tree use it for focus and selection, except default: with it they keep primary. Rating and the other components keep their own colour.

Locale#

locale is a BCP 47 tag, en-US unless you set one. It formats and parses NumberInput. It names the months and weekdays and picks the first day of the week in Calendar, DatePicker and DateRangePicker, sets the order of a typed date in the two pickers, and chooses the 12- or 24-hour clock in TimeInput. It also formats file sizes in FileUpload and numbers in charts and on the map, and sets how useTable sorts text. Where a component takes a locale prop of its own, that wins.

It is fixed, never read from the browser, so the server and the browser render the same text and hydration finds nothing to correct. Only the display changes with it: the values below stay 1500000, "2026-11-20" and "14:30" in every locale.

Values

volume
1500000
start
"2026-11-20"
time
"14:30"
locale.tsx
import { useState } from "react";
import { CenngeProvider, DatePicker, NumberInput, TimeInput, ToggleGroup, ToggleGroupItem } from "cennge-ui";

const locales = ["en-US", "de-DE", "fr-FR", "ja-JP"];

export default function AlertSettings() {
  const [locale, setLocale] = useState("en-US");
  const [volume, setVolume] = useState<number | null>(1500000);
  const [start, setStart] = useState<string | null>("2026-11-20");
  const [time, setTime] = useState<string | null>("14:30");

  const values = { volume, start, time };

  return (
    <div className="@container flex w-full flex-col gap-5">
      <ToggleGroup aria-label="Locale" size="sm" value={locale} onValueChange={(value) => value && setLocale(value)}>
        {locales.map((item) => (
          <ToggleGroupItem key={item} value={item}>
            {item}
          </ToggleGroupItem>
        ))}
      </ToggleGroup>
      <div className="grid gap-6 @lg:grid-cols-[minmax(0,1fr)_minmax(0,13rem)]">
        <div className="flex flex-col gap-4">
          <CenngeProvider locale={locale}>
            <NumberInput
              label="Minimum 24h volume"
              value={volume}
              onValueChange={setVolume}
              formatOptions={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }}
              step={10000}
            />
            <DatePicker label="Alerts from" value={start} onValueChange={setStart} />
            <TimeInput label="Daily report at" value={time} onValueChange={setTime} />
          </CenngeProvider>
        </div>
        <section aria-label="Values" className="flex flex-col gap-3 self-start rounded-large bg-surface-3 p-4">
          <h3 className="text-sm font-semibold">Values</h3>
          <dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1.5 font-mono text-[13px]">
            {Object.entries(values).map(([name, value]) => (
              <div key={name} className="contents">
                <dt className="text-muted">{name}</dt>
                <dd>{JSON.stringify(value)}</dd>
              </div>
            ))}
          </dl>
        </section>
      </div>
    </div>
  );
}

Reduced motion#

  • reducedMotion="user", the default, adds nothing: the components already follow the system's reduce motion setting.
  • reducedMotion="always" turns motion off whatever the system says, for a setting in your app, say. It wraps the children in an element with display: contents, and the stylesheet cuts every animation and transition inside it to 0.01 ms. A Carousel in it jumps between slides and does not start autoplay by itself; a Map's controls, clusters and popups move it without animation.

Overlays render in a portal at the end of <body>, outside that element, so they keep their motion: dialogs, drawers, popovers, menus, tooltips, the command palette and the popups of Select, Combobox and the date pickers. Switching between the two values mounts the provider's children again, and their state starts over.

Tooltip delay#

The provider mounts one Radix tooltip provider for everything inside it. tooltipDelay is how long a tooltip waits before it opens, 300 ms by default; a Tooltip's own delay wins. Without a provider, every Tooltip mounts its own, with the same 300 ms.

Nested providers#

An inner provider replaces the outer one for its part of the page; it does not merge with it. What it leaves out falls back to each component's own default, not to the outer provider, so repeat what you want to keep.

<CenngeProvider locale="de-DE" radius="lg">
  <App />
  {/* A dense toolbar: small, and still German with large corners. */}
  <CenngeProvider locale="de-DE" radius="lg" size="sm">
    <Toolbar />
  </CenngeProvider>
</CenngeProvider>

In your own components#

useCenngeDefaults() returns the nearest provider's color, size, radius and locale, or an empty object without a provider, so your components can follow the same defaults.

"use client";

import { useCenngeDefaults } from "cennge-ui";

export function Downloads({ count }: { count: number }) {
  const { locale = "en-US" } = useCenngeDefaults();
  return <span>{new Intl.NumberFormat(locale).format(count)} downloads</span>;
}

Props#

PropTypeDefault
reducedMotion
"always""user""user" follows the OS prefers-reduced-motion setting (components already do this). "always" turns motion off for everything inside the provider.
"user"
tooltipDelay
numberDelay before tooltips open, in ms.
300
children
React.ReactNode
–
color
"default""primary""secondary""success""warning""danger""info"
–
size
"sm""md""lg"
–
radius
"none""sm""md""lg""full"
–
locale
stringBCP 47 locale for number formatting and parsing, e.g. "ru-RU". Fixed (not the browser's) so server and client render the same text.
"en-US"