
Dashboard Patterns
- 123 installs
- 213 repo stars
- Updated August 4, 2026
- yonatangross/orchestkit
Design admin dashboards, analytics views, and data-heavy UIs with proven layout, component, and state patterns for SaaS and agent monitoring surfaces.
About
Guides developers through proven dashboard UI patterns in orchestkit including KPI layouts, data tables, charts, filters, and real-time updates for SaaS admin and analytics surfaces.
- KPI card layout grids
- Real-time data visualization patterns
- Filter and drill-down UX
- Role-based dashboard views
- Performance patterns for large datasets
Dashboard Patterns by the numbers
- 123 all-time installs (skills.sh)
- Ranked #1,011 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yonatangross/orchestkit --skill dashboard-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 123 |
|---|---|
| repo stars | ★ 213 |
| Last updated | August 4, 2026 |
| Repository | yonatangross/orchestkit ↗ |
What it does
Design admin dashboards, analytics views, and data-heavy UIs with proven layout, component, and state patterns for SaaS and agent monitoring surfaces.
Files
Dashboard Patterns
Dashboard UI patterns for building admin panels, analytics dashboards, and data-driven interfaces with React.
Layout Patterns
Responsive Dashboard Grid
function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen bg-muted/40">
<aside className="fixed inset-y-0 left-0 z-10 w-64 border-r bg-background">
<Sidebar />
</aside>
<main className="pl-64">
<header className="sticky top-0 z-10 border-b bg-background px-6 py-4">
<DashboardHeader />
</header>
<div className="p-6">
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">{children}</div>
</div>
</main>
</div>
);
}
function DashboardGrid() {
return (
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
<StatCard title="Revenue" value="$45,231" change="+12%" />
<StatCard title="Users" value="2,350" change="+5.2%" />
<StatCard title="Orders" value="1,245" change="+18%" />
<StatCard title="Conversion" value="3.2%" change="-0.4%" />
<div className="col-span-1 sm:col-span-2"><RevenueChart /></div>
<div className="col-span-1 sm:col-span-2"><TrafficChart /></div>
<div className="col-span-full"><RecentOrdersTable /></div>
</div>
);
}Widget Components
Stat Card Widget
import { TrendingUp, TrendingDown } from 'lucide-react';
interface StatCardProps {
title: string;
value: string | number;
change?: string;
changeType?: 'positive' | 'negative' | 'neutral';
icon?: React.ReactNode;
}
function StatCard({ title, value, change, changeType = 'neutral', icon }: StatCardProps) {
return (
<div className="rounded-xl border bg-card p-6">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-muted-foreground">{title}</p>
{icon && <div className="text-muted-foreground">{icon}</div>}
</div>
<div className="mt-2 flex items-baseline gap-2">
<p className="text-3xl font-bold">{value}</p>
{change && (
<span className={cn(
'flex items-center text-sm font-medium',
changeType === 'positive' && 'text-green-600',
changeType === 'negative' && 'text-red-600',
)}>
{changeType === 'positive' && <TrendingUp className="h-4 w-4" />}
{changeType === 'negative' && <TrendingDown className="h-4 w-4" />}
{change}
</span>
)}
</div>
</div>
);
}Widget Registry Pattern
type WidgetType = 'stat' | 'chart' | 'table' | 'list';
interface WidgetConfig {
id: string;
type: WidgetType;
title: string;
span?: number;
props: Record<string, unknown>;
}
const widgetRegistry: Record<WidgetType, React.ComponentType<any>> = {
stat: StatCard,
chart: ChartCard,
table: DataTable,
list: ListWidget,
};
function DashboardWidget({ config }: { config: WidgetConfig }) {
const Component = widgetRegistry[config.type];
if (!Component) return null;
return (
<div style={{ gridColumn: config.span ? `span ${config.span}` : undefined }}>
<Component title={config.title} {...config.props} />
</div>
);
}Real-Time Data Patterns
TanStack Query + SSE
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
function useRealtimeMetrics() {
const queryClient = useQueryClient();
const { data, isLoading } = useQuery({
queryKey: ['metrics'],
queryFn: fetchMetrics,
});
useEffect(() => {
const eventSource = new EventSource('/api/metrics/stream');
eventSource.onmessage = (event) => {
const update = JSON.parse(event.data);
queryClient.setQueryData(['metrics'], (old: Metrics | undefined) => ({
...old,
...update,
}));
};
eventSource.onerror = () => {
eventSource.close();
queryClient.invalidateQueries({ queryKey: ['metrics'] });
};
return () => eventSource.close();
}, [queryClient]);
return { data, isLoading };
}Data Table (TanStack Table)
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
flexRender,
type ColumnDef,
type SortingState,
} from '@tanstack/react-table';
const columns: ColumnDef<Order>[] = [
{ accessorKey: 'id', header: 'Order ID' },
{ accessorKey: 'customer', header: 'Customer' },
{ accessorKey: 'amount', header: 'Amount', cell: ({ getValue }) => `$${getValue<number>().toLocaleString()}` },
{ accessorKey: 'status', header: 'Status', cell: ({ getValue }) => <StatusBadge status={getValue()} /> },
];
function OrdersTable({ data }: { data: Order[] }) {
const [sorting, setSorting] = useState<SortingState>([]);
const [globalFilter, setGlobalFilter] = useState('');
const table = useReactTable({
data,
columns,
state: { sorting, globalFilter },
onSortingChange: setSorting,
onGlobalFilterChange: setGlobalFilter,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
return (
<div>
<input
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
placeholder="Search orders..."
className="mb-4 rounded border px-3 py-2"
/>
<table className="w-full">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id} onClick={header.column.getToggleSortingHandler()} className="cursor-pointer px-4 py-2 text-left">
{flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getIsSorted() === 'asc' && ' ↑'}
{header.column.getIsSorted() === 'desc' && ' ↓'}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id} className="border-t">
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="px-4 py-2">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
<div className="mt-4 flex items-center gap-2">
<button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>Previous</button>
<span>Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}</span>
<button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>Next</button>
</div>
</div>
);
}Skeleton Loading
function DashboardSkeleton() {
return (
<div className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="rounded-xl border bg-card p-6">
<div className="h-4 w-24 animate-pulse rounded bg-muted" />
<div className="mt-2 h-8 w-32 animate-pulse rounded bg-muted" />
</div>
))}
<div className="col-span-full rounded-xl border bg-card p-6">
<div className="mt-4 space-y-2">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-12 animate-pulse rounded bg-muted" />
))}
</div>
</div>
</div>
);
}Anti-Patterns (FORBIDDEN)
// NEVER: Fetch data in every widget independently (duplicated queries)
// NEVER: Re-render entire dashboard on single metric change
// NEVER: Hardcoded dashboard layout (not responsive)
// NEVER: Polling without intervals (infinite loop)
// NEVER: Missing loading states (flash of empty state)
// NEVER: Real-time updates without debounce (100 re-renders/sec)Key Decisions
| Decision | Recommendation |
|---|---|
| Layout | CSS Grid for 2D dashboard layouts |
| Real-time | SSE for server->client, WebSocket for bidirectional |
| Data table | TanStack Table for features |
| State | TanStack Query with granular keys |
| Loading | Skeleton for content areas |
Related Skills
recharts-patterns- Chart components for dashboardstanstack-query-advanced- Data fetching patternsstreaming-api-patterns- SSE and WebSocket implementation
Widget Composition Patterns
Widget Registry Pattern
Create a centralized registry for dynamic widget rendering:
// widget-registry.ts
import type { ComponentType, ReactNode } from 'react';
export interface WidgetConfig {
id: string;
type: string;
title: string;
span?: { cols: number; rows: number };
props?: Record<string, unknown>;
}
type WidgetComponent = ComponentType<{ config: WidgetConfig }>;
const widgetRegistry = new Map<string, WidgetComponent>();
export function registerWidget(type: string, component: WidgetComponent) {
widgetRegistry.set(type, component);
}
export function getWidget(type: string): WidgetComponent | undefined {
return widgetRegistry.get(type);
}Dashboard Grid Layout
Use CSS Grid for responsive dashboard layouts:
// dashboard-grid.tsx
interface DashboardGridProps {
widgets: WidgetConfig[];
columns?: number;
}
export function DashboardGrid({ widgets, columns = 4 }: DashboardGridProps) {
return (
<div
className="grid gap-4"
style={{
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
}}
>
{widgets.map((widget) => {
const Widget = getWidget(widget.type);
if (!Widget) return null;
return (
<div
key={widget.id}
style={{
gridColumn: `span ${widget.span?.cols ?? 1}`,
gridRow: `span ${widget.span?.rows ?? 1}`,
}}
>
<Widget config={widget} />
</div>
);
})}
</div>
);
}User-Customizable Layouts
Persist user layout preferences:
// use-dashboard-layout.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
export function useDashboardLayout(dashboardId: string) {
const queryClient = useQueryClient();
const { data: layout } = useQuery({
queryKey: ['dashboard-layout', dashboardId],
queryFn: () => fetchLayout(dashboardId),
});
const updateLayout = useMutation({
mutationFn: (newLayout: WidgetConfig[]) =>
saveLayout(dashboardId, newLayout),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['dashboard-layout', dashboardId],
});
},
});
return { layout, updateLayout };
}Real-Time Widget Updates
Use SSE for live dashboard data:
// use-widget-stream.ts
export function useWidgetStream(widgetId: string) {
const [data, setData] = useState<WidgetData | null>(null);
useEffect(() => {
const eventSource = new EventSource(`/api/widgets/${widgetId}/stream`);
eventSource.onmessage = (event) => {
setData(JSON.parse(event.data));
};
return () => eventSource.close();
}, [widgetId]);
return data;
}Key Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Layout system | CSS Grid | Native, no dependencies, responsive |
| State persistence | TanStack Query | Caching, background sync |
| Real-time updates | SSE | Simpler than WebSocket for read-only |
| Widget registry | Map-based | Type-safe, tree-shakeable |
import { ReactNode } from 'react';
import { motion } from 'motion/react';
import { fadeIn, cardHover } from '@/lib/animations';
import { cn } from '@/lib/utils';
interface WidgetCardProps {
title: string;
description?: string;
action?: ReactNode;
children: ReactNode;
className?: string;
noPadding?: boolean;
}
/**
* Base widget card component for dashboards
*/
export function WidgetCard({
title,
description,
action,
children,
className,
noPadding = false,
}: WidgetCardProps) {
return (
<motion.div
{...fadeIn}
className={cn('rounded-xl border bg-card shadow-sm', className)}
>
{/* Header */}
<div className="flex items-center justify-between border-b px-6 py-4">
<div>
<h3 className="font-semibold text-card-foreground">{title}</h3>
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
</div>
{action && <div>{action}</div>}
</div>
{/* Content */}
<div className={cn(!noPadding && 'p-6')}>{children}</div>
</motion.div>
);
}
interface StatWidgetProps {
title: string;
value: string | number;
change?: {
value: string;
type: 'positive' | 'negative' | 'neutral';
};
icon?: ReactNode;
className?: string;
}
/**
* Stat/metric widget for KPIs
*/
export function StatWidget({
title,
value,
change,
icon,
className,
}: StatWidgetProps) {
return (
<motion.div
{...fadeIn}
{...cardHover}
className={cn(
'rounded-xl border bg-card p-6 shadow-sm transition-colors',
className
)}
>
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-muted-foreground">{title}</p>
{icon && (
<div className="rounded-lg bg-muted p-2 text-muted-foreground">
{icon}
</div>
)}
</div>
<div className="mt-3 flex items-baseline gap-2">
<p className="text-3xl font-bold tracking-tight">{value}</p>
{change && (
<span
className={cn(
'text-sm font-medium',
change.type === 'positive' && 'text-green-600',
change.type === 'negative' && 'text-red-600',
change.type === 'neutral' && 'text-muted-foreground'
)}
>
{change.type === 'positive' && '↑'}
{change.type === 'negative' && '↓'}
{change.value}
</span>
)}
</div>
</motion.div>
);
}
interface ChartWidgetProps {
title: string;
description?: string;
children: ReactNode;
height?: number;
className?: string;
}
/**
* Chart widget with proper height handling
*/
export function ChartWidget({
title,
description,
children,
height = 300,
className,
}: ChartWidgetProps) {
return (
<WidgetCard
title={title}
description={description}
className={className}
>
<div style={{ height }}>{children}</div>
</WidgetCard>
);
}
// Example usage:
// <StatWidget
// title="Total Revenue"
// value="$45,231"
// change={{ value: "+12%", type: "positive" }}
// icon={<DollarSign className="h-4 w-4" />}
// />
//
// <ChartWidget title="Revenue Trend" height={300}>
// <ResponsiveContainer width="100%" height="100%">
// <LineChart data={data}>...</LineChart>
// </ResponsiveContainer>
// </ChartWidget>