> ## Documentation Index
> Fetch the complete documentation index at: https://docs.graphy.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Slots

Slots replace how a single region of the chart renders while the [spec](/sdk-next/concepts/how-a-chart-is-built) still owns *whether* that region exists and *what* data it receives. Your override gets the same render-ready props the default would. Pass them through the `slots` prop.

```tsx theme={null}
<GraphRenderer slots={{ Header: MyHeader, Tooltip: MyTooltip }} />
```

## Regions

| Slot | Region |
| - | - |
| `Header` | Title / subtitle area above the plot |
| `Footer` | Caption, source, and provenance-mark area below the plot |
| `Tooltip` | The hover tooltip |
| `Grid` | The plot grid lines |
| `Swatch` | The color swatch used in legend and tooltip |
| `EditorSurface` | The chart's editor layer, mounted over the frame in `mode="editable"` |
| `Legend` | The series legend |
| `Headline` | Headline numbers |
| `AxisTicks` | An axis's tick labels |
| `AxisLabel` | An axis's title |

`EditorSurface` defaults to a no-op, so a read-only embed bundles no editing code; `@graphysdk/react-renderer/editable` is what fills it. A chart that only paints leaves it alone.

<Note>
  Custom `Footer` / `Header` slots receive a `brandMark` visual (`'full' |
      'mini' | 'hidden'`), resolved from `config.content.brandMark` and the frame
  size. That prop is **not** the visibility toggle; the toggle is
  `brandMark.enabled` (or legacy `isBrandMarkVisible`). `source` remains your
  data-source attribution. The default badge paints inside the matching region
  when placement matches.
</Note>

## Three kinds of slot

How a region earns its space decides what you hand the slot. The type reflects it: layout-safe slots (the first two kinds) are bare components, layout-coupled slots are a `SlotOverride` object.

### Box-filling — `Tooltip`, `Grid`, `Swatch`, `EditorSurface`

These paint inside a box the layout already sized, or float free of it. Provide a component and nothing else:

```tsx theme={null}
import type { TooltipSlotProps } from '@graphysdk/react-renderer';

function MyTooltip({ content }: TooltipSlotProps) {
  return (
    <div className="my-tooltip">
      {content.heading ? <strong>{content.heading}</strong> : null}
      {content.rows.map((row) => (
        <p key={row.key} style={{ fontWeight: row.isPrimary ? 700 : 400 }}>
          {row.label}: {row.value}
        </p>
      ))}
    </div>
  );
}

<GraphRenderer slots={{ Tooltip: MyTooltip }} />;
```

Rows follow legend order. The hovered row is the one with `isPrimary` set, not the first.

The tooltip's positioning stays built in — you own the body, not the anchor.

### DOM-measured — `Header`, `Footer`

These reserve their own space, and the layout measures the rendered DOM to find out how much. Both prop types carry a `ref`: **forward it to your outer element**, or the region reserves zero height and the plot paints over it.

```tsx theme={null}
import type { HeaderSlotProps } from '@graphysdk/react-renderer';

function MyHeader({ ref, title, isTitleVisible }: HeaderSlotProps) {
  return (
    <div ref={ref} className="my-header">
      {isTitleVisible && typeof title === 'string' ? <h2>{title}</h2> : null}
    </div>
  );
}
```

### Layout-coupled — `Legend`, `Headline`, `AxisTicks`, `AxisLabel`

These reserve an edge band the layout sizes up front, so they're a `SlotOverride`: a `render` component **and** a `measure` that reports the region's size. Paint and reserved space must agree, so the layout asks `measure` how much room to leave:

```tsx theme={null}
<GraphRenderer
  slots={{
    AxisLabel: {
      render: MyAxisLabel,
      measure: (axis, ctx) =>
        axis.label === null
          ? 0
          : ctx.measureText(axis.label, {
              family: 'Inter',
              size: 12 * ctx.textScale,
            }).height + 8,
    },
  }}
/>
```

`Legend`, `AxisTicks` and `AxisLabel` take a `(region, ctx) => number` — the band's thickness in pixels, a height on the top and bottom edges and a width on the left and right. `Headline` takes a `HeadlineMeasurer` instead: an object with `measureHeadline(headline, size)` returning a `{ width, height }` box and `measureHeadlineItemWidths(headline, size)` returning each strip item's width.

```tsx theme={null}
import type { HeadlineMeasurer } from '@graphysdk/viz-engine';

const headlineMeasure: HeadlineMeasurer = {
  measureHeadline: () => ({ width: 240, height: 64 }),
  measureHeadlineItemWidths: (headline) => headline.items.map(() => 120),
};

<GraphRenderer
  slots={{ Headline: { render: MyHeadline, measure: headlineMeasure } }}
/>;
```

The second argument to the function-shaped measures is a `SlotMeasureContext`: `measureText(text, { family, size })` — the same Canvas-backed measurer the built-in regions use, taking a final pixel size — and the active `textScale` to multiply an em size by. Sizing from those keeps your reserved band matched to what actually paints.

<Warning>
  Give a layout-coupled slot's `measure` a stable reference (define it outside
  render or memoize it). The layout reads overrides through a ref so an inline
  `measure` still takes effect on the next paint — but that paint doesn't
  retrigger layout, so paint and reserved space drift apart.
</Warning>

Two of the measures are skipped where the band is empty: `AxisTicks`'s reserves `0` for an axis with no tick band, and `Legend`'s reserves `0` for an empty legend — your override isn't consulted in those cases. `AxisLabel`'s has no such gate.

