Complete reference for the Polyhedral SDK — components, APIs, and theme. Looking for working code? Browse the examples.
Sheets run inside a sandboxed iframe with React 19 and ReactDOM available as globals. The Polyhedral global provides access to data, state, dice rolling, theme variables, and the component library.
Polyhedral.sheet // getData(), setData(), onDataChange() Polyhedral.state // get(), set() — ephemeral UI state Polyhedral.dice // roll(), roll3d(), rollToChat() Polyhedral.storage // upload() — image uploads to Supabase Storage Polyhedral.events // on(), off(), once() — live game event subscriptions Polyhedral.components // 19 built-in React components Polyhedral.theme // current color tokens + onChange() Polyhedral.meta // instanceId, gameId, characterName, userId, isEditor Polyhedral.useFieldData // React hook for field data with live updates
19 pre-built components that match the Polyhedral design system. Destructure them from Polyhedral.components.
<Sheet>Root wrapper with padding and theme.
{ children }<Section>Card-like grouping with optional heading.
{ title?, children }<Row>Horizontal flex row.
{ gap?, children }<Column>Vertical flex column.
{ gap?, children }<Grid>CSS grid layout.
{ columns, gap?, children }<StatBlock>Label + value pair in a bordered box.
{ label, value }<AbilityScore>D&D-style ability score with modifier.
{ name, score }<HealthBar>Colored HP bar.
{ current, max, label? }<ResourceTrack>Pip-based tracker (spell slots, etc).
{ label, current, max, onToggle? }<Badge>Status pill. Variants: default, success, warning, danger.
{ children, variant? }<Portrait>Character portrait from field data URL.
{ field, size? }<EditableText>Text input bound to field data.
{ field, placeholder?, label? }<EditableNumber>Number input bound to field data.
{ field, min?, max?, label? }<Checkbox>Boolean toggle bound to field data.
{ field, label }<Select>Dropdown select bound to field data.
{ field, label?, options }<DiceButton>Rolls dice on click. Default mode 'chat' animates 3D dice and posts to chat. '3d' animates without chat. 'silent' is instant math only.
{ notation, label?, mode?: 'silent'|'3d'|'chat', children? }<Heading>h1, h2, or h3.
{ level?, children }<Text>Body text. Sizes: small, default, large.
{ size?, muted?, children }<Label>Small uppercase label.
{ children }Three tiers of dice rolling, from silent math to full 3D animation with chat integration.
Polyhedral.dice.roll(notation)Silent math — instant result, no 3D animation, no chat message. Returns {total, rolls}.
Polyhedral.dice.roll3d(notation, label?)3D animated roll — shows dice animation on screen but does not post to chat. Returns {total, rolls}.
Polyhedral.dice.rollToChat(notation, label?)3D animated roll + posts the result to chat. The label appears as a heading above the roll result. Returns {total, rolls}.
// Silent math — instant, no visuals
const result = await Polyhedral.dice.roll('2d6+3');
// 3D animated — dice fly across screen
const result = await Polyhedral.dice.roll3d('1d20', 'Attack Roll');
// 3D animated + posts to chat
const result = await Polyhedral.dice.rollToChat('1d20', 'Attack Roll');
// All three return { total: number, rolls: number[] }Upload images (portraits, icons, etc.) from within the sandbox. Files are stored in Supabase Storage and a public URL is returned.
Polyhedral.storage.upload(file)Accepts a File instance (max 5 MB, images only). Returns a Promise<string> resolving to the public URL.
// Let the user pick a portrait image
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = async () => {
const file = input.files[0];
const url = await Polyhedral.storage.upload(file);
Polyhedral.sheet.setData({ portrait: url });
};
input.click();Subscribe to live game events from within your sheet. When someone rolls dice, activates a map, or changes an encounter, your sheet is notified in real time — no polling, no server required.
events.on(eventType, callback) // subscribe, returns unsubscribe fn events.off(eventType, callback) // remove a callback events.once(eventType, callback) // subscribe for one event, then auto-remove
Callbacks receive a GameEvent with eventType, userId, payload, and timestamp.
// Subscribe to dice rolls
const unsub = Polyhedral.events.on('dice.rolled', (event) => {
console.log(event.userId, 'rolled', event.payload);
});
// Listen for a single encounter change
Polyhedral.events.once('encounter.created', (event) => {
// Update UI for new encounter
});
// Clean up when done
unsub();dice.rolled // notation, total, individual rolls encounter.created // encounter data encounter.updated // updated encounter data encounter.deleted // deleted encounter id map.activated // map shown to players map.deactivated // map hidden from players table.rolled // rollable table result
interface GameEvent {
eventType: string; // e.g. 'dice.rolled'
userId: string; // who triggered the event
payload: object; // event-specific data
timestamp: string; // ISO 8601
}A React hook that reads a single field from field_data and re-renders the component when it changes. Cleaner than calling getData() + onDataChange() manually.
const hp = Polyhedral.useFieldData('hp', 0);
const name = Polyhedral.useFieldData('name', 'Unnamed');
// Equivalent to:
// const [hp, setHp] = useState(Polyhedral.sheet.getData().hp ?? 0);
// useEffect(() => Polyhedral.sheet.onDataChange(d => setHp(d.hp ?? 0)), []);CSS custom properties are set on :root as HSL values. Use them with hsl(var(--name)) for consistent styling. Theme values update live when the user toggles dark/light mode — the SDK pushes new values to the iframe automatically and updates the CSS variables on :root.
--background --foreground --raised --card / --card-foreground --primary --primary-foreground --muted --muted-foreground --hover --destructive --destructive-foreground --warning --warning-foreground --border --ring --radius --radius-inner Legacy aliases (still available): --secondary / --secondary-foreground → mapped to muted --accent / --accent-foreground → mapped to muted --input → mapped to border
If you use inline styles or JavaScript-driven colors, use Polyhedral.theme to read current values and theme.onChange() to react to mode switches:
// Read current theme values
const bg = Polyhedral.theme.background; // HSL string
// React to dark/light mode changes
const unsub = Polyhedral.theme.onChange((theme) => {
document.body.style.color = `hsl(${theme.foreground})`;
});
// Clean up when done
unsub();A template's field_schema declares what data fields the sheet expects. It's optional — interactive components work without it — but defining one enables default values when creating instances and better tooling support.
Each field has a key (matching the field prop on components), a type, and optional label and default.
field_schema: [
{ key: "name", type: "text", label: "Character Name", default: "" },
{ key: "hp", type: "number", label: "Hit Points", default: 10 },
{ key: "level", type: "number", label: "Level", default: 1 },
{ key: "inspired", type: "boolean", label: "Inspired", default: false },
{ key: "portrait", type: "image", label: "Portrait" }
]text — String values (EditableText, Select)number — Numeric values (EditableNumber, AbilityScore)boolean — True/false toggles (Checkbox)image — URL string from storage upload (Portrait)field_data directly — they don't need the schema to function.name_field is set on the template, the instance name auto-syncs when that field changes.Import npm packages via esm.sh. The sandbox CSP allows scripts from esm.sh and cdn.jsdelivr.net.
import confetti from 'https://esm.sh/canvas-confetti@1';
// Fire confetti on a nat 20 (3D animated + posts to chat)
const result = await Polyhedral.dice.rollToChat('1d20', 'Attack Roll');
if (result.total === 20) {
confetti({ particleCount: 100, spread: 70 });
}Sheets run in a sandboxed iframe with restricted permissions for security:
fetch() — use the SDK for datalocalStorage / sessionStoragewindow.parent access — the SDK handles postMessage communication<style> tags — no CSS file importssrcDoc, so file paths like /images/logo.png have no server to resolve against. Inline images and fonts as base64 data URIs, or use Polyhedral.storage.upload() to get hosted URLs@font-face { src: url(data:font/ttf;base64,...) }The Polyhedral CLI provides all the tools you need to build, validate, and deploy custom sheets.
polyhedral docsPrint the full SDK reference to stdout — types, components, examples, and theme variables.
polyhedral listList your saved templates.
polyhedral pull [id]Download a template to a local file.
polyhedral pushValidate, upload, and publish a new version.
polyhedral validate [file]Validate JSX syntax locally. Reports line and column on error.
polyhedral devWatch file, auto-push on save, and open live preview.
polyhedral init [dir]Scaffold a new template project.
External builds let you use any framework (Svelte, Vue, plain HTML) instead of the in-app JSX editor. You provide a complete HTML document and the SDK is auto-injected into the <head> at render time.
The Polyhedral global is available just like in JSX builds, but since you're writing vanilla JS (or compiling to it), you'll use the imperative API instead of React components:
// Read a field value
const name = Polyhedral.getField('name');
// Write a field value (merges into field_data)
Polyhedral.setField('hp', 25);
// Listen for field changes
Polyhedral.onFieldChange((data) => {
document.getElementById('hp').textContent = data.hp;
});
// Dice rolling works the same way
const result = await Polyhedral.dice.rollToChat('1d20', 'Attack');
// Theme tokens are available as CSS variables
// color: hsl(var(--foreground));Use the Polyhedral CLI to set up an external build project. Run polyhedral init and select "external" as the build type, then use polyhedral dev for live preview and polyhedral push to deploy.