> Documentation index: https://www.gooddata.ai/docs/gooddata-ui/llms.txt lists every page of these docs as Markdown.

# Customize widgets and visualizations

> Choose which layer of a dashboard widget to replace (the widget, the visualization, or its body) and how drills flow through custom renderers.

API reference: [IDashboardInsightCustomizer](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.idashboardinsightcustomizer/index.md) (interface), [IDashboardWidgetCustomizer](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.idashboardwidgetcustomizer/index.md) (interface), [IDashboardCustomComponentProps](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.idashboardcustomcomponentprops/index.md) (interface)

The Dashboard component renders every widget through a small stack of replaceable React components. Pick the right layer and you keep the parts you don't want to rebuild: the title, the menu, the dashboard filters, exports, and drilling. This page maps the stack, names the plugin method and the [`Dashboard`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.dashboard/index.md) prop for each layer, and explains what a custom renderer has to do so that drilling keeps working. The individual methods are described in [Dashboard plugin API](https://www.gooddata.ai/docs/gooddata-ui/latest/dashboard-plugins/dashboard-plugins-api/index.md#customization-api).

## [How a widget is rendered](https://www.gooddata.ai/docs/gooddata-ui/latest/dashboard-plugins/customize-widgets/#how-a-widget-is-rendered)

From the outside in:

1. **Layout.** [`DefaultDashboardLayout`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.defaultdashboardlayout/index.md) places the dashboard's sections and items on the 12-column grid. Each item holds one widget.
2. **Widget.** For every item, the layout asks the widget provider for a component and renders it with [`IDashboardWidgetProps`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.idashboardwidgetprops/index.md). `DefaultDashboardWidget` picks a renderer by widget type: visualization, rich text, visualization switcher, or nested layout. It refuses custom widgets, which is why registering a custom widget type always includes registering its component.
3. **Visualization widget chrome.** The built-in visualization widget draws the frame: the headline with the title, the description, the **⋯** menu button and the menu it opens, and warnings such as a partial result.
4. **Visualization.** Inside the frame, the insight provider's component renders with [`IDashboardInsightProps`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.idashboardinsightprops/index.md). [`DefaultDashboardInsight`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.defaultdashboardinsight/index.md) resolves the dashboard filters that apply to the widget, works out what is drillable, shows the loading and error states, and then hands over to the body.
5. **Visualization body.** The body provider's component receives [`IInsightBodyProps`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.iinsightbodyprops/index.md): the visualization with the widget's filters already merged in, the drillable items, the chart config, the color palette, the locale, and callbacks. [`DefaultInsightBody`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.defaultinsightbody/index.md) is a thin wrapper around `InsightRenderer` from `@gooddata/sdk-ui-ext`.

Rich text widgets and visualization switcher widgets have their own providers at level 4 and no body layer.

## [Pick the layer](https://www.gooddata.ai/docs/gooddata-ui/latest/dashboard-plugins/customize-widgets/#pick-the-layer)

| You want to | Replace | You keep |
| :-- | :-- | :-- |
| Draw a visualization with your own library, such as D3 or a table | the body | the title, the menu, exports, dashboard filters, drillable items |
| Change how a visualization loads, or which filters it honors | the visualization | the title and the menu |
| Add a frame, a badge, or a footer to visualizations | nothing, decorate the visualization | everything |
| Put other content on the dashboard: text, an image, a form, your own component | nothing, add a custom widget type | the layout and the filters |
| Change the **⋯** menu | the menu items, the menu, or its button | everything else |
| Intercept every widget, including rich text | the widget | the layout |

## [Plugin API and Dashboard props](https://www.gooddata.ai/docs/gooddata-ui/latest/dashboard-plugins/customize-widgets/#plugin-api-and-dashboard-props)

