@shreyajirwankar/dashboard
v1.0.1
Published
Reusable, framework-agnostic React dashboard UI kit — layout shell, navigation, theming and data components for any React + TypeScript application.
Maintainers
Readme
@vrize/dashboard
A reusable, framework-agnostic React dashboard UI kit — layout shell, navigation, theming, and data-display components for any React + TypeScript web app.
The package ships only presentation and layout primitives. It owns no routes, no data fetching, and no business logic — every consuming project supplies its own logo, name, theme, navigation, routes, pages, and data.
Build dashboard once
↓
Publish @vrize/dashboard
↓
────────────────────────────
↓ ↓ ↓
Project A Project B Project C
↓ ↓ ↓
import import import
package package packageInstall
pnpm add @vrize/dashboard react react-domreact and react-dom are peer dependencies (>=18) — the package never
bundles its own copy of React, so it shares the host app's instance and stays
small.
Quick start
1. Import the stylesheet once, near your app's own global CSS:
// main.tsx
import '@vrize/dashboard/styles.css';2. Describe your dashboard — brand, navigation, theme, and how links should render — in one plain object:
// dashboard.config.tsx
import type { DashboardConfig, NavigationItem } from '@vrize/dashboard';
import { Link, useLocation } from 'react-router-dom';
import { HomeIcon, OrdersIcon, SettingsIcon } from './icons'; // your own icons
import logo from './assets/logo.svg';
export const navigation: NavigationItem[] = [
{ id: 'home', label: 'Home', href: '/', icon: <HomeIcon /> },
{
id: 'orders',
label: 'Orders',
href: '/orders',
icon: <OrdersIcon />,
badge: '12',
},
{
id: 'settings',
label: 'Settings',
icon: <SettingsIcon />,
children: [
{ id: 'settings-profile', label: 'Profile', href: '/settings/profile' },
{ id: 'settings-billing', label: 'Billing', href: '/settings/billing' },
],
},
];
export function useDashboardConfig(): DashboardConfig {
const location = useLocation();
return {
brand: {
name: 'Acme Inc.',
logo: <img src={logo} alt="" />,
href: '/',
},
navigation,
activePath: location.pathname,
theme: {
mode: 'system',
colors: { primary: '#4f46e5' },
radius: '10px',
},
// The package never imports a router — you decide how links render.
renderLink: ({ href, children, className, active, onClick }) => (
<Link to={href} className={className} onClick={onClick} aria-current={active ? 'page' : undefined}>
{children}
</Link>
),
};
}3. Wrap your routed content in AppShell:
// App.tsx
import { AppShell } from '@vrize/dashboard';
import { Routes, Route } from 'react-router-dom';
import { useDashboardConfig } from './dashboard.config';
import OrdersPage from './pages/OrdersPage';
import HomePage from './pages/HomePage';
export default function App() {
const config = useDashboardConfig();
return (
<AppShell config={config}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/orders" element={<OrdersPage />} />
</Routes>
</AppShell>
);
}That's it — sidebar, header, active-link highlighting, mobile drawer, and light/dark theming all come from the package. Your project owns every route, page component, and piece of business logic.
Building a page with the components
import {
PageHeader,
StatCard,
Card,
DataTable,
Pagination,
Button,
usePagination,
type ColumnDef,
} from '@vrize/dashboard';
interface Order {
id: string;
customer: string;
total: number;
status: 'paid' | 'pending';
}
const columns: ColumnDef<Order>[] = [
{ id: 'id', header: 'Order', accessor: 'id', sortable: true },
{ id: 'customer', header: 'Customer', accessor: 'customer', sortable: true },
{
id: 'total',
header: 'Total',
accessor: (row) => `$${row.total.toFixed(2)}`,
align: 'right',
sortable: true,
},
];
export default function OrdersPage() {
const orders = useOrders(); // your own data hook — the package has no opinion here
const pagination = usePagination({ total: orders.length, initialPageSize: 10 });
return (
<>
<PageHeader
title="Orders"
description="All orders across every channel."
breadcrumbs={[{ label: 'Home', href: '/' }, { label: 'Orders' }]}
actions={<Button variant="primary">New order</Button>}
/>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
<StatCard label="Revenue" value="$12,400" delta="+4.3%" trend="up" />
<StatCard label="Open orders" value={orders.length} />
<StatCard label="Refund rate" value="1.2%" trend="down" invertTrendColor />
</div>
<Card title="Recent orders">
<DataTable columns={columns} data={pagination.slice(orders)} getRowId={(row) => row.id} />
<Pagination
page={pagination.page}
pageCount={pagination.pageCount}
total={orders.length}
pageSize={pagination.pageSize}
onPageChange={pagination.setPage}
onPageSizeChange={pagination.setPageSize}
/>
</Card>
</>
);
}What each project supplies vs. what the package supplies
| Supplied by your project | Supplied by the package |
| --- | --- |
| Logo, company name, tagline | AppShell layout (sidebar + header + content) |
| Navigation items and routes | Sidebar / Header chrome, mobile drawer, collapse |
| Theme colors, radius, density | Theming engine (CSS variables, light/dark) |
| Router (react-router, Next, TanStack…) | renderLink bridge — no router dependency |
| Pages, data fetching, business logic | Card, StatCard, DataTable, Modal, Dropdown, Tabs, Button, Input, Breadcrumbs, PageHeader, Pagination, Badge, Avatar, EmptyState |
The package is framework-agnostic below the React layer: it has no dependency on any router, state manager, or data-fetching library, so it drops into a Next.js app, a Vite SPA, or a Remix app equally well.
Exports
Layout
AppShell, Sidebar, SidebarToggle, Header,
DashboardProvider, useDashboard, useDashboardOptionalComponents
Card, StatCard, DataTable, Pagination, Modal, Dropdown, Tabs,
Button, Input, Breadcrumbs, PageHeader,
Badge, Avatar, EmptyState, Skeleton, Spinner, ThemeToggleNavigation
NavLink, flattenNavigation, findNavigationTrail, isBranchActive, isSection,
defaultIsItemActiveTheme
ThemeProvider, useTheme, useThemeOptional, useScopeProps,
createThemeVars, themeVarsToCss, lightColors, darkColors, defaultThemeHooks
useDisclosure, usePagination, useMediaQuery, useClickOutside, useLocalStorageTypes
DashboardConfig, NavigationItem, NavigationSection, NavigationConfig,
ThemeConfig, ThemeColors, ThemeMode, ResolvedThemeMode,
BrandConfig, SidebarBehaviour, LinkRenderer, LinkRenderProps,
Size, Tone, Align, SortDirection, SortStateTheming
Every color, radius, spacing, and font is a CSS custom property written by
ThemeProvider (mounted for you by AppShell). Pass a ThemeConfig — no
class overrides or CSS-in-JS wiring required:
const theme: ThemeConfig = {
mode: 'system', // 'light' | 'dark' | 'system'
colors: { primary: '#16a34a' }, // light-mode overrides
darkColors: { primary: '#4ade80' }, // dark-mode-only overrides
radius: '6px',
density: 'compact', // 'comfortable' | 'compact'
sidebarWidth: '280px',
cssVars: { '--dsh-shadow-md': '0 8px 24px rgba(0,0,0,.12)' }, // escape hatch
};Portalled UI (Modal, Dropdown) automatically inherits the same theme scope
even though it renders into document.body.
Routing
The package never imports a router. Every internal link is rendered through
your renderLink function (DashboardConfig.renderLink), so it works
unchanged with React Router, Next.js <Link>, TanStack Router, or a plain
<a> (the default if you omit it).
Icons
The package ships no icon library dependency. Pass any ReactNode — an
<svg>, a lucide-react icon, an emoji — to NavigationItem.icon,
Button.leftIcon, StatCard.icon, and similar props. A small internal icon
set covers only the chrome the components own (chevrons, close button, sort
arrows).
Development
pnpm install
pnpm dev # watch build, unminified
pnpm build # typecheck + production build → dist/
pnpm typecheck # tsc --noEmit onlyBuild output:
dist/
├── index.js ESM bundle
├── index.cjs CommonJS bundle
├── index.d.ts rolled-up type declarations
└── styles.css stylesheet (import explicitly — never auto-injected)Publishing
pnpm build
pnpm publish --access restricted # or your org's registry flowprepublishOnly re-builds automatically, so dist/ is always fresh at
publish time. Only dist/, README.md, and LICENSE ship in the published
tarball — no demo app, no source .tsx, no dev tooling.
