A powerful, feature-rich drag-and-drop widget grid system for React applications. Built with TypeScript, performance optimized, and production-ready.
- π― Responsive Grid: Automatically scales to fit any screen size
- π±οΈ Drag & Drop: Intuitive widget positioning with collision detection and reflow
- π§© Custom Widgets: Easy integration of any React component
- π Flexible Layout: Configurable grid dimensions and widget sizes
- π¨ Themeable: Customizable styling with CSS variables
- β‘ Performance: Optimized for smooth interactions with throttled events
- π Group Filtering: Show/hide groups of widgets
- π Undo/Redo: Full history management with configurable limits
- π Multi-Selection: Select and manipulate multiple widgets at once
- π Bulk Operations: Move, resize, and delete multiple widgets simultaneously
- π¦ Templates: Save and reuse widget arrangements
- π Widget Linking: Create relationships between widgets
- π― Smart Layout: Auto-arrange with multiple strategies (compact, distribute, align)
- πΎ Import/Export: Save and restore entire grid states
- π Grid Analytics: Built-in metrics and occupancy tracking
- π SSR Compatible: Works with Next.js, Gatsby, and other SSR frameworks
npm install gridtech-reactimport React, { useState } from "react";
import { WidgetGrid, WidgetState, useWidgetActions } from "gridtech-react";
import "gridtech-react/dist/index.css";
// Define your custom widgets
const MyWidget = ({ title }: { title: string }) => (
<div style={{ padding: "1rem" }}>
<h3>{title}</h3>
<p>This is a custom widget!</p>
</div>
);
const widgetRenderers = {
custom: MyWidget,
};
function App() {
const [widgets, setWidgets] = useState<WidgetState[]>([
{
id: "widget-1",
type: "custom",
x: 0,
y: 0,
width: 4,
height: 3,
props: { title: "Hello World" },
},
]);
return (
<WidgetGrid
cols={24}
rows={12}
initialWidgets={widgets}
onWidgetsChange={setWidgets}
widgetRenderers={widgetRenderers}
preventOverlap={true}
defaultEditMode={true}
showControls={true}
/>
);
}function AdvancedGridDemo() {
const {
widgets,
selectedWidgets,
selectWidget,
selectMultiple,
selectAll,
bulkMoveWidgets,
bulkDeleteWidgets,
undo,
redo,
canUndo,
canRedo,
} = useWidgetActions({
cols: 24,
rows: 12,
preventOverlap: true,
defaultWidgetSize: { w: 4, h: 3 },
});
return (
<div>
<div className="toolbar">
<button onClick={() => selectAll()}>Select All</button>
<button onClick={() => bulkMoveWidgets(selectedWidgets, 1, 0)}>
Move Right
</button>
<button onClick={() => bulkDeleteWidgets(selectedWidgets)}>
Delete Selected
</button>
<button onClick={undo} disabled={!canUndo}>
Undo
</button>
<button onClick={redo} disabled={!canRedo}>
Redo
</button>
</div>
<WidgetGrid
cols={24}
rows={12}
initialWidgets={widgets}
selectedWidgets={selectedWidgets}
onWidgetSelect={selectWidget}
widgetRenderers={widgetRenderers}
preventOverlap={true}
/>
</div>
);
}function TemplateDemo() {
const {
widgets,
saveAsTemplate,
applyTemplate,
getAvailableTemplates,
autoArrangeWidgets,
exportGridState,
importGridState,
} = useWidgetActions({
cols: 24,
rows: 12,
preventOverlap: true,
defaultWidgetSize: { w: 4, h: 3 },
});
const handleSaveTemplate = () => {
const selectedIds = widgets.slice(0, 3).map((w) => w.id); // First 3 widgets
saveAsTemplate(selectedIds, "Dashboard Layout");
};
const handleAutoArrange = (strategy: "compact" | "distribute" | "align") => {
autoArrangeWidgets(strategy);
};
return (
<div>
<div className="template-controls">
<button onClick={handleSaveTemplate}>Save as Template</button>
<button onClick={() => applyTemplate("Dashboard Layout")}>
Apply Template
</button>
<button onClick={() => handleAutoArrange("compact")}>
Compact Layout
</button>
<button onClick={() => handleAutoArrange("distribute")}>
Distribute Evenly
</button>
</div>
<WidgetGrid
cols={24}
rows={12}
initialWidgets={widgets}
widgetRenderers={widgetRenderers}
preventOverlap={true}
/>
</div>
);
}function LinkedWidgetsDemo() {
const {
widgets,
linkWidgets,
unlinkWidgets,
getLinkedWidgets
} = useWidgetActions({
cols: 24,
rows: 12,
preventOverlap: true,
defaultWidgetSize: { w: 4, h: 3 }
});
const handleLinkWidgets = (sourceId: string, targetId: string) => {
linkWidgets(sourceId, targetId, 'data-flow');
};
const getWidgetConnections = (widgetId: string) => {
return getLinkedWidgets(widgetId);
};
return (
<WidgetGrid
cols={24}
rows={12}
initialWidgets={widgets}
onWidgetLink={handleLinkWidgets}
widgetRenderers={widgetRenderers}
renderConnections={(widgetId) => (
<ConnectionVisualization
connections={getWidgetConnections(widgetId)}
/>
)}
/>
);
}import { useWidgetActions, useResponsiveGrid } from 'gridtech-react';
const MyCustomGrid = () => {
const { widgets, addWidget, moveWidget } = useWidgetActions({
cols: 24,
rows: 12,
preventOverlap: true,
defaultWidgetSize: { w: 4, h: 3 }
});
const { cellWidth, cellHeight } = useResponsiveGrid({
cols: 24,
rows: 12
});
return (
<div>
<button onClick={() => addWidget('custom', { title: 'New Widget' })}>
Add Widget
</button>
{/_ Your custom grid implementation _/}
</div>
);
};| Prop | Type | Default | Description |
|---|---|---|---|
cols |
number |
24 |
Number of grid columns |
rows |
number |
12 |
Number of grid rows |
initialWidgets |
WidgetState[] |
[] |
Array of widget configurations |
onWidgetsChange |
(widgets: WidgetState[]) => void |
- | Callback when widgets change |
onSelectionChange |
(selectedIds: string[]) => void |
- | Callback when selection changes |
widgetRenderers |
{ [type: string]: ComponentType } |
- | Map of widget types to components |
preventOverlap |
boolean |
false |
Prevent widgets from overlapping |
defaultEditMode |
boolean |
false |
Start in edit mode |
groupFilters |
GroupFilter[] |
[] |
Array of group visibility filters |
onGroupFiltersChange |
(filters: GroupFilter[]) => void |
- | Callback when group filters change |
enableHoverToAdd |
boolean |
false |
Enable hover-to-add widget functionality |
availableWidgets |
AvailableWidget[] |
[] |
Available widget types for hover-to-add |
interactionModes |
InteractionModes |
{} |
Control which interactions are enabled |
The enhanced useWidgetActions hook provides comprehensive grid management:
const {
// Core state
widgets,
selectedWidgets,
templates,
links,
// History management
undo,
redo,
canUndo,
canRedo,
// Selection management
selectWidget,
selectMultiple,
selectAll,
clearSelection,
// Core operations
addWidget,
moveWidget,
resizeWidget,
deleteWidget,
// Bulk operations
bulkMoveWidgets,
bulkDeleteWidgets,
bulkResizeWidgets,
// Grid state management
exportGridState,
importGridState,
clearGrid,
getGridMetrics,
// Template management
saveAsTemplate,
applyTemplate,
getAvailableTemplates,
// Advanced layout
autoArrangeWidgets,
findOptimalPosition,
swapWidgets,
// Widget relationships
linkWidgets,
unlinkWidgets,
getLinkedWidgets
} = useWidgetActions({
cols: 24,
rows: 12,
preventOverlap: true,
defaultWidgetSize: { w: 4, h: 3 },
maxHistorySize: 50,
onWidgetsChange,
onSelectionChange
});interface WidgetState {
id: string;
type: string;
x: number;
y: number;
width: number;
height: number;
props?: Record<string, any>;
groupId?: string;
minW?: number;
minH?: number;
maxW?: number;
maxH?: number;
}interface GridTemplate {
id: string;
name: string;
widgets: Omit<WidgetState, "id">[];
createdAt: number;
}interface WidgetLink {
sourceId: string;
targetId: string;
linkType: string;
}interface GridExport {
widgets: WidgetState[];
templates: GridTemplate[];
links: WidgetLink[];
metadata: {
version: string;
exportedAt: number;
gridDimensions: { cols: number; rows: number };
};
}interface GridMetrics {
totalWidgets: number;
occupancy: number; // 0-1 representing percentage of grid occupied
averageWidgetSize: { width: number; height: number };
largestWidget: { width: number; height: number };
emptySpaces: number;
}interface GroupFilter {
groupId: string;
visible: boolean;
}selectWidget(id, addToSelection?)- Select single widget, optionally add to current selectionselectMultiple(ids)- Select multiple widgets by ID arrayselectAll()- Select all widgets in the gridclearSelection()- Clear current selection
bulkMoveWidgets(widgetIds, deltaX, deltaY)- Move multiple widgets by offsetbulkDeleteWidgets(widgetIds)- Delete multiple widgets at oncebulkResizeWidgets(widgetIds, scale)- Scale multiple widgets by factor
undo()- Undo last action (configurable history size)redo()- Redo last undone actioncanUndo/canRedo- Boolean flags for UI state
autoArrangeWidgets('compact' | 'distribute' | 'align')- Smart auto-arrangementfindOptimalPosition(width, height, preferences?)- Find best position for new widgetswapWidgets(id1, id2)- Exchange positions of two widgets
saveAsTemplate(widgetIds, templateName)- Save widget arrangement as templateapplyTemplate(templateName, position?)- Apply saved template to gridexportGridState()- Export complete grid state for persistenceimportGridState(state)- Restore grid from exported state
getGridMetrics()- Get occupancy, widget counts, and size analytics
GridTech React includes built-in error handling with console warnings for common scenarios:
// All errors are logged with 'GridTech:' prefix for easy filtering
// - No space available for new widgets
// - Cannot move widget due to space constraints
// - Cannot resize widget due to space constraints
// Monitor errors in production:
const originalWarn = console.warn;
console.warn = function (...args: any[]) {
if (args[0]?.includes("GridTech:")) {
// Send to your error tracking service
analytics.track("gridtech_warning", { message: args[0] });
}
originalWarn.apply(console, args);
};GridTech React is fully compatible with server-side rendering frameworks:
// Next.js example
import dynamic from "next/dynamic";
const WidgetGrid = dynamic(
() => import("gridtech-react").then((mod) => mod.WidgetGrid),
{ ssr: false }
);- Large Grids: Use
preventOverlap: falsefor grids with 100+ widgets - History Size: Adjust
maxHistorySizebased on memory constraints - Throttling: Mouse events are already throttled for optimal performance
- Memoization: Widget renderers should be memoized with React.memo()
const MemoizedWidget = React.memo(({ title, data }: WidgetProps) => {
return (
<div>
<h3>{title}</h3>
<ComplexVisualization data={data} />
</div>
);
});import { useWidgetActions, useResponsiveGrid } from "gridtech-react";
const MyCustomGrid = () => {
const { widgets, addWidget, moveWidget } = useWidgetActions({
cols: 24,
rows: 12,
preventOverlap: true,
defaultWidgetSize: { w: 4, h: 3 },
});
const { cellWidth, cellHeight } = useResponsiveGrid({
cols: 24,
rows: 12,
});
return (
<div>
<button onClick={() => addWidget("custom", { title: "New Widget" })}>
Add Widget
</button>
{/* Your custom grid implementation */}
</div>
);
};GridTech React supports simple group filtering - perfect for organizing widgets by category:
import React, { useState, useRef } from "react";
import {
WidgetGrid,
WidgetState,
GroupFilter,
WidgetGridRef,
} from "gridtech-react";
function App() {
const gridRef = useRef<WidgetGridRef>(null);
const [widgets, setWidgets] = useState<WidgetState[]>([
{
id: "chart-1",
type: "chart",
x: 0,
y: 0,
width: 4,
height: 3,
groupId: "analytics", // Assign to group
props: { title: "Sales Chart" },
},
{
id: "metric-1",
type: "metric",
x: 4,
y: 0,
width: 2,
height: 2,
groupId: "kpi", // Different group
props: { value: 1234, label: "Total Sales" },
},
]);
const [groupFilters, setGroupFilters] = useState<GroupFilter[]>([]);
// Hide/show groups
const toggleGroup = (groupId: string, visible: boolean) => {
gridRef.current?.setGroupVisible(groupId, visible);
};
return (
<div>
<button onClick={() => toggleGroup("analytics", false)}>
Hide Analytics
</button>
<button onClick={() => toggleGroup("kpi", false)}>Hide KPIs</button>
<WidgetGrid
ref={gridRef}
cols={24}
rows={12}
initialWidgets={widgets}
onWidgetsChange={setWidgets}
groupFilters={groupFilters}
onGroupFiltersChange={setGroupFilters}
widgetRenderers={widgetRenderers}
/>
</div>
);
}π Full Group Filtering Documentation
Version 0.5.0 maintains full backward compatibility while adding powerful new features:
// Existing code continues to work unchanged
const { widgets, moveWidget, addWidget, deleteWidget } = useWidgetActions({
cols: 12,
rows: 8,
preventOverlap: true,
defaultWidgetSize: { w: 4, h: 3 },
});
// New features are opt-in
const {
// Existing API (unchanged)
widgets,
moveWidget,
addWidget,
deleteWidget,
// New history features
undo,
redo,
canUndo,
canRedo,
// New selection features
selectedWidgets,
selectWidget,
clearSelection,
// New bulk operations
bulkMoveWidgets,
bulkDeleteWidgets,
// New templates
saveAsTemplate,
applyTemplate,
getAvailableTemplates,
// New linking
linkWidgets,
unlinkWidgets,
} = useWidgetActions({
cols: 12,
rows: 8,
preventOverlap: true,
defaultWidgetSize: { w: 4, h: 3 },
// New optional configuration
maxHistorySize: 50,
onWidgetsChange,
onSelectionChange,
});- Memoize grid options to prevent unnecessary re-renders:
const gridOptions = useMemo(
() => ({
cols: 12,
rows: 8,
gap: 8,
}),
[]
);- Use selection sparingly for large grids (>100 widgets)
- Limit history size for memory-conscious applications:
const actions = useWidgetActions({
cols: 24,
rows: 12,
preventOverlap: true,
maxHistorySize: 20, // Default is 50
});- Template management - Clear unused templates periodically:
// Clear all templates
actions.clearAllTemplates(); // Method to clear templatesGridTech uses CSS custom properties for easy theming:
:root {
--grid-background: #f5f5f5;
--grid-border: #e0e0e0;
--widget-background: white;
--widget-border: #ddd;
--widget-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
--widget-selected: #007acc;
--widget-linked: #22c55e;
}- β¨ NEW: Undo/Redo functionality with configurable history
- β¨ NEW: Multi-widget selection and bulk operations
- β¨ NEW: Template system for saving/loading layouts
- β¨ NEW: Widget linking with constraint management
- β¨ NEW: Smart auto-arrangement algorithms
- β¨ NEW: Grid analytics and export capabilities
- β¨ NEW: Enhanced TypeScript definitions
- π§ Improved performance and memory management
- π Comprehensive documentation updates
- Basic grid functionality
- Widget drag & drop
- Responsive breakpoints
- Group filtering
- CSS customization
MIT