How to Add Custom Dashboard Widgets to the Instatic Admin Interface
You add custom dashboard widgets to the Instatic admin interface by implementing a React component that composes the Widget chrome and registering it through the plugin SDK's api.dashboard.widgets.register method, provided your plugin manifest declares the dashboard.widgets.register permission.
Instatic's admin dashboard uses a unified widget system that treats first-party and third-party components equally. To add custom dashboard widgets to the Instatic admin interface, you utilize the same Widget primitive and registry mechanism that powers built-in widgets like PostsWidget and MediaWidget, without requiring any core code modifications or additional build steps.
Architecture Overview
The widget system consists of four distinct layers that handle rendering, metadata storage, and plugin integration:
| Layer | Responsibility | Key Source |
|---|---|---|
| Widget Chrome | Renders the card surface, title bar with tint dot and icon, action slots, drag-handle, and body container. Exposes the --tint CSS custom property for child components. |
src/ui/components/Widget/Widget.tsx |
| Widget Registry | Maintains a map of widget definitions (widgetId → metadata) that the dashboard grid consumes at runtime. |
src/core/dashboard/registry.ts |
| Plugin SDK | Exposes api.dashboard.widgets.register and defines the DashboardWidgetRegistration interface and permission constants. |
src/core/plugin-sdk/types/dashboardWidgets.ts |
| Dashboard Grid | Reads the registry to layout widgets in a CSS grid and handles the rendering lifecycle including skeleton placeholders. | src/admin/pages/dashboard/widgets/index.ts |
When the admin dashboard boots, the host UI imports the Widget component from @instatic/host-ui and populates the registry with built-in widgets. Plugins extending the registry must declare the dashboard.widgets.register permission in their manifest, as enforced by the runtime validation in src/core/plugins/runtime.ts.
Step 1: Create the Widget Component
Your custom widget is a standard React component that receives chrome props and renders content inside the Widget wrapper. Import Widget from @instatic/host-ui to ensure visual consistency with the admin interface.
import { Widget } from '@instatic/host-ui'
import { useAsyncResource } from '@ui/hooks/useAsyncResource'
import { Chart } from '@ui/components/charts/Chart'
export function RecentCommentsWidget({ widgetId }: { widgetId: string }) {
const { data: comments, loading } = useAsyncResource(
async () => await api.admin.comments.recent(),
[widgetId],
)
return (
<Widget
widgetId={widgetId}
title="Recent Comments"
icon={CommentIcon}
tint="mint"
span={4}
loading={loading}
>
{comments?.map(c => (
<Chart key={c.id} value={c.score} label={c.author} />
))}
</Widget>
)
}
The Widget component automatically handles the card chrome, loading states, and accessibility attributes. The tint prop sets the --tint CSS custom property that child chart primitives can reference for consistent coloring. Use useAsyncResource to follow Instatic's pattern for async data fetching within widgets.
Step 2: Register via the Plugin SDK
In your plugin's entry point (typically editor/index.ts), import your component and call the registration API:
import { api } from '@instatic/plugin-sdk'
import { RecentCommentsWidget } from './RecentCommentsWidget'
api.dashboard.widgets.register({
widgetId: 'my-plugin.recent-comments',
title: 'Recent Comments',
iconName: 'comment',
tint: 'mint',
defaultSize: { span: 4 },
Component: RecentCommentsWidget,
})
The registration payload must conform to the DashboardWidgetRegistration type defined in src/core/plugin-sdk/types/dashboardWidgets.ts. The iconName value is validated against the vendored pixel-art-icons package to ensure visual consistency with the admin design system.
Step 3: Configure Plugin Permissions
The registration call fails silently if your plugin lacks the required permission. Add dashboard.widgets.register to your plugin manifest:
{
"name": "my-plugin",
"apiVersion": "v2",
"permissions": ["dashboard.widgets.register"]
}
The runtime checks this permission in src/core/plugins/runtime.ts before allowing the registry modification.
Rendering Lifecycle
When the dashboard renders your custom widget, three operations occur:
- Component Mount: Your component receives the
widgetId,title,icon,tint, andspanprops from the registry metadata. - Grid Integration: The dashboard grid in
src/admin/pages/dashboard/widgets/index.tsreads the registry entry and positions your widget according to itsdefaultSize. - Permission Verification: The runtime validates the
dashboard.widgets.registerpermission before executing the registration call.
The widget appears immediately in the dashboard's block picker and grid layout without requiring a rebuild of the host application, as the component code is bundled with your plugin and loaded dynamically at runtime.
Key Source Files
Reference these implementation details when building custom widgets:
src/ui/components/Widget/Widget.tsx: The chrome component that supplies the card surface, title bar, and--tintCSS variable.src/core/dashboard/registry.ts: The central map storing all widget definitions that the dashboard grid consumes.src/core/plugin-sdk/types/dashboardWidgets.ts: TypeScript definitions forDashboardWidgetRegistrationand the permission constant.src/admin/pages/dashboard/widgets/PostsWidget.tsx: Reference implementation of a first-party widget showing the expected component structure.
Summary
- Add custom dashboard widgets to the Instatic admin interface by composing the
Widgetcomponent from@instatic/host-uiand registering viaapi.dashboard.widgets.register. - The
Widgetchrome insrc/ui/components/Widget/Widget.tsxprovides consistent styling, loading states, and accessibility without manual configuration. - Registration requires the
dashboard.widgets.registerpermission declared in your plugin manifest, enforced by the runtime insrc/core/plugins/runtime.ts. - Widget definitions are stored in
src/core/dashboard/registry.tsand rendered by the grid system insrc/admin/pages/dashboard/widgets/index.ts. - Use
useAsyncResourcefor data fetching and referencepixel-art-iconsnames for theiconNameproperty to maintain UI consistency.
Frequently Asked Questions
Do I need to modify Instatic core code to add custom widgets?
No. You add custom dashboard widgets to the Instatic admin interface entirely through the plugin SDK. Your widget component lives in your plugin's source tree, bundles with your plugin code, and registers at runtime through the api.dashboard.widgets.register method. The host application dynamically loads and renders your widget without requiring core modifications or rebuilds.
How do I handle data fetching and loading states?
Use the useAsyncResource hook imported from @ui/hooks/useAsyncResource. This follows the Instatic pattern for async operations within widgets and integrates with the Widget component's loading prop to display skeleton states automatically. Pass the loading boolean to the Widget component, which handles the visual loading chrome while your data resolves.
What icons can I use for my widget?
The iconName property in your registration payload must reference an icon from the vendored pixel-art-icons package. These names are validated at registration time to ensure visual consistency across the admin interface. Check the pixel-art-icons documentation or existing widgets like PostsWidget in src/admin/pages/dashboard/widgets/PostsWidget.tsx for valid icon name examples.
Can I control the default size of my widget on the dashboard?
Yes. Include a defaultSize object with a span property in your registration payload. The span value (typically between 2 and 6) determines the widget's width in the CSS grid layout. The dashboard grid in src/admin/pages/dashboard/widgets/index.ts uses this value for the initial placement, though users can resize widgets through the UI if the widget supports dynamic sizing.
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 →