@djangocfg/ui-core
Pure React UI library with 65+ components built on Radix UI + Tailwind CSS v4.
No Next.js dependencies — works with Electron, Vite, CRA, and any React environment.
Part of DjangoCFG — modern Django framework for production-ready SaaS applications.
Live Demo & Props
Install
pnpm add @djangocfg/ui-core
Why ui-core?
@djangocfg/ui-core | Electron, Vite, CRA, any React app |
@djangocfg/ui-tools | Heavy tools with lazy loading |
@djangocfg/ui-nextjs | Next.js apps (extends ui-core) |
Components (65+)
Components are organized by category in components/. All exports are available from the root:
import { Button, Dialog, Table } from '@djangocfg/ui-core';
Forms (18)
Button ButtonLink ButtonGroup Input Textarea Checkbox RadioGroup Switch Slider Label Form Field InputOTP PhoneInput InputGroup DownloadButton OTPInput Textarea
Select (8)
Select Combobox MultiSelect MultiSelectPro MultiSelectProAsync CountrySelect LanguageSelect
Select Components — All select components now support icons and badges:
Select — Radix primitives with icon on trigger/item, badge on item
Combobox — Searchable single-select with icon + badge in trigger and dropdown
MultiSelectPro — Multi-select with colored badges, icons, animations
See components/select/README.md for full documentation.
Layout (8)
Card Separator Skeleton AspectRatio Sticky ScrollArea Resizable Section
Overlay (9)
Dialog AlertDialog Sheet Drawer Popover HoverCard Tooltip ResponsiveSheet SidePanel
Default TooltipContent styling uses semantic popover tokens (bg-popover, text-popover-foreground, border-border, shadow) — not primary — so hints read as neutral floating UI.
SidePanel — non-modal side drawer for inspector panels, playgrounds, filters. Slides in from the right (side="right", default) or left edge. Unlike Sheet/Drawer, it does NOT lock the rest of the page — surrounding UI stays clickable and focusable. Esc-to-close (toggleable) and swipe-to-close via vaul. Use when you want a slide-in surface that co-exists with the page below; for modal side surfaces, prefer Sheet.
<SidePanel open={open} onOpenChange={setOpen} side="right">
<SidePanel.Content width="440px">
<SidePanel.Header>
<SidePanel.Title>Details</SidePanel.Title>
<SidePanel.Close className="ml-auto" />
</SidePanel.Header>
<SidePanel.Body>…</SidePanel.Body>
<SidePanel.Footer>…</SidePanel.Footer>
</SidePanel.Content>
</SidePanel>
Drawer — modal vaul-based panel that slides in from any edge (top / right / bottom / left). Picks a size from a preset table or takes an explicit CSS length; both are applied via inline style so vaul's first-paint measurement matches the final layout (no inset miscalc).
<Drawer direction="right">
<DrawerTrigger asChild><Button>Open</Button></DrawerTrigger>
<DrawerContent direction="right" size="lg">
<DrawerHeader>
<DrawerTitle>Details</DrawerTitle>
</DrawerHeader>
…
</DrawerContent>
</Drawer>
Size presets — sm md lg xl full. Maps to width for left/right, height for top/bottom:
| sm | min(100vw, 360px) | min(100vh, 240px) |
| md | min(100vw, 480px) | min(100vh, 360px) |
| lg | min(100vw, 640px) | min(100vh, 480px) |
| xl | min(100vw, 800px) | min(100vh, 640px) |
| full | 100vw | 100vh |
If you need an exact size, pass width / height (a CSS length string or number → px). Inline-applied so the vaul measurement is correct on first paint.
Migration note — the default size changed (was hardcoded 280px for left/right, auto for top/bottom). New default is size="md". Pass size="sm" (~360px) for the closest old left/right behavior, or set an explicit width / height.
Navigation (8)
Tabs Accordion Collapsible Command ContextMenu DropdownMenu Menubar NavigationMenu
Data (10)
Table Badge Avatar Progress Calendar Carousel Chart Toggle ToggleGroup DatePicker
Feedback (5)
Alert Toaster (Sonner) Spinner Empty Preloader
Specialized (7)
Kbd TokenIcon Item Portal ImageWithFallback CopyButton CopyField
Hooks (30+)
Hooks are organized by domain inside the package (src/hooks/<group>/).
Public import path is the single barrel:
import { useIsMobile, useScroll, useNavigate } from '@djangocfg/ui-core/hooks';
dom/ — DOM & viewport
useScroll(target?) | Reactive { x, y, direction, isScrolling } for window or any scrollable element. useSyncExternalStore + module-level shared store + rAF throttle + passive listener. |
useScrollPosition(target?) / useScrollDirection(target?) / useIsScrolling(target?) | Single-field variants — re-render only when their slice changes. |
useBodyScrollLock(locked) | Lock body scroll while locked=true; counter-based (multi-consumer safe), iOS-safe via position: fixed fallback. |
useCopy | Copy to clipboard. |
useImageLoader | Image loading state. |
media/ — viewport size
useMediaQuery(query) | Raw media query — pass any CSS query string. Exports BREAKPOINTS constants (Tailwind v4 defaults). |
useIsPhone() | < 640px — phones only. |
useIsMobile() | < 768px — phones + small tablets. |
useIsTabletOrBelow() | < 1024px — phones + tablets. |
state/ — state primitives
useDebounce | Debounce values. |
useDebouncedCallback | Debounced callbacks. |
useLocalStorage / useSessionStorage | Type-safe wrappers with TTL. |
useStoredValue | Unified API over local/session storage. |
device/ — environment detection
useBrowserDetect | Browser detection (Chrome, Safari, in-app browsers, etc.). |
useDeviceDetect | Device detection (mobile, tablet, desktop, OS, etc.). |
useShortcutModLabel() | Returns ⌘ or Ctrl for shortcut hints (Apple vs Windows/Linux); pairs with metaKey || ctrlKey handlers. |
Other groups
feedback/ | useToast, toast (Sonner). |
theme/ | useResolvedTheme — current resolved theme (light/dark/system). |
time/ | useCountdown, useCountdownFromSeconds. |
events/ | useEventListener, events (PubSub bus). |
hotkey/ | useHotkey, HotkeysProvider (react-hotkeys-hook). |
debug/ | useDebugTools. |
router/ | See Router Hooks below — its own subsection. |
import { useMediaQuery, useIsPhone, useIsMobile, BREAKPOINTS } from '@djangocfg/ui-core/hooks'
const isPhone = useIsPhone()
const isMobile = useIsMobile()
const isNarrow = useMediaQuery(`(max-width: ${BREAKPOINTS.sm - 1}px)`)
const isDark = useMediaQuery('(prefers-color-scheme: dark)')
const { direction, isScrolling } = useScroll()
const hideNav = direction === 'down' && isScrolling
Router Hooks
Framework-agnostic navigation primitives — work on plain History API by default, plug into any router via the adapter pattern. Full docs in src/hooks/router/README.md.
useLocation() | Reactive { pathname, search, hash, href }. |
useLocationProperty(get, getSsr) | Subscribe to ONE field — skip re-renders on unrelated changes. |
useNavigate() | navigate, navigateExternal, push, replace, back, forward. |
useQueryParams() | Record-style read/write of ?key=value URL state. |
useQueryState(key, parser) | Typed useState-style hook bound to one URL key (with clearOnDefault). |
useBackOrFallback() | Smart Back that falls back to a route when there's no in-app history. |
useUrlBuilder() | Pure URL assembly: build, withCurrentParams. |
useSmartLink(href) | Make any element a link (cmd-click, middle-click, Enter, Space). |
useIsActive(href) | Boolean for nav-item highlighting. |
useRouter() | Convenience facade composing everything. |
RouterAdapterProvider | Swap the navigation backend. |
parseAsString / parseAsInteger / parseAsFloat / parseAsBoolean / parseAsIsoDate / parseAsStringEnum / parseAsArrayOf / parseAsJson | Parsers for useQueryState. Each has .withDefault(value). |
import {
useNavigate,
useQueryState,
parseAsInteger,
} from '@djangocfg/ui-core/hooks';
const { navigate } = useNavigate();
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
navigate('/products');
setPage((p) => p + 1);
Next.js adapter
In Next apps, mount both adapters once near the root so navigation flows through next/navigation (server components + prefetch keep working) and <Link> delegates to next/link:
import { NextRouterAdapter, NextLinkProvider } from '@djangocfg/ui-core/adapters/nextjs';
<NextRouterAdapter>
<NextLinkProvider>
<App />
</NextLinkProvider>
</NextRouterAdapter>
next is an optional peer dependency — Wails / Electron / Vite consumers don't pull it in. @djangocfg/layouts/BaseApp mounts both adapters automatically.
Theme Palette Hooks
Hooks for accessing theme colors from CSS variables (useful for Canvas, SVG, charts, diagrams, etc.):
useThemePalette() | Full hex color palette from CSS variables |
useThemeColor(var, opacity?) | Single color by CSS var name — lighter alternative |
useStylePresets() | Pre-built { fill, stroke, color } configs for diagrams |
useBoxColors() | Semi-transparent RGBA colors for boxes/containers |
alpha(hex, opacity) | Convert hex color to rgba() string |
import {
useThemePalette,
useThemeColor,
useStylePresets,
useBoxColors,
alpha,
} from '@djangocfg/ui-core/styles/palette';
function MyCanvas() {
const palette = useThemePalette();
ctx.fillStyle = palette.primary;
ctx.fillStyle = alpha(palette.primary, 0.3);
}
function MyWaveform() {
const primary = useThemeColor('primary');
const primaryFaded = useThemeColor('primary', 0.3);
const errorBackground = useThemeColor('destructive', 0.1);
}
function MyChart() {
const presets = useStylePresets();
const boxes = useBoxColors();
return <Chart colors={[presets.success.fill, presets.warning.fill]} />;
}
Color Utilities (HSL conversion)
import { hslToHex, hslToRgbString, hslToRgba } from '@djangocfg/ui-core/styles/palette';
hslToHex('217 91% 60%');
hslToRgbString('217 91% 60%');
hslToRgba('217 91% 60%', 0.5);
Dialog Service
Zustand-powered dialog service replacing native window.alert, window.confirm, window.prompt with shadcn dialogs. Also provides window.dialog.auth() for triggering authentication dialogs.
import { DialogProvider, useDialog } from '@djangocfg/ui-core/lib/dialog-service';
function App() {
return (
<DialogProvider>
<YourApp />
</DialogProvider>
);
}
function Component() {
const { alert, confirm, prompt, auth } = useDialog();
const handleDelete = async () => {
const confirmed = await confirm({
title: 'Delete item?',
message: 'This action cannot be undone.',
variant: 'destructive',
});
if (confirmed) {
}
};
const handleProtected = async () => {
const didAuth = await auth({ message: 'Please sign in to continue' });
if (didAuth) {
}
};
}
window.dialog.alert({ message: 'Hello!' });
const ok = await window.dialog.confirm({ message: 'Are you sure?' });
const name = await window.dialog.prompt({ message: 'Enter your name:' });
const didAuth = await window.dialog.auth({ message: 'Session expired' });
Usage
import { Button, Card, Input } from '@djangocfg/ui-core';
import { toast } from '@djangocfg/ui-core/hooks';
function Example() {
return (
<Card>
<Input placeholder="Email" />
<Button onClick={() => toast.success('Saved!')}>
Submit
</Button>
</Card>
);
}
Electron Usage
import { Button, Dialog, useMediaQuery } from '@djangocfg/ui-core';
import '@djangocfg/ui-core/styles/globals';
function App() {
const isMobile = useMediaQuery('(max-width: 768px)');
return (
<Dialog>
<Button>Open Dialog</Button>
</Dialog>
);
}
Styling (Next.js / Tailwind v4)
In your app's globals.css, import the package styles and add @source directives for every workspace package that ships Tailwind classes. Tailwind v4 does not scan node_modules automatically.
@import "@djangocfg/ui-nextjs/styles";
@import "@djangocfg/layouts/styles";
@import "@djangocfg/ui-tools/styles";
@import "@djangocfg/debuger/styles";
@import "tailwindcss";
Each package that ships Tailwind classes exposes a ./styles entry containing a single @source directive — no manual path configuration needed.
For non-Next.js (Electron, Vite):
import '@djangocfg/ui-core/styles/globals';
Exports
@djangocfg/ui-core | All components & hooks |
@djangocfg/ui-core/components | Components only |
@djangocfg/ui-core/hooks | Hooks only (incl. router hooks) |
@djangocfg/ui-core/adapters/nextjs | <NextRouterAdapter> + <NextLinkProvider> for Next.js apps (optional peer: next) |
@djangocfg/ui-core/lib | Utilities (cn, etc.) |
@djangocfg/ui-core/lib/dialog-service | Dialog service |
@djangocfg/ui-core/utils | Runtime utilities (emitRuntimeError) |
@djangocfg/ui-core/styles | CSS |
@djangocfg/ui-core/styles/palette | Theme palette hooks & utilities |
Runtime Error Emitter
Emit runtime errors as events (caught by ErrorTrackingProvider in layouts):
import { emitRuntimeError } from '@djangocfg/ui-core/utils';
try {
doSomething();
} catch (error) {
emitRuntimeError('MyComponent', 'Operation failed', error, { extra: 'context' });
}
Links
<Link> and <ButtonLink> ship in ui-core itself — no Next.js required.
Default behavior renders <a> and routes clicks through useNavigate.
In Next.js apps, mount NextLinkProvider (from @djangocfg/ui-core/adapters/nextjs)
near the root and the same components delegate to next/link automatically.
@djangocfg/layouts/BaseApp does this for you.
import { Link, ButtonLink } from '@djangocfg/ui-core/components';
<Link href="/about">About</Link>
<ButtonLink href="/docs" variant="outline">Docs</ButtonLink>
What's NOT included (use ui-nextjs)
These features require Next.js or browser storage APIs:
Sidebar — 'use client' heavy, lives in ui-nextjs
Breadcrumb, BreadcrumbNavigation — same
Pagination, SSRPagination — same
DownloadButton — uses localStorage
useTheme — uses next-themes
Requirements
- React >= 18 or >= 19
- Tailwind CSS >= 4
Full documentation & examples