How Instatic's Dashboard Widget System Works: Registry-Driven Architecture Explained
Instatic's dashboard widget system uses a central metadata registry and a primitive Widget component to render consistent, extensible tiles that support drag-and-drop editing and runtime plugin registration.
The CoreBunch/Instatic repository implements a component-centric dashboard architecture that keeps every tile visually consistent while allowing plugins to contribute new functionality. This system balances strict UI uniformity with flexible extensibility through a registry-driven pattern that governs how widgets are defined, rendered, and manipulated.
The Widget Primitive and Visual Chrome
At the heart of the system lies the Widget component located in src/ui/components/Widget/Widget.tsx. This primitive provides the visual "chrome"—headers, icons, tint colors, drag handles, and overflow menus—that surrounds every tile on the dashboard.
Both first-party widgets (such as Storage, Posts, and Media) and third-party plugin widgets compose this component to inherit consistent styling and interaction patterns. The component accepts several key props that dictate its appearance and behavior:
widgetId– Unique identifier used by the drag-and-drop layer for tracking tile positionstitle,icon,tint– Visual metadata rendered in the headerspan– Grid column width (1–12) forwarded as adata-spanattributeediting– Boolean flag that swaps the kebab menu for a drag handle when the dashboard is in customize modeloading– Triggers a skeleton placeholder while async data resolves
The tint system deserves special attention. In src/ui/components/Widget/Widget.tsx at lines 49–52, the tint token is converted into a CSS custom property --tint. This allows child chart components to access the color value via var(--tint) without prop-drilling, ensuring that data visualizations automatically match their parent widget's theme.
Widget Registration and the Central Registry
Instatic manages widget availability through a central registry defined in src/core/dashboard/registry.ts. This store maintains metadata for every available widget, including identifiers, display names, icon references, default tint values, and grid span configurations.
The registry populates through two distinct pathways:
- Built-in widgets – Hard-coded registrations in
src/admin/pages/dashboard/widgets/*.tsx(such asStorageWidget.tsx,PostsWidget.tsx, andMediaWidget.tsx) - Plugin contributions – Runtime registration via the plugin SDK method
api.dashboard.widgets.register(...), type-defined insrc/core/plugin-sdk/types/dashboardWidgets.ts
Plugin registration is guarded by the dashboard.widgets.register permission, defined in src/core/plugin-sdk/types/permissions.ts, ensuring that only authorized extensions can inject new tiles into the dashboard grid.
Rendering States and Data Flow
When the dashboard grid renders a tile, it performs a registry lookup to retrieve the widget's metadata, then instantiates the Widget component with the appropriate props.
Loading States and Placeholders
During the brief window between plugin registration and component hydration, the system renders a WidgetSkeleton (defined in src/ui/components/Widget/Widget.tsx at lines 85–100). This placeholder paints an outline with a title shimmer and body skeleton, maintaining layout stability and signaling to users that content is loading. While the widget fetches data asynchronously through the apiRequest client (src/core/http/apiRequest.ts), the loading prop triggers SkeletonBlock elements and marks the section aria-busy for accessibility.
Drag-and-Drop Editing Mode
When users activate dashboard customization, the editing prop becomes true, causing the Widget header to display a DragAndDropSolidIcon instead of the standard menu. The surrounding drag-and-drop system in src/admin/pages/dashboard/DashboardGrid.tsx uses the widgetId attribute to track tile movements, persisting new layouts directly to the site document.
Plugin Integration via Host UI
Plugins integrate with the dashboard system through the @instatic/host-ui package, re-exported from src/admin/plugin-host-ui/index.ts. This host package exposes the Widget component and shared CSS tokens (including --accent-1 through --accent-4 from src/styles/globals.css), ensuring third-party widgets match the exact styling and accessibility markup of native tiles.
For widgets displaying tabular data, the WidgetList component in src/ui/components/WidgetList/WidgetList.tsx provides standardized row styling and spacing, extending the chrome system into list-based visualizations.
Code Examples
Registering a Plugin Widget
Plugins register custom widgets through the SDK with full TypeScript support:
import { api } from '@core/plugin-sdk'
api.dashboard.widgets.register({
widgetId: 'my-custom-widget',
title: 'My Custom Widget',
icon: require('pixel-art-icons/icons/star-solid'),
tint: 'sky',
defaultSize: { span: 4 },
render: (props) => <MyCustomBody {...props} />,
})
First-Party Widget Implementation
Built-in widgets follow the same compositional pattern as plugins:
import { Widget } from '@instatic/host-ui'
import { StackedBar } from '@ui/components/charts/StackedBar'
export function StorageWidget() {
const { data, loading } = useAsyncResource(fetchStorageStats)
return (
<Widget
widgetId="storage"
title="Storage"
icon={HardDriveSolidIcon}
tint="mint"
span={4}
editing={isEditing}
loading={loading}
>
<StackedBar data={data} />
</Widget>
)
}
Placeholder for Loading Widgets
Use the skeleton component to maintain layout stability during async widget loading:
import { WidgetSkeleton } from '@instatic/host-ui'
function EmptySlot({ widgetId, span }: { widgetId: string; span: number }) {
return <WidgetSkeleton widgetId={widgetId} span={span} />
}
Summary
- CoreBunch/Instatic implements a registry-driven widget system where
src/core/dashboard/registry.tsstores metadata for all available dashboard tiles. - The
Widgetcomponent insrc/ui/components/Widget/Widget.tsxprovides consistent chrome, CSS tint variables, and accessibility features to both built-in and plugin widgets. - Plugins register widgets via
api.dashboard.widgets.register()(defined insrc/core/plugin-sdk/types/dashboardWidgets.ts) after obtaining thedashboard.widgets.registerpermission. - The
editingprop activates drag-and-drop handles, whileWidgetSkeletonprevents layout shift during loading states. - Third-party widgets consume the
@instatic/host-uipackage to inherit exact styling tokens and behavior from the core system.
Frequently Asked Questions
How do plugins register new widgets in Instatic?
Plugins call api.dashboard.widgets.register() with a configuration object containing widgetId, title, icon, tint, and a render function. This method, defined in src/core/plugin-sdk/types/dashboardWidgets.ts, requires the dashboard.widgets.register permission and stores the metadata in the central registry at src/core/dashboard/registry.ts.
What is the purpose of the WidgetSkeleton component?
WidgetSkeleton serves as a placeholder that renders during the gap between plugin registration and component code loading, or while a widget fetches initial data. Implemented in src/ui/components/Widget/Widget.tsx at lines 85–100, it maintains grid layout stability and provides visual feedback through a shimmer effect and aria-busy markup.
How does Instatic ensure consistent styling across different widget sources?
All widgets—whether built-in or plugin-provided—compose the same Widget primitive from @instatic/host-ui, which injects standardized CSS custom properties including --tint and accent color tokens from src/styles/globals.css. This ensures that charts, lists, and headers automatically inherit the host application's theme without manual configuration.
What permissions are required to register dashboard widgets?
The system checks for the dashboard.widgets.register permission before allowing a plugin to execute api.dashboard.widgets.register(). This permission is defined in src/core/plugin-sdk/types/permissions.ts and prevents unauthorized extensions from injecting arbitrary UI into the dashboard grid.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →