@rqdhw3n/react-admin-layout
v1.0.3
Published
Production-ready admin dashboard layout for React — sidebar, navbar, dark mode, breadcrumbs, RBAC
Maintainers
Readme
@rqdhw3n/react-admin-layout
Production-ready admin dashboard layout for React — sidebar, navbar, dark mode, breadcrumbs, RBAC, mobile drawer.
Features
- Responsive collapsible sidebar
- Mobile drawer menu
- Top navbar with search, notifications, user menu
- Breadcrumbs + auto-generation from route path
- Dark mode with
localStoragepersistence - Light, dark, and live system mode; custom palettes; modern, minimal, and compact styles
- RBAC permission filtering on sidebar items
- React Router support (optional peer)
- Tailwind CSS — modern SaaS aesthetic
- Full TypeScript
Install
npm install @rqdhw3n/react-admin-layout lucide-react
# optional for routing
npm install react-router-domStyles (required)
import '@rqdhw3n/react-admin-layout/style.css'Works with Vite, Next.js (app/layout.tsx or _app.tsx), and CRA.
Next.js
// app/layout.tsx
import '@rqdhw3n/react-admin-layout/style.css'Tailwind in your app
If you use Tailwind v3+, add the package to content:
content: [
'./src/**/*.{js,ts,jsx,tsx}',
'./node_modules/@rqdhw3n/react-admin-layout/dist/**/*.js',
],Basic usage
import { AdminLayout, type SidebarItemType } from '@rqdhw3n/react-admin-layout'
import '@rqdhw3n/react-admin-layout/style.css'
import { Home, Users } from 'lucide-react'
const sidebarItems: SidebarItemType[] = [
{ label: 'Dashboard', icon: Home, path: '/dashboard' },
{
label: 'Users',
icon: Users,
children: [
{ label: 'List', path: '/users' },
{ label: 'Create', path: '/users/create', permission: 'users.create' },
],
},
]
export default function App() {
return (
<AdminLayout
sidebarItems={sidebarItems}
appName='My Admin'
user={{ name: 'Amine', email: '[email protected]' }}
permissions={{ users: ['view', 'create'] }}
activePath='/dashboard'
>
<YourPage />
</AdminLayout>
)
}Theming
Navigation position and separated panels
The layout supports three independent navigation positions and two panel arrangements:
<AdminLayout
sidebarItems={sidebarItems}
sidebarPosition="left" // "left" | "right" | "top"
layoutVariant="detached" // "default" | "detached"
layoutGap={12}
theme={{ mode: 'dark', style: 'modern' }}
>
<YourPage />
</AdminLayout>sidebarPosition="left"is the existing default;"right"places the sidebar on the right."top"renders horizontal navigation above the navbar, with click/keyboard disclosure panels for nested links. Escape and clicking outside close them. The navigation wraps on narrower desktop screens. Sidebar collapse is hidden in this position; its saved state is retained when switching back to a vertical sidebar.layoutVariant="default"keeps the original joined layout."detached"gives the sidebar and the main panel (navbar + content) rounded edges, an outer margin, and a visible gap between them.layoutGapis a nonnegative pixel value (default 12), used only for detached layouts. The actual gap is capped at 5vw to retain usable space on small screens. Invalid nonfinite values use 12.- Below 1024px, all positions use the mobile drawer. The right layout opens the drawer on the right; left and top layouts open it on the left.
All options update immediately when their props change and work with every theme mode and style.
They do not alter the stored theme or collapse preference. The exported types are
AdminLayoutSidebarPosition and AdminLayoutVariant. For example:
const [position, setPosition] = useState<AdminLayoutSidebarPosition>('left')
// Pass sidebarPosition={position}; call setPosition('right') or setPosition('top').To reproduce a dark layout with a contrasting frame like the reference:
theme={{
mode: 'dark',
style: 'modern',
colors: {
background: '#1e293b', // Visible outer frame and gap
surface: '#020617',
sidebarBackground: '#020617',
headerBackground: '#020617',
},
}}The root exposes data-sidebar-position and data-layout-variant. --ral-layout-gap is the CSS
gap token; nested layouts reset their own positioning and panel geometry. The demo includes
position, arrangement and spacing controls.
The optional theme prop adds custom colors and three visual styles to the existing layout.
Keep using sidebarItems, appName, and the other existing props. Import the stylesheet once:
import {
AdminLayout,
type AdminLayoutTheme,
type AdminLayoutThemeMode,
type AdminLayoutStyle,
} from '@rqdhw3n/react-admin-layout'
import '@rqdhw3n/react-admin-layout/styles.css'The original @rqdhw3n/react-admin-layout/style.css import remains supported and points to the same file.
Consumers do not need Tailwind installed to use the shipped styles. Tailwind configuration is only
needed when generating additional utility classes in your own app.
Basic dark mode
<AdminLayout sidebarItems={sidebarItems} theme={{ mode: 'dark' }}>
<YourPage />
</AdminLayout>Built-in style
<AdminLayout sidebarItems={sidebarItems} theme={{ style: 'compact' }}>
<YourPage />
</AdminLayout>Every style supports every mode and palette:
| Style | Sidebar / collapsed | Header | Navigation height | Content spacing | Radius | Appearance |
| --- | --- | --- | --- | --- | --- | --- |
| modern (default) | 260 / 72 px | 64 px | 44 px | 24 px | 10 px | Rounded navigation and cards, subtle shadows |
| minimal | 260 / 72 px | 64 px | 46 px | 32 px | 4 px | Flat surfaces, borders, extra whitespace |
| compact | 220 / 60 px | 52 px | 34 px | 16 px | 6 px | Dense navigation and content, smaller header |
adminLayoutPresets.modern, .minimal, and .compact are immutable, optional convenience objects:
import { adminLayoutPresets } from '@rqdhw3n/react-admin-layout'
<AdminLayout sidebarItems={sidebarItems} theme={{
...adminLayoutPresets.modern,
colors: { primary: '#15803d', activeText: '#ffffff', onPrimary: '#ffffff' },
}}>
<YourPage />
</AdminLayout>Custom colors
<AdminLayout sidebarItems={sidebarItems} theme={{
colors: {
primary: '#7c3aed',
activeText: '#ffffff',
onPrimary: '#ffffff',
sidebarBackground: '#111827',
sidebarText: '#cbd5e1',
},
}}>
<YourPage />
</AdminLayout>All colors accept CSS color values, including CSS variables. Default text/foreground pairs meet
4.5:1 contrast. When providing custom backgrounds, choose readable matching foregrounds:
sidebarText, headerText, hoverText, activeText, onPrimary, and onDanger.
The library does not infer contrast for arbitrary CSS colors.
Custom light/dark
<AdminLayout sidebarItems={sidebarItems} theme={{
mode: 'system',
light: { primary: '#2563eb' },
dark: { primary: '#60a5fa', activeText: '#0f172a', onPrimary: '#0f172a' },
}}>
<YourPage />
</AdminLayout>Resolution order is selected built-in light/dark palette → colors → selected light or dark overrides.
Missing, undefined, and empty color values retain the earlier value. An overridden primary also
sets activeBackground unless an explicit activeBackground override exists. Other color slots
remain independent. Neither the input configuration nor the built-in defaults are mutated.
Runtime switching and the existing toggle
import { useState } from 'react'
import { AdminLayout, type AdminLayoutThemeMode } from '@rqdhw3n/react-admin-layout'
function App() {
const [mode, setMode] = useState<AdminLayoutThemeMode>('system')
return (
<AdminLayout
sidebarItems={sidebarItems}
theme={{ mode, style: 'modern' }}
onThemeModeChange={setMode}
>
<YourPage />
</AdminLayout>
)
}An explicit theme.mode is controlled: it always wins over storage and initialDarkMode.
Prop changes apply immediately. Connect onThemeModeChange to update parent state when the
built-in ThemeToggle, useDarkMode(), or new hook requests a mode. Without that callback,
the controlled mode stays fixed. Toggling from system selects the opposite of the effective mode.
When theme.mode is omitted, the layout manages and persists the mode internally under the
existing storageKey. Existing stored darkMode values and initialDarkMode still work;
the saved state now also supports mode: 'system'. Controlled modes do not overwrite the
saved uncontrolled preference. Use distinct storage keys for independent layout preferences.
import { useAdminLayoutTheme } from '@rqdhw3n/react-admin-layout'
function FollowSystemButton() {
const { mode, resolvedMode, style, colors, setMode } = useAdminLayoutTheme()
return <button onClick={() => setMode('system')}>Follow system ({resolvedMode})</button>
}Render this hook beneath AdminLayout or LayoutProvider. mode is the requested mode;
resolvedMode is always light or dark. The existing useDarkMode() API is unchanged.
System mode subscribes to prefers-color-scheme changes and cleans up its listener. Server
rendering uses light for system mode; hydration then reads the OS preference and stored state.
Blocked storage and unavailable matchMedia are handled safely.
Public TypeScript API
export type AdminLayoutThemeMode = 'light' | 'dark' | 'system'
export type AdminLayoutResolvedMode = 'light' | 'dark'
export type AdminLayoutStyle = 'modern' | 'minimal' | 'compact'
export interface AdminLayoutColors {
primary?: string
secondary?: string
background?: string
surface?: string
text?: string
textSecondary?: string
sidebarBackground?: string
sidebarText?: string
headerBackground?: string
headerText?: string
hoverBackground?: string
hoverText?: string
activeBackground?: string
activeText?: string
border?: string
onPrimary?: string
danger?: string
onDanger?: string
overlay?: string
}
export interface AdminLayoutTheme {
mode?: AdminLayoutThemeMode
style?: AdminLayoutStyle
colors?: AdminLayoutColors
light?: AdminLayoutColors
dark?: AdminLayoutColors
}AdminLayoutProps and LayoutProviderProps add theme?: AdminLayoutTheme and
onThemeModeChange?: (mode: AdminLayoutThemeMode) => void. All existing props remain.
Also exported: ResolvedAdminLayoutTheme, lightThemeColors, darkThemeColors,
adminLayoutPresets, resolveTheme(theme?, systemMode?), and themeToCSSVariables(resolvedTheme).
The pure resolver defaults its system-mode argument to light; automatic OS tracking belongs to the provider.
CSS variables and scope
The root exposes .ral-layout.admin-layout-root, data-theme="light|dark", and
data-style="modern|minimal|compact". Palette variables are applied once at that boundary.
Layouts with an explicit theme have independent colors and density, including nested layouts.
The .dark class is also present on dark layout roots for existing descendant Tailwind utilities.
For backward compatibility, only layouts without any theme prop still synchronize the
document's .dark class. Use theme={{}} to opt into scoped theming with an uncontrolled toggle.
| Palette fields | CSS variables |
| --- | --- |
| primary, secondary | --ral-primary, --ral-secondary |
| background, surface | --ral-background, --ral-surface |
| text, textSecondary | --ral-text, --ral-text-secondary |
| sidebarBackground, sidebarText | --ral-sidebar-bg, --ral-sidebar-text |
| headerBackground, headerText | --ral-header-bg, --ral-header-text |
| hoverBackground, hoverText | --ral-hover-bg, --ral-hover-text |
| activeBackground, activeText | --ral-active-bg, --ral-active-text |
| border, onPrimary | --ral-border, --ral-on-primary |
| danger, onDanger, overlay | --ral-danger, --ral-on-danger, --ral-overlay |
Density tokens include --ral-sidebar-width, --ral-sidebar-collapsed, --ral-header-height,
--ral-radius, --ral-spacing, --ral-nav-item-height, and --ral-shadow.
Existing --admin-* variables remain as aliases. The optional .ral-card class gives consumer
cards the selected surface, border, radius, padding, and shadow. Other consumer components
must use these variables or their own theme integration; arbitrary child markup is not restyled.
.ral-layout.my-layout { --ral-sidebar-width: 280px; }
.my-card { background: var(--ral-surface); color: var(--ral-text); border: 1px solid var(--ral-border); }Use className="my-layout" on AdminLayout and load your overrides after the package CSS.
Transitions respect prefers-reduced-motion. Keyboard focus remains visible, nested navigation
works when collapsed, user menu supports arrow keys/Escape, and the mobile drawer traps and restores focus.
Demo and verification
Development scripts require Node.js 22.18+ (native TypeScript stripping for the CSS generator). This is a contributor requirement; the published package contains compiled JavaScript.
npm install
npm run build
npm run demoOpen http://127.0.0.1:4173. The demo includes light/dark/system, modern/minimal/compact,
default/indigo/blue/green/rose/orange palettes, separate mode palettes, and custom sidebar/header
colors. It consumes public package exports. /?legacy exercises the old uncontrolled API;
/?isolation demonstrates independent nested layouts.
npm run typecheck
npm test
npm run verify:package
npx playwright install chromium
npm run test:browserverify:package packs the local build, installs that tarball into an isolated Vite app under
.tmp, checks every export target, compiles ESM/CommonJS type consumers, and builds the demo.
The browser suite then runs that installed app, including screenshots in test-results.
It tests with React 18 and React Router 6; the library's development build uses Router 7.
On Windows, an existing Edge installation can be used instead of downloading Chromium:
$env:PLAYWRIGHT_CHANNEL = 'msedge'
npm.cmd run test:browserPublishing (manual)
No automatic publishing is configured. For the next minor release from the existing 1.0.1:
npm version minor --no-git-tag-version
npm install
npm run build
npm run typecheck
npm test
npm run verify:package
npm run test:browser
npm pack --dry-run
npm login
npm publish --access publicUse npm.cmd on Windows if PowerShell blocks npm.ps1. The package name remains
@rqdhw3n/react-admin-layout; the version is left unchanged until you run the release commands.
React Router
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { AdminLayout, RouterLink } from '@rqdhw3n/react-admin-layout'
function App() {
return (
<BrowserRouter>
<AdminLayout
enableRouter
LinkComponent={RouterLink}
sidebarItems={items}
appName='My Admin'
>
<Routes>
<Route path='/dashboard' element={<Dashboard />} />
</Routes>
</AdminLayout>
</BrowserRouter>
)
}Hooks
| Hook | Description |
|------|-------------|
| useSidebar() | Collapsed state, mobile drawer |
| useDarkMode() | Theme toggle |
| useAdminLayoutTheme() | Requested/effective mode, palette, style, mode setter |
| useLayoutState() | Full layout context |
| useBreadcrumbs() | Breadcrumb items |
Components
AdminLayout · Sidebar · SidebarItem · SidebarGroup · Navbar · NavbarSearch · UserMenu · NotificationMenu · Breadcrumbs · Footer · MobileSidebar · ThemeToggle · RouterLink
RBAC
permissions={{ users: ['view', 'create'], roles: ['view'] }}
// sidebar item
{ label: 'Create', path: '/users/create', permission: 'users.create' }Examples
examples/basic/App.example.tsxexamples/dark-mode/App.example.tsxexamples/sidebar-collapse/App.example.tsxexamples/theming/App.tsx
Build (library)
npm install
npm run buildOutputs:
dist/index.js(ESM)dist/index.cjs(CJS)dist/index.d.tsdist/index.d.cts(CommonJS declarations)dist/style.css
Peer dependencies
react>= 18react-dom>= 18lucide-react>= 0.400react-router-dom>= 6 (optional)
License
MIT
