Polyhedral SDK

Custom sheets let you design exactly the character sheet your game needs — stat blocks, dice rollers, trackers, notes, and more. They run inside a sandboxed iframe with a built-in React component library, and every field is automatically persisted to the database.

Build Types

The Polyhedral CLI handles auth, validation, pushing, and live preview. Choose a build type when you scaffold a project:

In-App (JSX)

Write JSX using the built-in component library. The platform compiles and renders it for you. Validation runs offline via Babel before each push.

External (HTML)

Bring your own HTML — plain HTML/JS, or the output of a bundler like Vite. Use any framework: React, Svelte, Vue, or vanilla JS.

Prerequisites

  1. A paid plan.
  2. The Polyhedral CLI installed.

Your First Sheet

Let's build a character sheet step by step. Each step adds a new capability so you can see how the pieces fit together.

1. The Boilerplate

Every sheet starts with Sheet (the root wrapper that provides padding and theme) and Section (a card-like grouping). Destructure them from Polyhedral.components and render to the root element.

const { Sheet, Section } = Polyhedral.components;

function CharacterSheet() {
  return (
    <Sheet>
      <Section title="Hello">
        <p>My first sheet!</p>
      </Section>
    </Sheet>
  );
}

// Shorthand: Polyhedral.render(<CharacterSheet />)
ReactDOM.createRoot(document.getElementById('root')).render(<CharacterSheet />);

2. Add Editable Fields

Interactive components like EditableText bind to the sheet's field_data via the field prop. When a player types a name, it's automatically saved to the database — no save button needed.

const { Sheet, Section, Row, EditableText, Portrait } = Polyhedral.components;

function CharacterSheet() {
  return (
    <Sheet>
      <Section>
        <Row gap={16}>
          <Portrait field="portrait" size="lg" />
          <div style={{ flex: 1 }}>
            <EditableText field="name" label="Character Name"
              placeholder="Enter name..." />
            <EditableText field="race" label="Race" />
            <EditableText field="class" label="Class" />
          </div>
        </Row>
      </Section>
    </Sheet>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<CharacterSheet />);

3. Display Data with Computed Values

Use Polyhedral.sheet.getData() to read the current field data. Display components like AbilityScore and StatBlock render values in styled containers — they're read-only display, not editable inputs.

const { Sheet, Section, Row, Grid, EditableText, Portrait,
        AbilityScore, StatBlock } = Polyhedral.components;

function CharacterSheet() {
  return (
    <Sheet>
      <Section>
        <Row gap={16}>
          <Portrait field="portrait" size="lg" />
          <div style={{ flex: 1 }}>
            <EditableText field="name" label="Character Name"
              placeholder="Enter name..." />
            <EditableText field="race" label="Race" />
            <EditableText field="class" label="Class" />
          </div>
        </Row>
      </Section>

      <Section title="Ability Scores">
        <Grid columns={6} gap={8}>
          <AbilityScore name="STR" score={Polyhedral.sheet.getData().str || 10} />
          <AbilityScore name="DEX" score={Polyhedral.sheet.getData().dex || 10} />
          <AbilityScore name="CON" score={Polyhedral.sheet.getData().con || 10} />
          <AbilityScore name="INT" score={Polyhedral.sheet.getData().int || 10} />
          <AbilityScore name="WIS" score={Polyhedral.sheet.getData().wis || 10} />
          <AbilityScore name="CHA" score={Polyhedral.sheet.getData().cha || 10} />
        </Grid>
      </Section>

      <Section title="Combat Stats">
        <Row gap={12}>
          <StatBlock label="AC" value={10} />
          <StatBlock label="Speed" value="30 ft" />
          <StatBlock label="Prof" value="+2" />
        </Row>
      </Section>
    </Sheet>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<CharacterSheet />);

4. Add Dice Rolling

The DiceButton component gives players one-click rolls that animate 3D dice and post results to the game chat. For custom logic (like checking for crits), call the dice API directly.

const { DiceButton } = Polyhedral.components;

// One-click dice button — animates 3D dice and posts to chat
<DiceButton notation="1d20" label="Attack Roll">Roll Attack</DiceButton>

// Or call the API directly for custom logic
const handleRoll = async () => {
  const result = await Polyhedral.dice.rollToChat('1d20+5', 'Attack Roll');
  if (result.total >= 20) {
    // Critical hit!
  }
};

Core Concepts

Templates vs Instances

A template is the sheet design — the JSX source, compiled code, and optional field schema. Templates are owned by you and reusable across games.

An instance is a deployed copy in a specific game. It holds the character's actual data (HP, stats, notes). The template code is copied into the instance at creation time, so instances are independent — updating the template doesn't change existing instances.

Lifecycle: create template → preview and iterate → deploy as instance to a game → players fill in their data.

Field Data Flow

Every instance has a field_data JSON object that stores the character's persisted data. Interactive components (EditableText, Checkbox, etc.) read and write to it automatically via their field prop. You can also read it directly with Polyhedral.sheet.getData() and write with Polyhedral.sheet.setData().

// Read all field data
const data = Polyhedral.sheet.getData();

// Write a single field (merges, doesn't replace)
Polyhedral.sheet.setData({ hp: 25 });

// Listen for changes from other components
Polyhedral.sheet.onDataChange((data) => {
  console.log('Field data changed:', data);
});

// Or use the React hook for cleaner code
const hp = Polyhedral.useFieldData('hp', 0);

State vs Field Data

field_data is persisted to the database — it survives page reloads and is shared across sessions. Polyhedral.state is ephemeral UI state that resets on reload. Use it for things like which tab is active, whether a section is collapsed, or temporary UI flags.

Rule of thumb: if a player would expect the value to be there next time they open the sheet, use field_data. If it's just UI chrome, use state.

// Ephemeral UI state — resets on reload
Polyhedral.state.set('activeTab', 'skills');
const tab = Polyhedral.state.get('activeTab');

// Example: tab switching with state
function TabbedSheet() {
  const [tab, setTab] = React.useState(
    Polyhedral.state.get('activeTab') || 'stats'
  );

  const switchTab = (name) => {
    setTab(name);
    Polyhedral.state.set('activeTab', name);
  };

  return (
    <Sheet>
      <Row gap={8}>
        <button onClick={() => switchTab('stats')}>Stats</button>
        <button onClick={() => switchTab('skills')}>Skills</button>
      </Row>
      {tab === 'stats' && <Section title="Stats">...</Section>}
      {tab === 'skills' && <Section title="Skills">...</Section>}
    </Sheet>
  );
}

Game Events

Sheets can subscribe to live game events — dice rolls, encounter changes, map activations, and more. When something happens in the game, every subscribed sheet is notified in real time via Polyhedral.events.

// Highlight the sheet when someone rolls dice
Polyhedral.events.on('dice.rolled', (event) => {
  console.log(event.userId, 'rolled:', event.payload);
});

// React to encounter changes
Polyhedral.events.on('encounter.created', (event) => {
  // Update your initiative tracker, combat dashboard, etc.
});

See the Events API reference for the full list of event types and the event shape.

The Sandbox

Sheets run inside a sandboxed iframe for security. No fetch(), no localStorage, no parent window access. All data flows through the SDK APIs. External packages can be imported via esm.sh. See the full constraints list in the reference.

Quick Start with AI

With the CLI installed, your AI assistant can build sheets for you end-to-end:

  1. It reads the SDK docs via polyhedral docs.
  2. It writes JSX using the component library and validates it with polyhedral validate.
  3. It pushes the sheet with polyhedral push and gives you a preview URL.
  4. You review, iterate, and when ready deploy it to a game as an instance.

Try asking something like:

"Build me a Blades in the Dark character sheet with
stress track, trauma checkboxes, and action ratings"

Examples

Complete, copy-pasteable examples to learn from and build on.

View all examples →

Next Steps