Generic React UI primitives for Devstroop web apps. Presentational, themeable, and project-agnostic — no auth, routing, data, or domain coupling. Composable building blocks that any app styles through design tokens.
Git-tagged distribution (no npm registry). Releases are tagged vX.Y.Z on the
release PR (see docs/DEVELOPMENT_STRATEGY.md):
"dependencies": {
"@devstroop/react-uikit": "github:devstroop/react-uikit#vX.Y.Z"
}To track unreleased main instead of a release, pin the branch or an
exact commit:
"dependencies": {
"@devstroop/react-uikit": "github:devstroop/react-uikit#main"
}npm installImport the stylesheet once, then use components:
import '@devstroop/react-uikit/style.css';
import { Button, Card, FormField, TextBox } from '@devstroop/react-uikit';
export function SignInForm() {
return (
<Card header="Sign in">
<FormField text="Email" helper="We never share it.">
<TextBox id="email" type="email" />
</FormField>
<Button type="submit">Continue</Button>
</Card>
);
}| import | gets you |
|---|---|
@devstroop/react-uikit/style.css |
tokens + utilities + every component sheet — the normal choice, once, in your entry |
@devstroop/react-uikit/tokens.css + …/utilities.css |
the --dx-* sheet and the utility classes alone |
@devstroop/react-uikit/base.css |
opt-in document layer (body font/color/margin, box-sizing), for when text outside components should be themed too |
Consuming the kit from source — aliasing lib/main.ts, the way demos/ and
the parity harness do — means importing lib/styles/tokens.css and
lib/utilities.css in your own entry; demos/main.tsx:2-3 is the reference.
Skip it and a dev build warns on the console instead of rendering quietly
unstyled, and vite dev and vite build now agree about being unthemed.
Every component below is exported from @devstroop/react-uikit; most are
demoed in the demos app (npm run dev → :5199). Props are documented as
JSDoc on each export in the published types.
Layout · Header · Body · Footer · Sidebar · SidebarToggle ·
Row · Column · Stack · AutoGrid
Text (full RadzenText hierarchy — see Text) ·
Icon (674 glyphs across 16 bundled sets — name takes a bare name for
the built-in set or set:glyph for any bundled collection,
size/strokeWidth)
Button (variant filled|flat|outlined|text, severity × shade
axes, size xs..xl, fullWidth, iconOnly, loading, visible) ·
ToggleButton (Button parity + pressed/defaultPressed and the
Radzen toggle axis toggleVariant/toggleSeverity/toggleShade/
toggleContent) · SplitButton (Button-rendered halves, items menu
with icon, loading/fullWidth/openAriaLabel) ·
FabMenu
Form · FormField · Fieldset · TextBox · TextArea · Password ·
Mask · Numeric · Select · DropDown · AutoComplete · ListBox ·
CheckBox · CheckBoxList · RadioButtonList · Switch · Slider ·
Rating · ColorPicker · DatePicker · TimeSpanPicker · SecurityCode ·
Upload · SelectBar · Label · DropZone
CheckBox also exposes an indeterminate (mixed) prop — the browser-native
partially-checked state, wired for pointer and keyboard.
Validation: Form + useFormField with validator helpers (required,
email, pattern, minLength, maxLength, range, compare,
requiredTrue, custom, runValidators). Field and Input remain
exported but are deprecated — prefer FormField and TextBox.
Alert · Progress · Skeleton · EmptyState · Toast (via
ToastProvider + useToast) · Dialog (declarative, or imperative via
DialogProvider + useDialog() → confirm() / alert() promises) ·
Tooltip (wrapper, or document-wide delegation via targetSelector,
optional durationMs auto-dismiss)
Breadcrumb · Link · Menu · PanelMenu · ProfileMenu · Tabs ·
Steps · Toc · Pager · context menus via ContextMenuProvider +
useContextMenu()
Table · DataGrid (sorting, filtering, editing, paging, frozen columns,
column picker/reorder/resize, multi-level grouping, footer aggregates,
row virtualization, CSV export, server-side onRangeChange ranges) ·
DataList · VirtualGrid · Tree ·
PickList · Pivot · Chart (line, area, bar, column, scatter, bubble,
pie, donut, gauge, radar, funnel, heatmap; stacked series via stack) ·
Gantt · Scheduler · Timeline · DataFilter · QRCode ·
Barcode
Card · Badge · Avatar · Stat · Accordion · Carousel ·
Splitter
ThemeToggle (light/dark/system appearance with persistence, applies
data-theme) · ThemeService · tokens below in Theming.
ThemeToggle is the package's dark/light control. Selecting a
runtime theme (fluent / material-3 / zen / …) is a project-level
implementation, not a uikit component — the demos ship theirs as
demos/components/ThemeSwitcher, and nothing in the package exports
a palette picker.
useFormContext · useFormField · useToast · useDialog ·
useContextMenu · useMediaQuery · formatMasked · getByPath
All interactive components: forwardRef, className passthrough, full DOM
attribute support. A11y baseline: semantic elements, aria-invalid on invalid
controls, role="switch" for Switch, keyboard focus-visible rings.
Components consume design tokens exclusively — no hardcoded values. Tokens are hand-maintained in lib/styles/tokens.css (--dx-*), bundled into the published style.css, and also copied to the standalone ./tokens.css / ./utilities.css exports. Nothing imports CSS for you: lib/main.ts deliberately carries no CSS side effect, because a sideEffects glob never survived Rolldown's tree-shaking, so vite dev rendered themed while vite build shipped every var(--dx-*) unresolved (GAP-015).
- Light defaults on
:root; dark values under[data-theme="dark"]on<html>. Hosts without an explicitdata-themeget an OS-dark fallback via@media (prefers-color-scheme: dark)(specificity-capped with:where()so app:rootrules win ties). - Toggle at runtime with the
ThemeTogglecomponent, or setdocument.documentElement.dataset.themeto"light"/"dark"yourself.
The flat values mirror uikit's default scoped theme (--dx-color-*) by
convention — every other text-*/border-*/hue flat here is the exact same
hex. The canonical scoped set ships in the uikit repo (themes/<name>/tokens.css,
generated by scripts/generate-css.mjs, opt-in per framework via
frameworks/<name>/uikit.yml; only default is vendored, see
specs/tokens.md "Distribution & consumption"). React is the flat-first
implementation: it consumes --dx-* directly and never references the scoped
layer, so re-deriving a flat value means mirroring it (plus its --dx-on-*
pair) against the uikit theme and noting it in the uikit issue.
Override any subset on your :root (or a scoped container) — later rules win:
:root {
--dx-primary-color: #7c3aed;
--dx-primary-hover-color: #6d28d9;
--dx-radius-input: 6px;
}Every chromatic hue (primary, secondary, info, success, warning, danger) plus the
achromatic hues (light, base, dark) ships a 5-step Radzen-Shade scale, each in light and
dark themes:
| Token | Meaning |
|---|---|
--dx-{hue}-color |
Default (base) step |
--dx-{hue}-lighter-color |
45% white mix |
--dx-{hue}-light-color |
30% white mix |
--dx-{hue}-dark-color |
15% black mix |
--dx-{hue}-darker-color |
30% black mix |
--dx-on-{hue}-{step}-color |
Foreground for that step (var() alias, drift-proof: pale steps → darker ink, dark steps → light ink, near-white/-black hues → opposite body token) |
Button, Badge, Alert, Progress, SplitButton, and ToggleButton
consume these through their shade prop (lighter | light | dark | darker) on every variant — no brightness() filters. Pale
filled steps pair with dark text automatically; lighter/light text-variant steps suit dark
surfaces. Hover states fall back to the base-hue hover token. Values are M3 approximations
(AA-unverified); keep the three theme blocks (:root, :root[data-theme="dark"], OS fallback)
in sync when overriding.
See lib/styles/tokens.css for the full list (color, radius, space, font, shadow, transition/motion, z-index, control heights).
The demos app only (not the published package) ships five brand
palettes — fluent, material-3, github, material, shadcn — that
re-map the same --dx-* tokens onto each brand's look:
<html data-palette="github" data-theme="dark"></html>- Styles live in
demos/styles/(fluentandmaterial-3sheets are generated from upstream source byscripts/extract-palettes.mjs; the rest are hand-authored token sheets). They are not bundled intostyle.css, so consumers get them by copying those files (or the transform script) into their own app —data-paletteis only a scoping convention. - Load the sheet(s) first, then set
document.documentElement.dataset.palette; remove the attribute to fall back to the default tokens. Dark variants work through the usualdata-theme="dark"blocks (see the demos'sPALETTE_LOADERSfor the pattern).
/* after copying demos/styles/github.css into your app */
:root[data-palette='github'] {
--dx-primary-color: #0969da;
/* … */
}Text covers the full Radzen text hierarchy — DisplayH1..H6, H1..H6,
Subtitle1/2, Body1/2, Button, Caption, Overline via textStyle —
with automatic semantic elements (tagName="Auto" default; override with
Div | Span | P | H1..H6 | A | Button | Pre | Strong) and textAlign
(Left | Right | Center | Justify | Start | End | JustifyAll). The Text
prop takes precedence over children. Pair any style with
a dx-text-* color utility (default | muted | primary | success | warning | danger | info) instead of ad-hoc muted/brand text
classes in products. Anchor-link headings are intentionally unsupported
(no consumer need).
See docs/DEVELOPMENT_STRATEGY.md for branch/PR/release protocol.
npm install
npm run lint # eslint
npm run typecheck # tsc --noEmit (includes e2e/)
npm test # vitest
npm run build # vite lib build → dist/ (es + cjs + d.ts + style.css)
npm run format # prettier --write .
npm run dev # demos app on :5199
npm run test:e2e # playwright + axe against the demos buildMIT