---
title: Capability Matrix
description: What XY can be styled and extended with, per renderer, generated from the capability registry.
---

# Capability Matrix

<!-- generated by scripts/gen_capability_matrix.py --write; do not edit -->

Every styling question about XY has the same two halves: *can I change this*,
and *does the change survive where I need it*. This page answers both from the
registry the implementation is checked against.

- **11** mark style properties across **22** mark kinds, drawn by all three renderers.
- **48** stable chrome slots for CSS and Tailwind in the browser.
- **1** way to add a mark kind XY does not ship, without forking it.

## Mark style properties

What a mark's `style=` accepts. Anything outside this raises while the chart is
built — one renderer never silently ignores what another draws.

| property | vocabulary | mark kinds | webgl | svg | native | status |
|---|---|---|---|---|---|---|
| `opacity` | css | `area`, `bar`, `box`, `column`, `contour`, `ecdf`, `error_band`, `errorbar`, `funnel`, `heatmap`, `hexbin`, `hist`, `histogram`, `line`, `ribbon`, `scatter`, `segments`, `stairs`, `stem`, `step`, `triangle_mesh`, `violin` | full | full | full | shipped |
| `fill` | svg | `area`, `bar`, `box`, `column`, `error_band`, `hist`, `histogram`, `scatter`, `triangle_mesh`, `violin` | full | full | full | shipped |
| `fill-opacity` | svg | `area`, `bar`, `box`, `column`, `error_band`, `funnel`, `heatmap`, `hexbin`, `hist`, `histogram`, `ribbon`, `scatter`, `triangle_mesh`, `violin` | full | full | full | shipped |
| `stroke` | svg | `area`, `bar`, `box`, `column`, `contour`, `ecdf`, `error_band`, `errorbar`, `funnel`, `hist`, `histogram`, `line`, `ribbon`, `scatter`, `segments`, `stairs`, `stem`, `step`, `triangle_mesh` | full | full | full | shipped |
| `stroke-opacity` | svg | `area`, `bar`, `box`, `column`, `contour`, `ecdf`, `error_band`, `errorbar`, `funnel`, `hist`, `histogram`, `line`, `ribbon`, `scatter`, `segments`, `stairs`, `stem`, `step`, `triangle_mesh` | full | full | full | shipped |
| `stroke-width` | svg | `area`, `bar`, `box`, `column`, `contour`, `ecdf`, `error_band`, `errorbar`, `funnel`, `hist`, `histogram`, `line`, `ribbon`, `scatter`, `segments`, `stairs`, `stem`, `step`, `triangle_mesh` | full | full | full | shipped |
| `stroke-dasharray` | svg | `area`, `ecdf`, `line`, `stairs`, `step` | full | full | full | shipped |
| `stroke-linecap` | svg | `ecdf`, `line`, `stairs`, `step` | full | full | full | shipped |
| `border-radius` | css | `bar`, `column`, `hist`, `histogram` | full | full | full | shipped |
| `wedge-gap` | xy | `bar`, `column`, `hist`, `histogram` | full | full | full | shipped |
| `marker-shape` | xy | `scatter` | full | full | full | shipped |

### Notes

- **`opacity`** — Multiplies the mark's own alpha in every renderer.
- **`fill`** — A plain color compiles to the mark's paint; a `linear-gradient(...)` compiles to a gradient and is accepted only by area and rect kinds, which are the ones with a gradient program.
- **`fill-opacity`** — Independent of `opacity`; the two multiply.
- **`stroke`** — The paint for line-like geometry, and the border for filled marks.
- **`stroke-width`** — CSS px; a bare number is px, matching the chrome style convention.
- **`stroke-dasharray`** — 2-8 positive px lengths, or `none`. The WebGL client tracks arc length on the CPU so dashes stay continuous across segments and constant on screen through zoom.
- **`stroke-linecap`** — Line family only — a cap is open-path geometry. XY's default is `round`, not CSS's `butt`, because the native rasterizer has always drawn round and is the reference for static export. Verified per renderer: a Rust coverage test, a rasterized-ink test, and three Chromium screenshots that hash differently per cap.
- **`border-radius`** — Rect kinds only. `corner_radius=(tip, base)` rounds the two ends separately.
- **`wedge-gap`** — Gap between neighbouring polar wedges, in px. Rect kinds under `coords="polar"` only; ignored elsewhere. Deliberately a LENGTH rather than an angle: an angular pad's seam is `r * dtheta` wide, so it tapers to nothing at the hole and reads as uneven spacing. The angular inset therefore grows as the radius shrinks, which is the same construction as d3's padAngle/padRadius pair. An XY vocabulary name: CSS has no gap between two arcs.
- **`marker-shape`** — 17 shapes, drawn as analytic signed-distance fields in all three renderers. An XY vocabulary name: CSS has no shape keyword for a non-DOM point mark, and the CSS spelling and `symbol=` compile to the same value.

