# The hosted viz component contract

> The contract for a hosted Tableau viz extension: one Component function, two metadata lines, React 18 and d3 v7 as globals, and a locked-down sandbox.

Canonical: https://ext.tableauops.com/knowledge/component-contract
Updated: 2026-09-18
Author: Eric Summers


A hosted TableauOps viz extension is a single self-contained React component. You write the component to a strict contract, and the host compiles it, writes the `.trex`, and serves it. The contract is what lets the same source run in the builder preview, in the hosted preview harness, and inside Tableau's embedded browser without a build step. This is the shape the AI builder and the MCP server both require.

## The shape

The source is exactly one function literally named `Component`, a normal function declaration (not arrow-assigned), preceded by exactly two metadata comment lines and nothing else. No prose, no imports, no exports, no other top-level statements.

```jsx
// viz: {"type":"bar","variant":"horizontal"}
// fields: [{"id":"item","name":"Item","accepts":"dimension","max":1,"required":true,"icon":"level-of-detail","hint":"One mark per value, e.g. Sub-Category."},{"id":"value","name":"Value","accepts":"measure","max":1,"required":true,"icon":"metric","hint":"Bar length, e.g. SUM(Sales)."}]
function Component({ data, schema, theme, fields, config }) {
  const rows = Array.isArray(data) ? data : [];
  if (rows.length === 0) return <div style={{ padding: 20 }}>No data yet</div>;

  const labelField = fields?.item || schema?.find(f => f.type === 'string')?.name;
  const valueField = fields?.value || schema?.find(f => f.type === 'number')?.name;
  // ... build scales, return an <svg> that fills the container ...
  return <svg viewBox="0 0 800 500" style={{ width: '100%', height: '100%' }}>{/* marks */}</svg>;
}
```

## The two metadata lines

They are load-bearing and they are the only comments allowed in the block.

**Line 1, `// viz:`**, classifies the chart: `{"type":"bar","variant":"horizontal"}`, where `type` is one of the known chart types (bar, line, area, scatter, donut, heatmap, table, kpi, map, other) and `variant` is an optional one-word refinement. This is what lets the host classify the generation by chart type with no extra calls.

**Line 2, `// fields:`**, declares the Marks-card tiles. The host reads it and writes one `<encoding>` per entry into the `.trex`. Never omit it: a viz extension with no encodings loads with nowhere to drop fields and silently renders empty. Each entry carries `id`, `name`, `accepts` (dimension, measure, date, any), `max`, `required`, `icon`, and `hint`. See [viz extension encodings](/knowledge/viz-extension-encodings) for how each maps to the manifest and the two-to-four-tile cap.

## The props

The component receives a single object:

| Prop | What it is |
|---|---|
| `data` | Array of plain row objects, may be empty. |
| `schema` | `[{ name, type }]` describing the fields, may be undefined. |
| `theme` | Design tokens (palette, typography, density, radius), may be undefined. |
| `fields` | Your tile ids mapped to the field name the user dropped there. A `max` above 1 gives an array; an empty tile is undefined. |
| `config` | Configure-dialog values, when the extension exposes them. |

Always pick fields in this order: the tile the user filled, then the schema, then the first row's keys. Keep the schema fallback so the component still draws in a preview where there is no Marks card and `fields` is undefined.

## The sandbox: two globals, nothing else

The sandbox provides `React` (React 18) and `d3` (D3 v7) as globals. Do not import them, and do not import anything else: no npm packages, no framer-motion, no recharts. JSX is fine, it is compiled before your code runs. The sandbox also has no network (`fetch`, `XMLHttpRequest`, `WebSocket` are all unavailable), no parent window, no `localStorage` or cookies, and no direct DOM access (`document.querySelector`, `d3.select` on a real node). D3 is for the math (scales, layouts, path strings from `d3.arc` / `d3.line`); React owns the render. The root element must fill its container with a `viewBox` and `width`/`height` of `100%`, because the viz renders into whatever pane Tableau gives it.

## FAQ

### What must the function be called?

`Component`, exactly, and it must be a normal function declaration, not an arrow function assigned to a variable. The host looks for that name when it compiles.

### Can I import React, d3, or an npm package?

No. `React` (18) and `d3` (v7) are provided as globals; use them directly. No imports or exports are allowed at all, and no other library is available in the sandbox.

### What happens if I omit the // fields: line?

The extension ships with no encodings, so Tableau gives the user nowhere to drop a field and the viz renders empty. It is the single most common way a hosted viz extension fails. Always include it, with two to four tiles.

### Why does my code fail when it calls fetch or localStorage?

The sandbox blocks them by design: no network, no parent window, no storage, no direct DOM. A hosted viz gets all of its data through the `data` and `fields` props, and persists nothing itself.


---
Try it live: Build one in Studio — https://ext.tableauops.com/studio
