react-dashboard-grid
v1.0.4
Published
A flexible, themeable React dashboard library with drag-and-drop layout, a rich widget library, and a fully functional Add Widget modal. Built on top of react-grid-layout, it provides a powerful and customizable solution for building complex dashboards wi
Maintainers
Readme
react-dashboard-grid
react-dashboard-grid is a React component library for building interactive, data-rich dashboards. It ships with a complete set of built-in widgets - stat cards, charts, tables, gauges, calendars, and more - alongside a drag-and-drop layout engine, a live theming system, and a modal for adding widgets at runtime. Every widget is sized and positioned on a 12-column grid, so dashboards compose predictably whether you're building an analytics product, an ops tool, or an internal admin panel. The library is written in TypeScript and exports full type definitions.
Demo
Check out the interactive components on Storybook.
Table of Contents
- Installation
- Quick Start
- Dashboard Props
- Widget Layout and Positioning
- Add Widget Modal
- Widget Reference
- Theming
- Widget Sizing
- Drag, Resize & Edit Mode
- Full Example
- TypeScript Exports
- FAQ
Installation
npm install react-dashboard-gridQuick Start
import { Dashboard } from "react-dashboard-grid";
import type { WidgetItem } from "react-dashboard-grid";
const items: WidgetItem[] = [
{
id: "revenue",
type: "stat",
size: "small",
data: {
title: "Revenue",
value: "$48,295",
change: "+12.4%",
changeType: "up",
},
},
{
id: "sales-chart",
type: "chart",
size: "medium",
data: {
chartType: "line",
title: "Monthly Sales",
data: [
{ label: "Jan", value: 30 },
{ label: "Feb", value: 55 },
{ label: "Mar", value: 40 },
{ label: "Apr", value: 70 },
],
},
},
];
export default function App() {
return (
<Dashboard
title="My Dashboard"
subtitle="Overview of key metrics"
items={items}
/>
);
}<Dashboard> Props
| Prop | Type | Default | Description |
| -------------------- | ---------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| items | WidgetItem[] | required | Initial widget list. Dashboard manages its own state internally — new widgets are appended without requiring parent re-render. |
| title | string | | Header title |
| subtitle | string | | Header subtitle |
| showAddWidget | boolean | true | Show "+ Add Widget" button |
| showEditLayout | boolean | true | Show "Edit Layout" toggle |
| showSearch | boolean | true | Show search input |
| headerActions | DashboardAction[] | | Extra buttons rendered in the header |
| onLayoutChange | (layout: WidgetLayoutChange[]) => void | | Fires after drag/resize; receives clean backend-ready payload |
| onAddWidget | () => void | | Side-effect callback — fires after a widget is added. Dashboard handles state internally. |
| onSearch | (query: string) => void | | Fires on search input change |
| theme | DashboardTheme | see defaults | Theme object (see Theming section) |
| className | string | | Root element class |
| widgetTemplates | WidgetTemplate[] | DEFAULT_TEMPLATES | Templates for the Add Widget modal |
| searchLabel | string | "Search widgets..." | Placeholder for search input |
| editLabel | string | "Edit Layout" | Edit button label |
| editingLabel | string | "Done" | Edit button label when active |
| addLabel | string | "+ Add Widget" | Add widget button label |
| disableEditingGrid | boolean | false | Suppress grid overlay while editing |
Widget Layout and Positioning
Every WidgetItem accepts an optional layout property that controls where it appears on the grid. The dashboard uses a 12-column grid, and each widget occupies a number of columns determined by its size (or customSize). The layout object lets you pin a widget to a specific column (x) and row (y).
interface WidgetLayout {
x: number; // Column index (0–11). 0 = left edge.
y: number; // Row index. 0 = top. Rows are unitless — each unit corresponds to one row-height slot.
}x is the starting column, zero-indexed from the left edge. A small widget is 3 columns wide, so setting x: 9 places it flush against the right edge of a 12-column grid. A large widget is 12 columns wide, so it always starts at x: 0.
y is the starting row. Row heights are determined by the widget's size: a small widget is 2 row units tall, medium is 3, and large is 4. To place two rows of small widgets without overlap, the second row should start at y: 2.
Here is a worked example from the Storybook showcasing a typical 4-up stat row followed by a chart row:
const items: WidgetItem[] = [
// Four stat cards across the top — each 3 columns wide, starting at y: 0
{ id: "s1", type: "stat", size: "small", layout: { x: 0, y: 0 }, data: { ... } },
{ id: "s2", type: "stat", size: "small", layout: { x: 3, y: 0 }, data: { ... } },
{ id: "s3", type: "stat", size: "small", layout: { x: 6, y: 0 }, data: { ... } },
{ id: "s4", type: "stat", size: "small", layout: { x: 9, y: 0 }, data: { ... } },
// Two medium charts on the next row — small widgets are 2 rows tall, so y: 2
{ id: "c1", type: "chart", size: "medium", layout: { x: 0, y: 2 }, data: { ... } },
{ id: "c2", type: "chart", size: "medium", layout: { x: 6, y: 2 }, data: { ... } },
];If you omit layout, the grid engine places widgets automatically in left-to-right, top-to-bottom order. This is fine for simple dashboards, but you should provide explicit layout values whenever you want consistent positioning — especially if widgets will be added or removed dynamically.
After a user drags or resizes widgets, the updated positions are reported via onLayoutChange, so you can persist them and restore the same positions on next load.
Add Widget Modal
The AddWidgetModal is driven by programmer-supplied templates. Each template includes the complete, pre-configured WidgetItem data (minus id) that will be cloned onto the dashboard when the user clicks "Add to Dashboard".
WidgetTemplate interface
interface WidgetTemplate {
key: string; // Unique identifier for this template
label: string; // Display name in the modal
description: string; // One-sentence description
component: Omit<WidgetItem, "id">; // The complete, pre-configured widget data
}Custom templates
import { Dashboard } from "react-dashboard-grid";
import type { WidgetTemplate } from "react-dashboard-grid";
const MY_TEMPLATES: WidgetTemplate[] = [
{
key: "monthly-revenue",
label: "Monthly Revenue",
description: "Tracks total revenue across the current month.",
component: {
type: "stat",
size: "small",
data: {
title: "Monthly Revenue",
value: "$0",
change: "vs last month",
changeType: "neutral",
},
},
},
{
key: "conversion-funnel",
label: "Conversion Funnel",
description: "Bar chart showing drop-off at each funnel stage.",
component: {
type: "chart",
size: "medium",
data: {
chartType: "bar",
title: "Conversion Funnel",
data: [
{ label: "Visits", value: 10000 },
{ label: "Signups", value: 4500 },
{ label: "Activated", value: 2100 },
{ label: "Paid", value: 800 },
],
},
},
},
{
key: "team-members",
label: "Team List",
description: "Scrollable list of team members with role badges.",
component: {
type: "list",
size: "medium",
data: {
title: "Team Members",
items: [
{
id: "1",
label: "Alice Johnson",
badge: "Admin",
badgeColor: "#6366f1",
},
{
id: "2",
label: "Bob Chen",
badge: "Editor",
badgeColor: "#22c55e",
},
],
},
},
},
];
export default function App() {
return (
<Dashboard title="Analytics" items={[]} widgetTemplates={MY_TEMPLATES} />
);
}Using DEFAULT_TEMPLATES
If you do not pass widgetTemplates, the modal uses DEFAULT_TEMPLATES which covers every built-in widget type:
import { DEFAULT_TEMPLATES } from "react-dashboard-grid";
const templates = [
...DEFAULT_TEMPLATES,
{
key: "custom-kpi",
label: "Custom KPI",
description: "Your custom KPI widget.",
component: {
type: "stat",
size: "small",
data: { title: "KPI", value: "—" },
},
},
];Standalone modal usage
import { AddWidgetModal } from "react-dashboard-grid";
import type { WidgetItem } from "react-dashboard-grid";
function MyPage() {
const [open, setOpen] = useState(false);
const [widgets, setWidgets] = useState<WidgetItem[]>([]);
return (
<>
<button onClick={() => setOpen(true)}>Add Widget</button>
<AddWidgetModal
isOpen={open}
onClose={() => setOpen(false)}
onAdd={(widget) => setWidgets((prev) => [...prev, widget])}
primaryColor="#6366f1"
/>
</>
);
}Widget Reference
stat - Stat Card
Displays a single KPI metric with an optional trend arrow and icon.
{
id: "revenue",
type: "stat",
size: "small",
data: {
title: "Monthly Revenue",
value: "$48,295",
change: "+12.4% vs last month",
changeType: "up", // "up" | "down" | "neutral"
icon: <SomeIcon />, // optional React node
},
}StatData props:
| Prop | Type | Description |
| ------------ | ----------------------------- | ------------------------- |
| title | string | Metric label |
| value | string \| number | Main displayed value |
| change | string | Optional trend text |
| changeType | "up" \| "down" \| "neutral" | Controls arrow and color |
| icon | ReactNode | Optional icon (top-right) |
chart - Chart Card
Supports 5 chart types via a single unified chartType discriminator.
Line Chart
{
id: "sales-trend",
type: "chart",
size: "medium",
data: {
chartType: "line",
title: "Sales Trend",
subtitle: "Last 6 months",
data: [
{ label: "Jan", value: 400, value2: 300 },
{ label: "Feb", value: 600, value2: 450 },
],
showGrid: true,
showLegend: true,
seriesLabels: { value: "Revenue", value2: "Costs" },
colors: { primary: "#6366f1", secondary: "#f59e0b" },
},
}Bar Chart
{
id: "quarterly",
type: "chart",
size: "medium",
data: {
chartType: "bar",
title: "Quarterly Performance",
stacked: false,
data: [
{ label: "Q1", value: 120, value2: 80 },
{ label: "Q2", value: 180, value2: 110 },
],
},
}Area Chart
{
id: "traffic",
type: "chart",
size: "medium",
data: {
chartType: "area",
title: "Site Traffic",
stacked: false,
data: [{ label: "Mon", value: 500 }, { label: "Tue", value: 750 }],
},
}Pie / Donut Chart
{
id: "channels",
type: "chart",
size: "medium",
data: {
chartType: "donut", // or "pie"
title: "Traffic Sources",
showLegend: true,
data: [
{ label: "Organic", value: 45, color: "#6366f1" },
{ label: "Direct", value: 30, color: "#22c55e" },
{ label: "Referral", value: 25, color: "#f59e0b" },
],
},
}Histogram
{
id: "distribution",
type: "chart",
size: "medium",
data: {
chartType: "histogram",
title: "Score Distribution",
data: [
{ label: "0–20", value: 5 },
{ label: "20–40", value: 15 },
{ label: "40–60", value: 32 },
{ label: "60–80", value: 28 },
{ label: "80–100", value: 12 },
],
},
}ChartData shared props:
| Prop | Type | Description |
| -------------- | ------------------ | --------------------------------------------- |
| title | string | Card title |
| subtitle | string | Optional subtitle |
| data | ChartDataPoint[] | Array of { label, value, value2?, value3? } |
| colors | ChartColors | Override series colors |
| showGrid | boolean | Show grid lines (default: true) |
| showLegend | boolean | Show legend (default: false) |
| seriesLabels | object | Labels for multi-series legend |
list - List Card
Scrollable list of labelled rows with optional values, badges, and icons.
{
id: "top-pages",
type: "list",
size: "medium",
data: {
title: "Top Pages",
items: [
{ id: "1", label: "/home", value: "12,400 views", badge: "↑8%", badgeColor: "#22c55e" },
{ id: "2", label: "/pricing", value: "8,200 views", badge: "↓2%", badgeColor: "#ef4444" },
{ id: "3", label: "/docs", value: "5,600 views" },
],
},
}ListItem props:
| Prop | Type | Description |
| ------------ | ----------- | ------------------------- |
| id | string | Unique row key |
| label | string | Row label |
| value | string | Optional right-side value |
| badge | string | Optional badge text |
| badgeColor | string | Badge background color |
| icon | ReactNode | Optional leading icon |
table - Data Table
Sortable, paginated table for dense data.
{
id: "orders",
type: "table",
size: "large",
data: {
title: "Recent Orders",
subtitle: "Last 30 days",
sortable: true,
striped: true,
rowsPerPage: 5,
columns: [
{ key: "id", label: "Order ID", width: 100 },
{ key: "customer", label: "Customer" },
{ key: "total", label: "Total", align: "right", render: (v) => `$${v}` },
{ key: "status", label: "Status", render: (v) => <StatusBadge status={v} /> },
],
rows: [
{ id: "#1001", customer: "Alice", total: 299, status: "paid" },
{ id: "#1002", customer: "Bob", total: 149, status: "pending" },
],
},
}TableData props:
| Prop | Type | Description |
| ------------- | ----------------------- | -------------------------- |
| title | string | Card title |
| subtitle | string | Optional subtitle |
| columns | TableColumn[] | Column definitions |
| rows | Record<string, any>[] | Row data |
| sortable | boolean | Enable column sorting |
| striped | boolean | Alternate row shading |
| rowsPerPage | number | Rows per page (default: 5) |
TableColumn props:
| Prop | Type | Description |
| -------- | ------------------------------- | -------------------- |
| key | string | Row data key |
| label | string | Column header |
| width | string \| number | Column width |
| align | "left" \| "center" \| "right" | Cell alignment |
| render | (value, row) => ReactNode | Custom cell renderer |
gauge - Gauge Card
Semicircle gauge with a percentage fill and needle.
{
id: "cpu",
type: "gauge",
size: "small",
data: {
title: "CPU Usage",
subtitle: "Average across cores",
value: "73%",
label: "utilisation",
percentage: 73, // 0–100
changeText: "↑4% from last hour",
color: "#6366f1", // optional override
},
}profile - Profile Card
User card with avatar, name, role badge, and optional stat row.
{
id: "user-profile",
type: "profile",
size: "small",
data: {
name: "Sarah Chen",
email: "[email protected]",
role: "Senior Engineer",
badgeColor: "#6366f1",
avatarUrl: "https://example.com/avatar.jpg",
// avatarEmoji: "👩💻", // fallback if no URL
stats: [
{ label: "PRs", value: 142 },
{ label: "Reviews", value: 89 },
{ label: "Issues", value: 24 },
],
},
}splitstat - Split Progress Card
Main value with a labelled multi-segment stacked progress bar.
{
id: "disk-usage",
type: "splitstat",
size: "small",
data: {
title: "Storage Used",
titleAction: "Manage",
value: "284",
unit: "GB",
minValue: "0 GB",
maxValue: "500 GB",
segments: [
{ label: "Documents", percentage: 40, value: "114 GB", color: "#6366f1" },
{ label: "Media", percentage: 35, value: "99 GB", color: "#22c55e" },
{ label: "Backups", percentage: 25, value: "71 GB", color: "#f59e0b" },
],
},
}calendar - Calendar Card
Monthly calendar with event dot indicators.
{
id: "schedule",
type: "calendar",
size: "medium",
data: {
title: "Schedule",
highlightToday: true,
primaryColor: "#6366f1",
onDateSelect: (date) => console.log("Selected:", date),
events: [
{ date: "2024-04-15", label: "Sprint Review", color: "#6366f1" },
{ date: "2024-04-20", label: "Release", color: "#22c55e" },
{ date: "2024-04-20", label: "Standup", color: "#f59e0b" },
],
},
}text - Text Block Card
Free-form text or notes widget.
{
id: "notes",
type: "text",
size: "small",
data: {
title: "Notes",
body: "Q2 planning begins next week. Focus areas: retention and activation.",
},
}custom - Custom Card
Renders any React component inside a standard dashboard card shell. The card handles all sizing, border, and scroll behaviour — your component just needs to render its own content and can assume it has full width and height available.
import { MyCustomWidget } from "./MyCustomWidget";
{
id: "my-widget",
type: "custom",
size: "medium",
component: MyCustomWidget,
props: {
userId: "abc123",
showControls: true,
},
}The underlying CustomCard wraps your component like this:
interface Props {
component: React.ComponentType<any>;
props?: Record<string, unknown>;
}
export const CustomCard: React.FC<Props> = ({
component: Component,
props,
}) => (
<div style={{ width: "100%", height: "100%", overflow: "auto" }}>
<Component {...(props ?? {})} />
</div>
);Your component receives props spread directly as React props. The outer div is width: 100%, height: 100% with overflow: auto, so scrollable content works without any extra wrapper in your component. Any React component with no required props (or with all required props covered by the props field) can be dropped in. This is useful for embedding third-party charts, maps, custom forms, or any one-off widget that doesn't fit the standard types.
Theming
Pass a theme object to <Dashboard> to customise the visual style:
<Dashboard
items={items}
theme={{
primaryColor: "#7c3aed", // Accent / interactive color
backgroundColor: "#0f172a", // Dashboard background
cardBackground: "#1e293b", // Widget card background
borderRadius: "16px", // Card corner radius
textColor: "#f1f5f9", // Primary text
mutedColor: "#94a3b8", // Secondary / muted text
borderColor: "#334155", // Card borders
fontFamily: "'DM Sans', sans-serif",
}}
/>Theme reference
DashboardTheme props:
| Prop | Default | Description |
| ----------------- | ----------------------- | -------------------------------------------- |
| primaryColor | #6366f1 | Accent color for highlights, buttons, charts |
| backgroundColor | #f4f5f7 | Dashboard canvas background |
| cardBackground | #ffffff | Card surface background |
| borderRadius | 12px | Card border radius |
| textColor | #111827 | Primary text color |
| mutedColor | #6b7280 | Muted / secondary text |
| borderColor | #e5e7eb | Card and divider borders |
| fontFamily | system-ui, sans-serif | Font stack |
Dark theme
const darkTheme = {
primaryColor: "#818cf8",
backgroundColor: "#0f172a",
cardBackground: "#1e293b",
borderRadius: "14px",
textColor: "#f1f5f9",
mutedColor: "#94a3b8",
borderColor: "#334155",
};Teal/mint theme
const tealTheme = {
primaryColor: "#14b8a6",
backgroundColor: "#f0fdfa",
cardBackground: "#ffffff",
borderRadius: "8px",
textColor: "#134e4a",
mutedColor: "#5eead4",
borderColor: "#99f6e4",
};Midnight blue theme
Deep navy background with electric blue accents — ideal for ops dashboards and monitoring tools.
const midnightBlueTheme = {
primaryColor: "#3b82f6",
backgroundColor: "#0a0f1e",
cardBackground: "#111827",
borderRadius: "10px",
textColor: "#e2e8f0",
mutedColor: "#64748b",
borderColor: "#1e3a5f",
fontFamily: "'Inter', sans-serif",
};Warm sand theme
Earthy, neutral tones with amber accents — great for productivity and note-taking apps.
const warmSandTheme = {
primaryColor: "#d97706",
backgroundColor: "#fdf6ec",
cardBackground: "#fffbf5",
borderRadius: "10px",
textColor: "#1c1917",
mutedColor: "#a8a29e",
borderColor: "#e7d9c8",
fontFamily: "'Georgia', serif",
};Forest green theme
Nature-inspired greens with rich, organic feel — suits health, sustainability, or finance dashboards.
const forestGreenTheme = {
primaryColor: "#16a34a",
backgroundColor: "#f0fdf4",
cardBackground: "#ffffff",
borderRadius: "12px",
textColor: "#14532d",
mutedColor: "#6b7280",
borderColor: "#bbf7d0",
fontFamily: "'DM Sans', sans-serif",
};Rose quartz theme
Soft pinks and warm whites — great for lifestyle, wellness, or consumer-facing dashboards.
const roseQuartzTheme = {
primaryColor: "#e11d48",
backgroundColor: "#fff1f2",
cardBackground: "#ffffff",
borderRadius: "16px",
textColor: "#881337",
mutedColor: "#fb7185",
borderColor: "#fecdd3",
fontFamily: "'Lato', sans-serif",
};Slate enterprise theme
High-contrast, professional slate palette — designed for enterprise admin panels and B2B tools.
const slateEnterpriseTheme = {
primaryColor: "#0284c7",
backgroundColor: "#f1f5f9",
cardBackground: "#ffffff",
borderRadius: "6px",
textColor: "#0f172a",
mutedColor: "#64748b",
borderColor: "#cbd5e1",
fontFamily: "'system-ui', sans-serif",
};Sunset orange theme
Warm, energetic gradient palette — eye-catching for marketing dashboards and campaign trackers.
const sunsetOrangeTheme = {
primaryColor: "#ea580c",
backgroundColor: "#fff7ed",
cardBackground: "#ffffff",
borderRadius: "12px",
textColor: "#431407",
mutedColor: "#9a3412",
borderColor: "#fed7aa",
fontFamily: "'Nunito', sans-serif",
};Arctic white theme
Ultra-clean, minimal whites with cool blue accents — perfect for medical, scientific, or data-heavy UIs.
const arcticWhiteTheme = {
primaryColor: "#0ea5e9",
backgroundColor: "#f8fafc",
cardBackground: "#ffffff",
borderRadius: "8px",
textColor: "#0c4a6e",
mutedColor: "#94a3b8",
borderColor: "#e0f2fe",
fontFamily: "'IBM Plex Sans', sans-serif",
};Charcoal ink theme
Dark charcoal with stark white text and vivid cyan accents — ideal for developer tools and terminal-style dashboards.
const charcoalInkTheme = {
primaryColor: "#06b6d4",
backgroundColor: "#18181b",
cardBackground: "#27272a",
borderRadius: "4px",
textColor: "#fafafa",
mutedColor: "#71717a",
borderColor: "#3f3f46",
fontFamily: "'JetBrains Mono', monospace",
};Widget Sizing
Three predefined sizes:
| Size | Grid columns | Row height units | Pixels (approx.) |
| -------- | ------------ | ---------------- | ---------------- |
| small | 3 | 2 | 160px tall |
| medium | 6 | 3 | 240px tall |
| large | 12 | 4 | 320px tall |
Custom sizes are also supported:
{
id: "wide-chart",
type: "chart",
customSize: { w: 9, h: 3 }, // 9 columns × 3 row heights
// ...
}Drag, Resize & Edit Mode
Click Edit Layout to enter edit mode. In edit mode, all widgets show a drag handle and glow ring. Widgets with resizable: true show a resize handle. Resizing snaps to the nearest predefined size by default; use customSize for free sizing. onLayoutChange fires with the updated layout after drag or resize stops.
<Dashboard
items={items}
onLayoutChange={(layout) => {
saveLayout(layout);
}}
/>WidgetLayoutChange payload:
interface WidgetLayoutChange {
id: string;
x: number;
y: number;
size: "small" | "medium" | "large" | { w: number; h: number };
draggable: boolean;
resizable: boolean;
}Full Example
import React, { useState } from "react";
import { Dashboard, DEFAULT_TEMPLATES } from "react-dashboard-grid";
import type { WidgetItem, WidgetTemplate } from "react-dashboard-grid";
const initialItems: WidgetItem[] = [
{
id: "w1",
type: "stat",
size: "small",
data: {
title: "Revenue",
value: "$124k",
change: "+8.2%",
changeType: "up",
},
},
{
id: "w2",
type: "stat",
size: "small",
data: {
title: "Users",
value: "32,104",
change: "+1,200",
changeType: "up",
},
},
{
id: "w3",
type: "chart",
size: "medium",
data: {
chartType: "line",
title: "Revenue Over Time",
data: [
{ label: "Jan", value: 42000 },
{ label: "Feb", value: 58000 },
{ label: "Mar", value: 51000 },
{ label: "Apr", value: 74000 },
{ label: "May", value: 69000 },
{ label: "Jun", value: 91000 },
],
},
},
{
id: "w4",
type: "gauge",
size: "small",
data: { title: "NPS Score", value: "72", percentage: 72 },
},
];
const myTemplates: WidgetTemplate[] = [
...DEFAULT_TEMPLATES,
{
key: "mrr-stat",
label: "MRR Tracker",
description: "Monthly recurring revenue with goal progress.",
component: {
type: "stat",
size: "small",
data: { title: "MRR", value: "$0", changeType: "neutral" },
},
},
];
export default function App() {
return (
<Dashboard
title="Analytics Dashboard"
subtitle="Real-time product metrics"
items={initialItems}
widgetTemplates={myTemplates}
theme={{
primaryColor: "#6366f1",
backgroundColor: "#f8fafc",
cardBackground: "#ffffff",
borderRadius: "12px",
}}
onLayoutChange={(layout) => console.log("Layout changed:", layout)}
onAddWidget={() => console.log("Widget added")}
/>
);
}TypeScript Exports
import {
// Components
Dashboard,
AddWidgetModal,
DEFAULT_TEMPLATES,
// Types
type DashboardProps,
type WidgetItem,
type WidgetTemplate,
type AddWidgetModalProps,
type DashboardTheme,
type DashboardAction,
type WidgetLayoutChange,
// Widget data types
type StatData,
type ChartData,
type ListData,
type ListItem,
type TextData,
type GaugeData,
type ProfileData,
type SplitStatData,
type TableData,
type TableColumn,
type CalendarData,
type CalendarEvent,
type SplitStatSegment,
// Utilities
WIDGET_SIZES,
type WidgetSize,
} from "react-dashboard-grid";FAQ
Q: How do I pre-populate the modal with my product's specific widgets?
Pass widgetTemplates to <Dashboard> or <AddWidgetModal>. Each template includes the full, pre-configured widget data — no extra configuration step for the user.
Q: Can I disable the modal and handle "Add Widget" myself?
Yes. Pass showAddWidget={false} and use your own button that calls your own state logic. Or use onAddWidget as a callback and build your own flow.
Q: Can I have non-snapping, freely resizable widgets?
Yes. Set resizable: true and customSize: { w, h } on the widget. This bypasses snap-to-size and allows free resizing within minW/maxW/minH/maxH bounds.
Q: How do I persist the layout?
Use onLayoutChange — it fires after every drag or resize stop with a clean WidgetLayoutChange[] payload. Save this to your backend and pass it back via items[n].layout.
Q: Can I render completely custom content in a widget?
Yes. Use type: "custom" and pass any React component via the component field. The component receives your props object spread as React props and is wrapped in a full-bleed, scrollable container.
