@cjournois/ui-components (1.8.0)

Published 2026-09-07 06:11:49 +00:00 by cjournois

Installation

@cjournois:registry=
npm install @cjournois/ui-components@1.8.0
"@cjournois/ui-components": "1.8.0"

About this package

@cjournois/ui-components

Shared React components and design tokens for the apps in this workspace.

It ships its own CSS, in named cascade layers. That is the central design choice: the consumer apps span Tailwind 3, Tailwind 4 and hand-written CSS, so a Tailwind-dependent library could not serve all of them. Your own styles win over the library's automatically — layers are consulted before specificity, and unlayered author styles beat layered ones, so no !important and no @source plumbing.

Install

bun add @cjournois/ui-components
// once, near your app entry
import '@cjournois/ui-components/styles.css';

import { useTheme } from '@cjournois/ui-components';

React 19 is a peer dependency.

What's in it

29 components, grouped by what they are for:

Actions Button, IconButton, TextLink, Menu
Layout Surface, Card, Section, PageHeader, Separator
Status Badge, Chip, StatusDot, Stat, Progress, Avatar
Feedback Alert, Toast, Dialog, ConfirmDialog, SlideOver, Tooltip
States EmptyState, Skeleton, ErrorState
Forms Field, TextField, SearchField, TextArea, Select, Checkbox, Switch, RadioGroup, FormActions
Disclosure Tabs, SegmentedControl, Accordion, Disclosure, Table

The set is deliberately closed. Where accessibility is genuinely hard — focus trapping, typeahead, roving focus, live-region politeness — the behaviour comes from Base UI; everything else is plain markup. There is no data grid, no carousel, no date picker and no icon set, and the reasoning for each omission is written down rather than left to be re-argued.

Theming

Light and dark resolve once, at the token layer, via light-dark() against the computed color-scheme. There is no second stylesheet and no per-component dark variant.

const { theme, resolved, setTheme } = useTheme();   // 'light' | 'dark' | 'system'

useTheme sets data-cj-theme on <html> and persists the choice. Apps that persist a non-system theme should also inline themeScript in <head> to set the attribute before first paint; apps that only ever use system need no JavaScript at all.

If your app uses Tailwind, read this first

The library ships its CSS in cascade layers so that your styles win automatically. A framework reset is the one case where that rule works against you: Tailwind 3 emits its preflight unlayered, and unlayered author styles beat layered ones however specific the layered rule is. The measured result is a library button with no background and no border — silently, in every component that has one.

Put the reset in a layer and leave the utilities alone:

/* index.css — and this file must be imported BEFORE the library's stylesheet */
@layer tw-preflight, cj.reset, cj.base, cj.components, cj.overrides;

@layer tw-preflight {
	@tailwind base;
}

@tailwind components;
@tailwind utilities;
import './index.css';                              // declares the layer order
import '@cjournois/ui-components/styles.css';

The import order is the part that goes wrong. CSS fixes layer order at the first mention of a name. Import the library first and your tw-preflight is appended as the highest layer instead of the lowest, which puts the reset back on top of everything.

Tailwind 4 already emits preflight into @layer base, so it needs the order pinned rather than the wrapper. Either way, verify it: render a <Button> and check it has a background.

Making it yours

An app may override exactly eight tokens, at :root:

:root {
  --cj-accent:         light-dark(#c2410c, #ec3013);
  --cj-accent-hover:   light-dark(#9a3412, #f4552f);
  --cj-accent-soft:    light-dark(#fef2ec, #2a1a16);
  --cj-accent-border:  light-dark(#fcd9c8, #4a2a22);
  --cj-text-on-accent: light-dark(#ffffff, #1a1815);
  --cj-font-sans:      'Archivo', ui-sans-serif, system-ui, sans-serif;  /* see below */
  --cj-font-mono:      'IBM Plex Mono', ui-monospace, monospace;         /* see below */
  --cj-radius-scale:   1;      /* one knob: sharp to round */
}

Three things to get right, all of which fail quietly otherwise:

  • Leave the two font tokens alone. The library now serves Geist Sans and Geist Mono from lib/fonts/, so they default to a face that is actually there. Naming a font you do not ship is how four of the five apps spent months rendering as the system stack while every token read passed — pointing these at a font you have not self-hosted reintroduces exactly that. Override them only with a recorded reason, and only alongside your own @font-face.

  • A colour override must be a light-dark() pair. A flat value opts your accent out of theming while every other token keeps switching.

  • Set them at :root. A var() inside a custom property is substituted where the property is declared, so --cj-radius-scale on a descendant has no effect on the radius ramp.

You only need --cj-accent: accent-soft, accent-border and the focus ring derive from it.

Everything else — text, borders, status colours, spacing, elevation, motion, the type ramp — is the design system's and is identical across apps. That is deliberate: a unified experience over per-app expressiveness.

Surfaces are the one exception, and only for an app whose look is the product. A public-facing app may also override:

:root {
  --cj-surface-page:    hsl(45 23% 95%);   /* the page behind everything */
  --cj-surface-default: hsl(42 20% 97%);   /* cards, panels */
  --cj-surface-raised:  hsl(42 20% 97%);   /* popovers, menus */
}

An internal tool should not: the point of a shared surface ramp is that five apps feel like one.

If you do override a surface, re-check your contrast. The text tokens are tuned against the library's own surfaces, and moving the ground beneath them is exactly how a readable pairing stops being one — both apps that took this route had page colours that put --cj-text-secondary at 4.37:1 and 4.36:1, under AA, and had to lighten by 2%.

className is the escape hatch, on every component.

See also

  • AGENT.md — the working contract: commands, test layers, how to add a component
  • docs/components/ui-components.md (repo root) — which component to reach for, and the pairs that get confused: Alert vs Toast vs Dialog, Tabs vs SegmentedControl, Switch vs Checkbox
  • docs/theming.md — the full theming contract and what is deliberately not overridable
  • demo/ — a real consuming app: component gallery plus four screen templates. just demo

Dependencies

Dependencies

ID Version
@base-ui/react 1.6.0

Development dependencies

ID Version
@biomejs/biome 2.5.1
@cjournois/biome-config-ts ^0.1.0
@cjournois/eslint-config-ts ^0.1.0
@eslint/js ^9.39.4
@happy-dom/global-registrator ^20.11.1
@playwright/test ^1.56.0
@stryker-mutator/core ^9.6.1
@testing-library/dom ^10.4.0
@testing-library/react ^16.3.0
@types/bun 1.3.14
@types/react ^19.0.0
@types/react-dom ^19.0.0
@vitejs/plugin-react ^5.0.0
axe-core ^4.11.0
eslint ^9.39.4
eslint-plugin-import ^2.32.0
globals ^17.4.0
happy-dom ^20.0.0
react ^19.2.4
react-dom ^19.2.4
tsdown ^0.22.0
typescript ^6.0.0
typescript-eslint ^8.58.0
vite ^8.0.16

Peer dependencies

ID Version
react ^19.0.0
react-dom ^19.0.0
Details
npm
2026-09-07 06:11:49 +00:00
0
Corentin JOURNOIS
UNLICENSED
latest
337 KiB
Assets (1)
Versions (19) View all
1.8.0 2026-09-07
1.7.0 2026-08-16
1.6.1 2026-08-16
1.6.0 2026-08-16
1.5.1 2026-08-14