# How to Add Custom Dashboard Widgets to the Instatic Admin Interface

> Learn to add custom dashboard widgets to the Instatic admin interface using React components and the plugin SDK. Enhance your Instatic experience with personalized dashboards.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: how-to-guide
- Published: 2026-08-02

---

**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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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.

```tsx
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`](https://github.com/CoreBunch/Instatic/blob/main/editor/index.ts)), import your component and call the registration API:

```ts
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`](https://github.com/CoreBunch/Instatic/blob/main/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:

```json
{
  "name": "my-plugin",
  "apiVersion": "v2",
  "permissions": ["dashboard.widgets.register"]
}

```

The runtime checks this permission in [`src/core/plugins/runtime.ts`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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:

- **[`src/ui/components/Widget/Widget.tsx`](https://github.com/CoreBunch/Instatic/blob/main/src/ui/components/Widget/Widget.tsx)**: The chrome component that supplies the card surface, title bar, and `--tint` CSS variable.
- **[`src/core/dashboard/registry.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/dashboard/registry.ts)**: The central map storing all widget definitions that the dashboard grid consumes.
- **[`src/core/plugin-sdk/types/dashboardWidgets.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/types/dashboardWidgets.ts)**: TypeScript definitions for `DashboardWidgetRegistration` and the permission constant.
- **[`src/admin/pages/dashboard/widgets/PostsWidget.tsx`](https://github.com/CoreBunch/Instatic/blob/main/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 `Widget` component from `@instatic/host-ui` and registering via `api.dashboard.widgets.register`.
- The `Widget` chrome in [`src/ui/components/Widget/Widget.tsx`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/runtime.ts).
- Widget definitions are stored in [`src/core/dashboard/registry.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/dashboard/registry.ts) and rendered by the grid system in [`src/admin/pages/dashboard/widgets/index.ts`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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.