Each layer has a method on the plugin customizer and a prop on the [`Dashboard`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.dashboard/index.md) component. Use the plugin API when the customization ships as a plugin. Use the props when your own React app renders the Dashboard component and you don't ship a plugin. Both take the same provider function: it receives the object to render and returns a component, or `undefined` to fall back to the default. A decorator receives the next provider in the chain instead and wraps what it returns.

| Layer | Plugin API | Dashboard prop | Component props | Default component |
| :-- | :-- | :-- | :-- | :-- |
| Layout | `layout().withCustomProvider()`, `withCustomDecorator()`, `customizeFluidLayout()` | [`DashboardLayoutComponentProvider`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.dashboardlayoutcomponentprovider/index.md) | [`IDashboardLayoutProps`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.idashboardlayoutprops/index.md) | [`DefaultDashboardLayout`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.defaultdashboardlayout/index.md) |
| Widget | `customWidgets().addCustomWidget()` | [`WidgetComponentProvider`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.widgetcomponentprovider/index.md) | [`IDashboardWidgetProps`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.idashboardwidgetprops/index.md) | `DefaultDashboardWidget` |
| Visualization | `insightWidgets().withCustomProvider()`, `withTag()`, `withCustomDecorator()` | [`InsightComponentProvider`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.insightcomponentprovider/index.md) | [`IDashboardInsightProps`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.idashboardinsightprops/index.md) | [`DefaultDashboardInsight`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.defaultdashboardinsight/index.md) |
| Visualization body | `insightWidgets().withCustomInsightBodyProvider()` (alpha) | [`InsightBodyComponentProvider`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.insightbodycomponentprovider/index.md) (alpha) | [`IInsightBodyProps`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.iinsightbodyprops/index.md) | [`DefaultInsightBody`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.defaultinsightbody/index.md) |
| Menu button | — | [`InsightMenuButtonComponentProvider`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.insightmenubuttoncomponentprovider/index.md) (alpha) | [`IDashboardInsightMenuButtonProps`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.idashboardinsightmenubuttonprops/index.md) | `DefaultDashboardInsightMenuButton` |
| Menu | — | [`InsightMenuComponentProvider`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.insightmenucomponentprovider/index.md) (alpha), `insightMenuItemsProvider` (alpha) | [`IDashboardInsightMenuProps`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.idashboardinsightmenuprops/index.md) | [`DefaultDashboardInsightMenu`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.defaultdashboardinsightmenu/index.md) |
| Rich text widget | `richTextWidgets().withCustomProvider()`, `withCustomDecorator()` | [`RichTextComponentProvider`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.richtextcomponentprovider/index.md) | [`IDashboardRichTextProps`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.idashboardrichtextprops/index.md) | [`DefaultDashboardRichText`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.defaultdashboardrichtext/index.md) |
| Visualization switcher | `visualizationSwitcherWidgets().withCustomSwitcherProvider()`, `withCustomToolbarProvider()`, and their decorators | [`VisualizationSwitcherComponentProvider`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.visualizationswitchercomponentprovider/index.md), [`VisualizationSwitcherToolbarComponentProvider`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.visualizationswitchertoolbarcomponentprovider/index.md) | [`IDashboardVisualizationSwitcherProps`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.idashboardvisualizationswitcherprops/index.md) | [`DefaultDashboardVisualizationSwitcher`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.defaultdashboardvisualizationswitcher/index.md) |

The alpha entries can change between minor releases. See [API maturity](https://www.gooddata.ai/docs/gooddata-ui/latest/integrations/platform/api-maturity/index.md).

## [Draw a visualization yourself](https://www.gooddata.ai/docs/gooddata-ui/latest/dashboard-plugins/customize-widgets/#draw-a-visualization-yourself)

Replacing the body is the least work when a built-in chart isn't what you need but everything around it is. Your component gets the visualization (`insight`) with the widget's dashboard filters merged in, so executing it is enough. Tag the visualizations in your workspace that should use the custom body, and register the body in the plugin:

```tsx
import { insightTags } from "@gooddata/sdk-model";
import { DashboardPluginV1, type DashboardContext, type IDashboardCustomizer } from "@gooddata/sdk-ui-dashboard";

import { BarsBody } from "./BarsBody.js";

export class Plugin extends DashboardPluginV1 {
    public register(_ctx: DashboardContext, customize: IDashboardCustomizer): void {
        customize
            .insightWidgets()
            .withCustomInsightBodyProvider((insight) =>
                insightTags(insight).includes("custom-bars") ? BarsBody : undefined,
            );
    }
}
```

The body renders the result as a list. It reports loading and errors through the callbacks it received, so the widget frame can react, and pushes the data view so that the **⋯** menu offers CSV and XLSX export of what you rendered:

```tsx
import { useEffect, useMemo } from "react";
import { useExecutionDataView } from "@gooddata/sdk-ui";
import type { IInsightBodyProps } from "@gooddata/sdk-ui-dashboard";

export function BarsBody(props: IInsightBodyProps) {
    const { insight, backend, workspace, onLoadingChanged, onError, pushData, LoadingComponent, ErrorComponent } =
        props;

    // The visualization already carries the dashboard filters that apply to this widget.
    const execution = useMemo(
        () => backend.workspace(workspace).execution().forInsight(insight),
        [backend, workspace, insight],
    );
    const { result, status, error } = useExecutionDataView({ execution }, [execution]);

    useEffect(() => {
        onLoadingChanged?.({ isLoading: status === "loading" });
    }, [status, onLoadingChanged]);

    useEffect(() => {
        if (error) {
            onError?.(error);
        }
    }, [error, onError]);

    useEffect(() => {
        if (result) {
            pushData?.({ dataView: result.dataView });
        }
    }, [result, pushData]);

    if (status === "error") {
        return <ErrorComponent message={error?.message ?? "Unknown error"} />;
    }
    if (status !== "success" || !result) {
        return <LoadingComponent />;
    }

    const slices = result.data().slices().toArray();
    return (
        <ol>
            {slices.map((slice) => (
                <li key={slice.id}>
                    {slice.sliceTitles().join(" / ")}: {slice.dataPoints()[0]?.formattedValue()}
                </li>
            ))}
        </ol>
    );
}
```

Without a plugin, pass the same function as the [`InsightBodyComponentProvider`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.insightbodycomponentprovider/index.md) prop of the [`Dashboard`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.dashboard/index.md) component.

A custom body has to implement drilling itself. The built-in charts compare each rendered point with `drillableItems`, mark the matches as clickable, and call `onDrill` with an [`IDrillEvent`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui/sdk-ui.idrillevent/index.md) when the user clicks one. In your own body, `drillableItems` arrive as header predicates and object references. Turn them into predicates with `convertDrillableItemsToPredicates` from `@gooddata/sdk-ui`, test each point's headers with `isSomeHeaderPredicateMatched`, and call `onDrill` with the clicked point's `intersection`. [Make visualizations drillable](https://www.gooddata.ai/docs/gooddata-ui/latest/integrations/interactivity/drillable-items/index.md) describes the predicates, and [How drills flow](#how-drills-flow) below describes what the dashboard does with the event.

## [Replace or decorate the whole visualization](https://www.gooddata.ai/docs/gooddata-ui/latest/dashboard-plugins/customize-widgets/#replace-or-decorate-the-whole-visualization)

When loading, filtering, or drilling itself should work differently, register a component for the visualization layer with `withCustomProvider()` or `withTag()`, or wrap the default with `withCustomDecorator()`. Your component receives [`IDashboardInsightProps`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.idashboardinsightprops/index.md) with the `insight`, the `widget`, and the drill callbacks, and it is responsible for everything inside the frame, including loading and error states. [Customize rendering of visualizations](https://www.gooddata.ai/docs/gooddata-ui/latest/dashboard-plugins/dashboard-plugins-api/index.md#customize-rendering-of-visualizations) walks through the three methods and shows a decorator.

The `insight` at this layer does not have the dashboard filters merged in. Resolve them with [useWidgetFilters](https://www.gooddata.ai/docs/gooddata-ui/latest/dashboard-plugins/use-widget-filters/index.md) and pass them to the execution, or render an [`InsightView`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-ext/sdk-ui-ext.insightview/index.md) and let it do the work; see [Enable CSV and XLSX export for custom visualization renderers](https://www.gooddata.ai/docs/gooddata-ui/latest/dashboard-plugins/dashboard-plugins-api/index.md#enable-csv-and-xlsx-export-for-custom-visualization-renderers) for the `pushData` detail that keeps the export menu working.

## [Change the widget menu](https://www.gooddata.ai/docs/gooddata-ui/latest/dashboard-plugins/customize-widgets/#change-the-widget-menu)

The **⋯** menu is built from a list of items. To add, remove, or reorder them, pass `insightMenuItemsProvider` to the [`Dashboard`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.dashboard/index.md) component. The function receives the default items and a `closeMenu` callback, and returns the items to show:

```tsx
import { Dashboard, type InsightMenuItemsProvider } from "@gooddata/sdk-ui-dashboard";

const menuItems: InsightMenuItemsProvider = (_insight, widget, defaultItems, closeMenu) => [
    ...defaultItems,
    { type: "separator", itemId: "my-separator" },
    {
        type: "button",
        itemId: "copy-widget-id",
        itemName: "Copy widget ID",
        onClick: () => {
            void navigator.clipboard.writeText(widget.identifier);
            closeMenu();
        },
    },
];

export function DashboardPage({ dashboard }: { dashboard: string }) {
    return <Dashboard dashboard={dashboard} insightMenuItemsProvider={menuItems} />;
}
```

Items are buttons, separators, submenus, or groups; the [`IInsightMenuItem`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.iinsightmenuitem/index.md) union in the API reference lists their fields. To restyle the menu or its button rather than its contents, replace them through [`InsightMenuComponentProvider`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.insightmenucomponentprovider/index.md) and [`InsightMenuButtonComponentProvider`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.insightmenubuttoncomponentprovider/index.md). Plugins have no access to the menu. They can decorate the visualization instead, or trigger their own commands from a custom widget.

## [Add a custom widget](https://www.gooddata.ai/docs/gooddata-ui/latest/dashboard-plugins/customize-widgets/#add-a-custom-widget)

A custom widget is content that isn't a saved visualization: a text block, a form, an image, your own component. You register a type name with its component and then place instances of it in the layout. The component gets [`IDashboardWidgetProps`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.idashboardwidgetprops/index.md), where `widget` is your [`ICustomWidget`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.icustomwidget/index.md) with the `customType` and any extra data you passed to `newCustomWidget`. [Use custom widgets](https://www.gooddata.ai/docs/gooddata-ui/latest/dashboard-plugins/dashboard-plugins-api/index.md#use-custom-widgets) covers registration and layout placement. To compute data that honors the dashboard filters, use [useCustomWidgetExecutionDataView](https://www.gooddata.ai/docs/gooddata-ui/latest/dashboard-plugins/use-custom-widget-data-view/index.md), and to read the filters themselves, [useWidgetFilters](https://www.gooddata.ai/docs/gooddata-ui/latest/dashboard-plugins/use-widget-filters/index.md).

## [How drills flow](https://www.gooddata.ai/docs/gooddata-ui/latest/dashboard-plugins/customize-widgets/#how-drills-flow)

A drill starts in the body and travels up through commands and events. Knowing the sequence helps when you replace a layer or want to react to clicks from outside.

1. The Dashboard component computes `drillableItems` for each widget: the drill interactions the dashboard author configured on the widget, implicit drill-downs from attribute hierarchies, cross-filtering, [Key Driver Analysis](https://www.gooddata.ai/docs/gooddata-ui/latest/ai-modules/key-driver-analysis/index.md), and anything you added with the `changeDrillableItems` command. The body gets them as a prop.
2. The user clicks a drillable point and the body calls `onDrill`. The dashboard dispatches the `drill` command and fires `GDC.DASH/EVT.DRILL.REQUESTED`.
3. The dashboard matches the click against the configured drill definitions and fires `GDC.DASH/EVT.DRILL.RESOLVED`. Its payload has the `drillEvent` with the clicked `intersection` and the matching `drillDefinitions`, and the `drillContext` with the `widget` and `insight`.
4. With exactly one matching definition that isn't restricted, the dashboard runs it right away. With several, with a restricted drill, or for Key Driver Analysis, which always offers a choice of comparisons, it shows a menu. With `disableDefaultDrills` set in the dashboard `config`, nothing runs after `RESOLVED` and your event handler decides. Each kind of drill has its own command and `REQUESTED`/`RESOLVED` pair: `DRILL_DOWN`, `DRILL_TO_INSIGHT`, `DRILL_TO_DASHBOARD`, `DRILL_TO_CUSTOM_URL`, `DRILL_TO_ATTRIBUTE_URL`, `CROSS_FILTERING`, and `KEY_DRIVER_ANALYSIS`, all under `GDC.DASH/EVT.DRILL.`.

Items you add with `changeDrillableItems` make points clickable without a configured drill. For those, `drillDefinitions` in the resolved event is empty and nothing else happens, so handle the click yourself. This plugin makes every point of the `revenue` measure clickable once the dashboard is initialized and logs the clicks:

```ts
import { HeaderPredicates } from "@gooddata/sdk-ui";
import {
    DashboardPluginV1,
    changeDrillableItems,
    type DashboardContext,
    type DashboardEventHandler,
    type DashboardInitialized,
    type IDashboardCustomizer,
    type IDashboardDrillResolved,
    type IDashboardEventHandling,
    isDashboardDrillResolved,
} from "@gooddata/sdk-ui-dashboard";

const onDrillResolved: DashboardEventHandler<IDashboardDrillResolved> = {
    eval: isDashboardDrillResolved,
    handler: (event) => {
        const { drillEvent, drillContext } = event.payload;
        // drillEvent.drillContext.intersection: the measure and attribute headers of the clicked point
        // drillEvent.drillDefinitions: configured drills that match the click, empty for your own items
        // drillContext.widget: the widget the click came from
    },
};

export class Plugin extends DashboardPluginV1 {
    public register(_ctx: DashboardContext, _customize: IDashboardCustomizer, handlers: IDashboardEventHandling): void {
        handlers.addEventHandler<DashboardInitialized>("GDC.DASH/EVT.INITIALIZED", (_event, dispatch) => {
            dispatch(changeDrillableItems([HeaderPredicates.identifierMatch("revenue")]));
        });
        handlers.addCustomEventHandler(onDrillResolved);
    }
}
```

Outside a plugin, pass the handlers in the `eventHandlers` prop of the [`Dashboard`](https://www.gooddata.ai/docs/gooddata-ui/latest/api-reference/sdk-ui-dashboard/sdk-ui-dashboard.dashboard/index.md) component; they receive the same `dispatch` argument. From a component rendered inside the dashboard, dispatch the command with [`useDispatchDashboardCommand`](https://www.gooddata.ai/docs/gooddata-ui/latest/dashboard-plugins/dashboard/index.md#commands). [Handle dashboard events](https://www.gooddata.ai/docs/gooddata-ui/latest/dashboard-plugins/dashboard-events/index.md) explains handlers in detail, and [Make visualizations drillable](https://www.gooddata.ai/docs/gooddata-ui/latest/integrations/interactivity/drillable-items/index.md) explains the predicates for standalone charts, which the dashboard reuses here.
