@cjournois/ui-components (1.5.0)
Installation
@cjournois:registry=npm install @cjournois/ui-components@1.5.0"@cjournois/ui-components": "1.5.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;
--cj-font-mono: 'IBM Plex Mono', ui-monospace, monospace;
--cj-radius-scale: 1; /* one knob: sharp to round */
}
Two things to get right, both of which fail quietly otherwise:
- 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. Avar()inside a custom property is substituted where the property is declared, so--cj-radius-scaleon 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 componentdocs/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 Checkboxdocs/theming.md— the full theming contract and what is deliberately not overridabledemo/— 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 |