### Pressable guides in `point-and-edit`

A press on a tick label, an axis title, a legend pill, a direct label or a grid line selects its axis, legend or grid lines through attributes on the node that paints it. A custom `Grid`, `AxisTicks`, `AxisLabel` or `Legend` joins by spreading the same attributes, only where `useGuidesTakePress()` is true:

```tsx theme={null}
import { stampAxisEditTarget, useGuidesTakePress, type AxisTicksSlotProps } from '@graphysdk/react-renderer';

function MyAxisTicks({ formattedAxes, tickRects }: AxisTicksSlotProps) {
  const takesPress = useGuidesTakePress();
  // ...for each axis and each tick at `tickIndex`:
  return (
    <g {...(takesPress ? stampAxisEditTarget(axis.scaleAestheticKey, tickIndex) : {})} cursor={takesPress ? 'pointer' : undefined}>
      <text>{tick.formattedLabel}</text>
    </g>
  );
}
```

* **Tick labels** spread `stampAxisEditTarget(scaleAestheticKey, tickIndex)`, where `tickIndex` counts the axis's `ticks` in `formattedAxes`, so a double click on a band axis's tick takes that band.
* **Axis titles** spread `stampAxisEditTarget(scaleAestheticKey)`, with no tick.
* **Legend pills and direct labels** spread `stampLegendEditTarget({ legendIndex, itemIndex })`, counting `formattedLegends` and that legend's items, so a double click takes the item's group.
* **Grid lines** spread `stampGridEditTarget(axis)`, `'x'` or `'y'`, on a group holding the stroke and a hit area of your own, such as a transparent stroke a few pixels wide with `pointer-events="stroke"`. The capture rect over the panel takes the event, so the editor reads the element stack under it and finds the stamp there.

The attributes go on the element holding the paint and whatever area should take the press, and that element must take pointer events. A node without them takes no press, and a click there selects the chart. A grid line your slot hides or moves takes its hit area with it: nothing else answers for it.

## Slot props

Every props type is exported from `@graphysdk/react-renderer`, along with `DefaultHeader`, `DefaultFooter`, `DefaultTooltip`, `DefaultGrid` and `DefaultSwatch` to wrap or delegate to.

| Slot | Props type | Members |
| - | - | - |
| `Header` | `HeaderSlotProps` | `ref?`, `headerRect`, `mode?`, `title`, `isTitleVisible`, `subtitle`, `isSubtitleVisible`, `brandMark` |
| `Footer` | `FooterSlotProps` | `ref?`, `footerRect`, `mode?`, `caption`, `isCaptionVisible`, `source`, `isSourceVisible`, `brandMark` |
| `Tooltip` | `TooltipSlotProps` | `content` — `{ heading, rows, comment }` |
| `Grid` | `GridSlotProps` | `axes`, `panelBorderSizes`, `panelFrameRect`, `panelRect` |
| `Swatch` | `SwatchSlotProps` | `shape`, `color`, `surface`, `label?`, `lineType?`, `width?`, `height?`, `alpha?`, `cornerRadius?` |
| `EditorSurface` | `EditorSurfaceSlotProps` | `frameElement`, `panelRect`, `formattedAxes` |
| `Legend` | `LegendSlotProps` | `formattedLegends`, `rects` |
| `Headline` | `HeadlineSlotProps` | `headline`, `rect`, `resolvedSize`, `visibleItemCount` |
| `AxisTicks` | `AxisTicksSlotProps` | `formattedAxes`, `tickRects` |
| `AxisLabel` | `AxisLabelSlotProps` | `formattedAxes`, `labelRects` |

The rects a slot receives are SVG-local for the in-panel regions (`Grid`, `AxisTicks`, `AxisLabel`) and layout rects for the rest.

### A custom `Grid` in point and edit

In `point-and-edit` mode a press on a grid line selects the grid of its axis, and the editor finds the line by a stamp on the node that paints it. A custom `Grid` joins the same way: spread `stampGridEditTarget(scaleAestheticKey)` on a group holding each line's stroke and a transparent hit stroke on the same coordinates, and render both only where `useGuidesTakePress()` is true. A line you hide or move takes its hit area with it, and a line you do not stamp takes no press.

```tsx theme={null}
import {
  stampGridEditTarget,
  useGuidesTakePress,
} from '@graphysdk/react-renderer';

function MyGridLine({
  scaleAestheticKey,
  ...lineProps
}: { scaleAestheticKey: 'x' | 'y' | 'ySecondary' } & React.SVGProps<SVGLineElement>) {
  const takesPress = useGuidesTakePress();
  if (!takesPress) {
    return <line {...lineProps} />;
  }
  return (
    <g {...stampGridEditTarget(scaleAestheticKey)}>
      <line {...lineProps} />
      <line
        {...lineProps}
        stroke="transparent"
        strokeWidth={7}
        pointerEvents="stroke"
      />
    </g>
  );
}
```

<Note>
  A slot's `measure` lands on the layout measurer under its own name:
  `AxisTicks.measure` becomes `measureAxis`, `AxisLabel.measure` becomes
  `measureAxisLabel`, and `Headline.measure`'s two methods keep theirs. That's
  the `SlotMeasureOverrides` shape you'll see in the layout types.
</Note>

## Related

* [Geom renderers](/sdk-next/extending/geom-renderers) — replace a whole geom's paint, not just a region
* [Interactivity](/sdk-next/rendering/interactivity) — the `Tooltip` slot in context
* [Theming](/sdk-next/rendering/theming) — restyle regions with tokens before reaching for a slot


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.