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:

  1. Component Mount: Your component receives the widgetId, title, icon, tint, and span props from the registry metadata.
  2. Grid Integration: The dashboard grid in src/admin/pages/dashboard/widgets/index.ts reads the registry entry and positions your widget according to its defaultSize.
  3. Permission Verification: The runtime validates the dashboard.widgets.register permission 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:

Summary

  • Add custom dashboard widgets to the Instatic admin interface by composing the Widget component from @instatic/host-ui and registering via api.dashboard.widgets.register.
  • The Widget chrome in src/ui/components/Widget/Widget.tsx provides consistent styling, loading states, and accessibility without manual configuration.
  • Registration requires the dashboard.widgets.register permission declared in your plugin manifest, enforced by the runtime in src/core/plugins/runtime.ts.
  • Widget definitions are stored in src/core/dashboard/registry.ts and rendered by the grid system in src/admin/pages/dashboard/widgets/index.ts.
  • Use useAsyncResource for data fetching and reference pixel-art-icons names for the iconName property 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →