Dashboard Renderer Contributions
Extensions can replace Superset's built-in dashboard renderer with a custom implementation. This allows dashboards to be displayed in entirely different ways — kiosk layouts, alternative grid engines, story-style presentations — while reusing Superset's data fetching, authentication, theming, and URL/permalink handling.
Overview
The dashboard renderer is a single-slot contribution point with two tiers:
- Superset's built-in renderer is itself registered as the default provider (
superset.dashboard-renderer) through the same contribution point. It renders whenever no custom renderer is active — including when theENABLE_EXTENSIONSfeature flag is off — so dashboards always display, extensions or not. - At most one custom renderer is active at a time. The most recently registered renderer wins; a previously registered custom renderer is displaced and unregistered with a console warning. The default provider is never displaced.
- Disposing the active custom renderer's
Disposablefalls back to the built-in default. - Custom renderers handle view mode only. When a dashboard enters edit mode, the host always renders the built-in renderer (which owns drag-and-drop editing, undo/redo, and the component pane), returning to the custom renderer when edit mode exits.
- A custom renderer that throws is contained by an error boundary; the host does not fall back to the built-in renderer on error.
The host keeps its behavior identical regardless of which renderer is active: it fetches the dashboard, charts, and datasets, resolves initial filter state from the URL (permalinks, native_filters_key, legacy filter params), injects dashboard CSS, and manages the document title. The renderer receives the results as props.
The Props Contract
Your renderer component receives DashboardRendererProps from @apache-superset/core/dashboards:
| Prop | Type | Description |
|---|---|---|
dashboard | DashboardInfo | Identity and parsed metadata: id, uuid, slug, title, css, metadata (parsed json_metadata), layout (parsed position_json), isPublished, isManagedExternally |
charts | DashboardChart[] | Chart (slice) definitions as returned by GET /api/v1/dashboard/{id}/charts |
datasets | DashboardDataset[] | Datasets as returned by GET /api/v1/dashboard/{id}/datasets |
initialDataMask | DashboardDataMask | Initial filter state resolved by the host from the URL |
initialActiveTabs | string[]? | Layout component ids of the initially active tabs (from permalink) |
initialAnchor | string? | Layout component id to scroll to on mount (permalink anchor) |
uiConfig | DashboardUiConfig? | Chrome-hiding flags (hideTitle, hideTab, hideChartControls, emitDataMasks), mirroring the embedded SDK's uiConfig |
onDataMaskChange | callback? | Reserved — not supplied by the host yet |
onActiveTabsChange | callback? | Reserved — not supplied by the host yet |
The contract is designed to be Redux-free: everything a renderer needs to display a dashboard arrives via props, and host services are available through the public window.superset namespaces (authentication, navigation, theme, translation, and so on).
Renderer responsibilities
- Chart data fetching: the host does not fetch chart data. Query for it yourself (e.g.
POST /api/v1/chart/datawith query contexts built from each chart'sform_data). - Filter orchestration: applying
initialDataMask, reacting to filter interactions, and refreshing affected charts are the renderer's responsibility. - Layout interpretation:
dashboard.layoutis the parsedposition_jsoncomponent tree (rows, columns, tabs, charts, markdown); interpret as much or as little of it as your presentation needs.
Theming works out of the box: renderers are mounted inside the host's theme providers, so useTheme from @apache-superset/core/theme reflects the dashboard's active theme.
Registering a Renderer
Register the renderer as a module-level side effect in your extension's entry point:
import { dashboards } from '@apache-superset/core';
import type { ComponentType } from 'react';
const KioskDashboardRenderer: ComponentType<
dashboards.DashboardRendererProps
> = ({ dashboard, charts, initialDataMask }) => (
<main>
<h1>{dashboard.title}</h1>
{/* render charts from `charts` + `dashboard.layout` */}
</main>
);
dashboards.registerDashboardRenderer(
{ id: 'acme.kiosk-dashboard', name: 'Kiosk Dashboard Renderer' },
KioskDashboardRenderer,
);
registerDashboardRenderer returns a Disposable. Disposing it removes your renderer if it is still the active one; disposing after being displaced by a newer registration is a no-op.
You can observe slot changes with dashboards.onDidRegisterDashboardRenderer and dashboards.onDidUnregisterDashboardRenderer, and inspect the active provider with dashboards.getDashboardRenderer() (which returns the built-in default when no custom renderer is active).
Augmenting the built-in renderer
To augment rather than fully replace the built-in renderer, retrieve the default provider and wrap its component:
const defaultProvider = dashboards.getDefaultDashboardRenderer();
dashboards.registerDashboardRenderer(
{ id: 'acme.framed-dashboard', name: 'Framed Dashboard' },
props => (
<AcmeFrame>
{defaultProvider && <defaultProvider.component {...props} />}
</AcmeFrame>
),
);
Example Implementation
A minimal renderer that presents the dashboard as a flat list of chart cards, ignoring the grid layout — the kind of starting point a kiosk or mobile-style renderer might build on:
ChartListRenderer.tsx
import type { dashboards } from '@apache-superset/core';
export default function ChartListRenderer({
dashboard,
charts,
uiConfig,
}: dashboards.DashboardRendererProps) {
return (
<main>
{!uiConfig?.hideTitle && <h1>{dashboard.title}</h1>}
<ul>
{charts.map(chart => (
<li key={chart.id}>
<h2>{chart.slice_name}</h2>
<p>{String(chart.viz_type)}</p>
{/* Fetch and render data for this chart, e.g. by building a
query context from chart.form_data and POSTing to
/api/v1/chart/data */}
</li>
))}
</ul>
</main>
);
}
index.tsx
import { dashboards } from '@apache-superset/core';
import ChartListRenderer from './ChartListRenderer';
// Registration is a module-load side effect — no activate() call needed
dashboards.registerDashboardRenderer(
{ id: 'my-extension.chart-list', name: 'Chart List Renderer' },
ChartListRenderer,
);
To interpret the full grid instead, walk dashboard.layout from its ROOT_ID entry — each node's type (TABS, TAB, ROW, COLUMN, CHART, MARKDOWN, ...) and children describe the arrangement, and CHART nodes carry the chart id in meta.chartId.
Dashboards API Reference
All methods are available on the dashboards namespace from @apache-superset/core:
| Method / Event | Description |
|---|---|
registerDashboardRenderer(descriptor, component) | Register a custom dashboard renderer. Returns a Disposable to unregister; disposing falls back to the built-in default. |
getDashboardRenderer() | Returns the active provider — the custom renderer when one is registered, otherwise the built-in default. |
getDefaultDashboardRenderer() | Returns the built-in default provider (superset.dashboard-renderer), e.g. to wrap it. |
onDidRegisterDashboardRenderer(listener) | Subscribe to registration events. Returns a Disposable. |
onDidUnregisterDashboardRenderer(listener) | Subscribe to unregistration events (including displacement by a newer registration). Returns a Disposable. |
Manifest Declaration
Declare the renderer in your extension's Contributions metadata (at most one per extension):
{
"dashboardRenderer": {
"id": "acme.kiosk-dashboard",
"name": "Kiosk Dashboard Renderer",
"description": "Full-screen kiosk presentation of dashboards"
}
}
Current Limitations
- Extensions load asynchronously after startup, so a dashboard opened before your extension finishes loading renders with the built-in renderer first and swaps to yours when registration lands.
onDataMaskChangeandonActiveTabsChangeare defined in the contract but not consumed by the host yet — filter state changed inside a custom renderer does not persist to permalinks.- While a custom renderer is active the host still hydrates its internal dashboard state so permalinks and embedded behavior remain intact; this is transparent to renderers but means the built-in state bookkeeping still runs.
Next Steps
- Contribution Types — Explore other contribution types
- Editors Extension Point — Another replace-the-default contribution point
- Development — Set up your development environment