@djangocfg/ui-core
v2.1.466
Published
Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps
Maintainers
Readme

@djangocfg/ui-core
Framework-agnostic React UI library: 70+ shadcn/Radix components on Tailwind v4, semantic theme tokens, palette hooks for Canvas/SVG, plus a router-adapter system that lets the same <Link> / <Sidebar> / <SSRPagination> work under Next.js, Vite, Electron, Wails, or plain React.
Install
pnpm add @djangocfg/ui-coreWorks in any React host. Next.js apps import components from here directly and
add the Next router adapter from @djangocfg/ui-core/adapters/nextjs (see
Router adapters below); Next-specific server utilities (sitemap, health,
OG images, config) live in the separate @djangocfg/nextjs package.
Import styles once at the app root — use the golden path, which pins everything to the right cascade layers so import order can't break layout utilities:
/* FIRST style import in your app entry CSS */
@import "@djangocfg/ui-core/styles/full"; /* Tailwind v4 + tokens + base + utilities, layer-safe */The plain @djangocfg/ui-core/styles entry does NOT import Tailwind and emits
its CSS unlayered — if you use it you own the layer ordering (import
tailwindcss first). See src/styles/README.md § App setup for why. Prefer
…/styles/full.
Quick start
import { UiProviders, Button, Card } from '@djangocfg/ui-core';
<UiProviders>
<Card><Button>Hello</Button></Card>
</UiProviders><UiProviders> mounts Tooltip / Dialog / Toast in the right order and a top-level error Boundary (a crash shows a recoverable fallback, not a white screen). Mount it once at the root — library components (and everything in @djangocfg/ui-tools) trust it to be there and never nest their own (a second TooltipProvider is the canonical "Tooltip must be used within TooltipProvider" trap). Pass onError to forward crashes to your logger, errorFallback for a custom (e.g. i18n) crash screen, or errorBoundary={false} to opt out.
Catalogue
| Group | Examples |
|---|---|
| components/data/ | Avatar · Badge · Card · Table · BalancedText · Skeleton |
| components/forms/ | Button · Input · Textarea · Select · Switch · Checkbox · Slider · Form |
| components/feedback/ | Alert · Toast · Banner · Progress · Spinner |
| components/overlay/ | Dialog · Drawer · Popover · Tooltip · HoverCard · Sheet · ContextMenu · DropdownMenu |
| components/navigation/ | Sidebar · Tabs · Breadcrumb · Pagination · NavigationMenu · Command |
| components/layout/ | Container · Grid · Stack · Separator · ScrollArea · Sticky |
| components/select/ | Combobox · MultiSelect |
| components/effects/ | Glass · Marquee · Backdrop |
| components/specialized/ | Accordion · Collapsible · Toggle · Calendar · DatePicker |
| components/boundary/ | ErrorBoundary |
Imports stay flat — group folders are organisational.
Hooks (/hooks)
| Topic | Hooks |
|---|---|
| dom/ | useSize · useResizeObserver · useMeasure · useMutationObserver · useIntersection |
| device/ | useIsMobile · useIsTouch · useMediaQuery · useOnline · useViewportSize · useOrientation |
| state/ | useLocalStorage · useSessionStorage · useToggle · useCounter · useDebouncedValue |
| events/ | useEventListener · useClickOutside · useKeyPress · useFocusWithin |
| theme/ | useTheme · useResolvedTheme · useThemePreset |
| feedback/ | useToast · useDialog · useClipboard · useConfirm |
| hotkey/ | useHotkey (single key + chord) |
| audio/ | useBeep · useSpeak (Web Speech) |
| tabs/ | useCrossTab (BroadcastChannel coordination) |
| media/ | useMediaPermissions · useUserMedia · useDevices |
| time/ | useNow · useInterval · useTimeout |
| router/ | useLink · useRouter (router-adapter consumer) |
Lib utilities (/lib)
cn(...)—clsx+tailwind-mergeshortcutgetIntensity(value, thresholds)— quantise values into discrete bins (heatmaps, gauges)createLogger()— leveled console loggerdialog-service— imperativeconfirm()/alert()/prompt()returning promisespersist— typed localStorage / sessionStorage hookspretext(subpath@djangocfg/ui-core/lib/pretext) — DOM-free text measurement via @chenglou/pretext; powers<BalancedText>and is the primitive for non-CSS line balancing
Router adapters
The router-aware primitives (Sidebar, Link, SSRPagination, and the
useRouter / useNavigate / useLocation hooks) navigate through a pluggable
adapter, so the same components work under any host. ui-core itself is
router-agnostic and has zero router dependency — pick the adapter for your
host and mount it once near the root.
Which adapter for which host
| Host | Mount this | Extra dependency you install |
|---|---|---|
| Next.js (App Router) | NextRouterAdapter from @djangocfg/ui-core/adapters/nextjs | next (you already have it) |
| Vite / React-Router SPA | ReactRouterProvider from @djangocfg/ui-core/adapters/react-router | react-router ≥7 — add to your app's dependencies |
| Wails / Electron / plain React / Storybook | nothing — the default History-API adapter is used automatically | none (zero deps) |
next and react-router are optional peers: the base package never imports
them. Only the matching sub-path entry does, so the dep is resolved only when
you import that adapter. Never add both — import the one sub-path for your host.
Where to mount it
Both provider adapters wrap a subtree; every ui-core router call inside it flows through your app's router. Mount once, near the root:
// Next.js — inside the client provider stack (below your i18n provider)
import { NextRouterAdapter } from '@djangocfg/ui-core/adapters/nextjs';
<NextRouterAdapter><AppLayout>{children}</AppLayout></NextRouterAdapter>
// Vite / React-Router — INSIDE the router context (an element RouterProvider
// renders, e.g. a layout route wrapping <Outlet/>), NOT around <RouterProvider>.
import { ReactRouterProvider } from '@djangocfg/ui-core/adapters/react-router';
function Shell() {
return <ReactRouterProvider><Layout><Outlet /></Layout></ReactRouterProvider>;
}React-Router gotcha: mounting
ReactRouterProvideraround<RouterProvider>throws — that subtree has no router context, souseNavigatefails. Always mount it inside.
What happens if you skip it
Without an adapter, hooks fall back to the default History-API adapter
(window.history.pushState + window.location). This is intentional and works
everywhere with zero deps — but in a framework host it silently loses the
framework's routing behavior:
- Next.js without
NextRouterAdapter: no RSC refetch on navigation, no route loader, no prefetch. Links still change the URL, but the App Router doesn't react. → always mount it in Next apps. - React-Router without
ReactRouterProvider: programmaticuseNavigate/<Link>mutatewindow.historydirectly and bypass the<Outlet/>swap, so navigation desyncs from the data router. → always mount it in RR SPAs.
For a plain SPA (Wails/Electron/CRA) the History-API default is the right backend — mount nothing.
Link adapters (optional, Next.js)
By default <Link> from @djangocfg/ui-core/components renders a plain <a>.
To route Next's prefetch / RSC handling through it, also mount NextLinkProvider
alongside NextRouterAdapter. For locale-prefixed hrefs with next-intl, pass
createNextIntlLinkAdapter(IntlLink) as AppLayout's linkAdapter — both live
in @djangocfg/ui-core/adapters/nextjs.
Deeper dive: full adapter reference, the
useRouter/useNavigate/useLocation/useQueryStatehook surface, and a ~20-line recipe for custom routers (TanStack Router, wouter, Remix, custom transports) live insrc/hooks/router/README.md.
Using
@djangocfg/layouts?BaseAppalready mounts the Next adapters for you — you don't wire them by hand.
Theming (/styles)
Tailwind v4 with semantic tokens, not raw color scales:
<Card className="bg-card border-border text-foreground" />
<Button className="bg-primary text-primary-foreground" />
<Alert className="bg-warning-background text-warning-foreground border-warning-border" />Tokens live in :root / .dark as fully-wrapped CSS colors; @theme inline exposes them as --color-X references, so opacity modifiers (bg-card/40, border-foreground/20) resolve via color-mix for every semantic token.
Preset text/fill pairs target WCAG AA for compact product UI. For the macOS
preset this is enforced by pnpm check:contrast; primary and destructive
foregrounds intentionally switch between light and dark ink where the system
fill's luminance requires it.
@custom-variant dark (&:where(.dark, .dark *)) binds the dark: variant to the .dark class on <html> (not prefers-color-scheme) — every theme-switcher in this monorepo toggles that class.
Tailwind's text-* size utilities are bridged to the preset --font-size-* scale. Product-specific density belongs in a small CSS override after the static preset import — never in a runtime token builder. See src/styles/README.md.
Programmatic theme colors for Canvas / SVG / Mermaid:
import { useThemeColor, alpha, useStylePresets } from '@djangocfg/ui-core/styles/palette';
const primary = useThemeColor('primary'); // #00d9ff (hex, not oklch)
const dim = alpha(primary, 0.15); // 'rgba(0, 217, 255, 0.15)'
const { success, warning, danger } = useStylePresets();Always hex-strings — color-mix(...) / oklch(...) syntax is rejected by Canvas2D fillStyle.
Live playground covers all tokens, presets, and dark-mode pairs.
Maintenance rule
After any change to components or hooks — update this README and bump the package patch version. Consumers pin to npm versions; surface drift in this file is the canonical changelog.
Requirements
- React 18 or 19
- Tailwind CSS v4 (host imports
@djangocfg/ui-core/styles)
License
MIT — © djangocfg.