## Chrome slots

Stable `data-xy-slot` names that take `class_names=` and `styles=`. The native
raster and vector writers have no cascade, so per-slot styling is a browser
mechanism; put anything that must survive export in the chart-level `style=`
token bag or in mark and axis `style=`, which every renderer reads.

| slot | browser | native raster | native vector |
|---|---|---|---|
| `root` | full | partial | partial |
| `title` | full | partial | partial |
| `chrome` | full | none | none |
| `canvas` | full | none | none |
| `annotation_layer` | full | none | none |
| `labels` | full | none | none |
| `legend` | full | partial | partial |
| `legend_title` | full | partial | partial |
| `legend_item` | full | none | none |
| `legend_swatch` | full | none | none |
| `legend_label` | full | partial | partial |
| `colorbar` | full | partial | partial |
| `colorbar_bar` | full | none | none |
| `colorbar_extension` | full | none | none |
| `colorbar_line` | full | none | none |
| `colorbar_tick` | full | partial | partial |
| `colorbar_minor_tick` | full | none | none |
| `colorbar_title` | full | partial | partial |
| `tooltip` | full | none | none |
| `tooltip_title` | full | none | none |
| `tooltip_row` | full | none | none |
| `tooltip_label` | full | none | none |
| `tooltip_value` | full | none | none |
| `modebar` | full | none | none |
| `modebar_drag_handle` | full | none | none |
| `modebar_control_group` | full | none | none |
| `modebar_separator` | full | none | none |
| `modebar_button` | full | none | none |
| `modebar_icon` | full | none | none |
| `modebar_zoom_value` | full | none | none |
| `modebar_indicator` | full | none | none |
| `modebar_selection_icon` | full | none | none |
| `modebar_menu` | full | none | none |
| `modebar_menu_separator` | full | none | none |
| `modebar_menu_icon` | full | none | none |
| `modebar_menu_label` | full | none | none |
| `modebar_history_controls` | full | none | none |
| `selection` | full | none | none |
| `crosshair_x` | full | none | none |
| `crosshair_y` | full | none | none |
| `badge` | full | none | none |
| `badge_item` | full | none | none |
| `axis_band` | full | none | none |
| `axis_line` | full | none | none |
| `tick_mark` | full | none | none |
| `tick_label` | full | partial | partial |
| `axis_title` | full | partial | partial |
| `annotation_label` | full | none | none |

### Notes

- **`root`** (via `chart style=`) — `styles={'root': ...}` is browser-only, but the chart-level `style=` token bag targets the same element and every renderer reads it (`spec['dom']['style']`). Prefer it for anything that must survive export.
- **`title`** (via `styles={'title': ...}`) — Vector (SVG, PDF) honors font-size, font-weight, font-style, font-family, letter-spacing, opacity and the text paint (`fill`, or `color`). The raster writer's glyph primitive takes a size and one RGBA paint and nothing else, so it honors font-size and the paint only — font-weight, font-style, font-family, letter-spacing and opacity are vector-only rather than silently approximated. Properties outside the subset stay browser-only.
- **`legend`** (via `styles={'legend': ...} / xy.legend(style=...) / --chart-legend-bg`) — The frame box. Both spellings and the theme token now converge on one merged declaration block before the writers see it, so what agrees in the browser agrees in a PNG. `background`, `boxShadow`, `borderRadius`, `--xy-legend-frame-alpha`, and `padding`/`rowGap` in `em` are honored; an explicit background paints opaque, as it does in the browser.
- **`legend_title`** (via `styles={'legend_title': ...}`) — Vector (SVG, PDF) honors font-size, font-weight, font-style, font-family, letter-spacing, opacity and the text paint (`fill`, or `color`). The raster writer's glyph primitive takes a size and one RGBA paint and nothing else, so it honors font-size and the paint only — font-weight, font-style, font-family, letter-spacing and opacity are vector-only rather than silently approximated. Properties outside the subset stay browser-only.
- **`legend_label`** (via `styles={'legend_label': ...}`) — Vector (SVG, PDF) honors font-size, font-weight, font-style, font-family, letter-spacing, opacity and the text paint (`fill`, or `color`). The raster writer's glyph primitive takes a size and one RGBA paint and nothing else, so it honors font-size and the paint only — font-weight, font-style, font-family, letter-spacing and opacity are vector-only rather than silently approximated. Properties outside the subset stay browser-only.
- **`colorbar`** (via `styles={'colorbar': ...}`) — Vector (SVG, PDF) honors font-size, font-weight, font-style, font-family, letter-spacing, opacity and the text paint (`fill`, or `color`). The raster writer's glyph primitive takes a size and one RGBA paint and nothing else, so it honors font-size and the paint only — font-weight, font-style, font-family, letter-spacing and opacity are vector-only rather than silently approximated. Properties outside the subset stay browser-only.
- **`colorbar_tick`** (via `styles={'colorbar_tick': ...}`) — Vector (SVG, PDF) honors font-size, font-weight, font-style, font-family, letter-spacing, opacity and the text paint (`fill`, or `color`). The raster writer's glyph primitive takes a size and one RGBA paint and nothing else, so it honors font-size and the paint only — font-weight, font-style, font-family, letter-spacing and opacity are vector-only rather than silently approximated. Properties outside the subset stay browser-only.
- **`colorbar_title`** (via `styles={'colorbar_title': ...}`) — Vector (SVG, PDF) honors font-size, font-weight, font-style, font-family, letter-spacing, opacity and the text paint (`fill`, or `color`). The raster writer's glyph primitive takes a size and one RGBA paint and nothing else, so it honors font-size and the paint only — font-weight, font-style, font-family, letter-spacing and opacity are vector-only rather than silently approximated. Properties outside the subset stay browser-only.
- **`tick_label`** (via `styles={'tick_label': ...}`) — Vector (SVG, PDF) honors font-size, font-weight, font-style, font-family, letter-spacing, opacity and the text paint (`fill`, or `color`). The raster writer's glyph primitive takes a size and one RGBA paint and nothing else, so it honors font-size and the paint only — font-weight, font-style, font-family, letter-spacing and opacity are vector-only rather than silently approximated. Properties outside the subset stay browser-only.
- **`axis_title`** (via `styles={'axis_title': ...}`) — Vector (SVG, PDF) honors font-size, font-weight, font-style, font-family, letter-spacing, opacity and the text paint (`fill`, or `color`). The raster writer's glyph primitive takes a size and one RGBA paint and nothing else, so it honors font-size and the paint only — font-weight, font-style, font-family, letter-spacing and opacity are vector-only rather than silently approximated. Properties outside the subset stay browser-only.

## Extension points

See [Custom Marks](/docs/xy/advanced/custom-marks/) for the worked example.

| extension point | status | entry point | limits |
|---|---|---|---|
| mark_plugin_composition | shipped | `xy.register_mark / xy.MarkPlugin / xy.mark` | composes built-in marks only, one level deep; cannot reach the Figure, the trace list, or the column store; cannot add a GPU primitive |
| mark_plugin_shader | planned | `—` | — |
| custom_renderer | planned | `—` | — |

## Where renderers still disagree

Differences in the defaults that no property selects. They are listed because
an undocumented difference reads as a bug; a documented one is a contract.

| what | browser | svg | native png | visible when |
|---|---|---|---|---|
| Interior vertices of a wide polyline | the notch two overlapping segment quads leave | round (the writer names it explicitly) | round (the capsule distance field fills the vertex) | stroke-width above ~4px at a sharp angle |

For what is still alpha, see
[Limitations and Alpha Status](/docs/xy/api-reference/limitations-and-alpha-status/).
