← Getting Started

Examples

Complete, copy-pasteable custom sheet examples. Each one is a working sheet you can use as a starting point — paste the code into a new template via MCP or the CLI.

Simple Stat Block

A minimal character overview with portrait, name fields, ability scores, and combat stats. Good starting point for any system.

Key Patterns

  • Layout with Sheet, Section, Row, and Grid
  • EditableText for persisted string fields
  • Portrait bound to field_data via the field prop
  • AbilityScore and StatBlock for read-only display
  • Reading live data with Polyhedral.sheet.getData()
const { Sheet, Section, Row, Grid, StatBlock, AbilityScore, EditableText, Portrait, Heading } = 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" placeholder="Enter race..." />
            <EditableText field="class" label="Class" placeholder="Enter 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={Polyhedral.sheet.getData().ac || 10} />
          <StatBlock label="Speed" value={Polyhedral.sheet.getData().speed || "30 ft"} />
          <StatBlock label="Prof" value={Polyhedral.sheet.getData().proficiency || "+2"} />
          <StatBlock label="Init" value={Polyhedral.sheet.getData().initiative || "+0"} />
        </Row>
      </Section>
    </Sheet>
  );
}

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

Combat Tracker

A combat-focused sheet with a health bar, editable HP, dice buttons for attacks and saves, and a quick-roll toolbar. Demonstrates interactive components and dice rolling.

Key Patterns

  • HealthBar with current/max values from field_data
  • EditableNumber for numeric inputs with min/max
  • DiceButton for one-click rolls that post to chat
  • Reading computed values from field_data
const { Sheet, Section, Row, Column, HealthBar, EditableNumber, EditableText, DiceButton, Heading, Text } = Polyhedral.components;

function CombatSheet() {
  const data = Polyhedral.sheet.getData();
  const currentHP = data.hp || 0;
  const maxHP = data.maxHp || 20;

  return (
    <Sheet>
      <Section>
        <Heading level={2}>Combat Tracker</Heading>
        <HealthBar current={currentHP} max={maxHP} />
        <Row gap={12}>
          <EditableNumber field="hp" label="Current HP" min={0} max={999} />
          <EditableNumber field="maxHp" label="Max HP" min={1} max={999} />
        </Row>
      </Section>

      <Section title="Actions">
        <Row gap={8}>
          <DiceButton notation="1d20+5">Attack</DiceButton>
          <DiceButton notation="2d6+3">Damage</DiceButton>
          <DiceButton notation="1d20+2">Save</DiceButton>
        </Row>
      </Section>

      <Section title="Quick Rolls">
        <Row gap={8}>
          <DiceButton notation="1d20">d20</DiceButton>
          <DiceButton notation="1d12">d12</DiceButton>
          <DiceButton notation="1d10">d10</DiceButton>
          <DiceButton notation="1d8">d8</DiceButton>
          <DiceButton notation="1d6">d6</DiceButton>
          <DiceButton notation="1d4">d4</DiceButton>
        </Row>
      </Section>

      <Section title="Notes">
        <EditableText field="notes" placeholder="Combat notes..." />
      </Section>
    </Sheet>
  );
}

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

Full Character Sheet

A complete D&D 5e character sheet with identity, ability scores, saving throws, skills, hit points, death saves, attacks, spellcasting, equipment, and notes. Demonstrates calculated fields, conditional logic, and most SDK components.

Key Patterns

  • Computed values (proficiency bonus, ability modifiers, spell save DC)
  • Checkbox for proficiency toggles with calculated bonuses
  • Select dropdown for alignment and spellcasting ability
  • ResourceTrack for spell slots, death saves, and hit dice
  • DiceButton with dynamic notation based on field_data
  • Polyhedral.sheet.setData() for programmatic updates
const {
  Sheet, Section, Row, Column, Grid,
  Heading, Text, Label, Badge,
  Portrait, EditableText, EditableNumber, Checkbox, Select,
  AbilityScore, StatBlock, HealthBar, ResourceTrack, DiceButton
} = Polyhedral.components;

const ABILITIES = ['str','dex','con','int','wis','cha'];
const ABILITY_LABELS = { str:'Strength', dex:'Dexterity', con:'Constitution', int:'Intelligence', wis:'Wisdom', cha:'Charisma' };

const SKILLS = [
  { name:'Acrobatics',       ability:'dex' },
  { name:'Animal Handling',  ability:'wis' },
  { name:'Arcana',           ability:'int' },
  { name:'Athletics',        ability:'str' },
  { name:'Deception',        ability:'cha' },
  { name:'History',          ability:'int' },
  { name:'Insight',          ability:'wis' },
  { name:'Intimidation',     ability:'cha' },
  { name:'Investigation',    ability:'int' },
  { name:'Medicine',         ability:'wis' },
  { name:'Nature',           ability:'int' },
  { name:'Perception',       ability:'wis' },
  { name:'Performance',      ability:'cha' },
  { name:'Persuasion',       ability:'cha' },
  { name:'Religion',         ability:'int' },
  { name:'Sleight of Hand',  ability:'dex' },
  { name:'Stealth',          ability:'dex' },
  { name:'Survival',         ability:'wis' },
];

const ALIGNMENTS = [
  { value:'lg', label:'Lawful Good' },
  { value:'ng', label:'Neutral Good' },
  { value:'cg', label:'Chaotic Good' },
  { value:'ln', label:'Lawful Neutral' },
  { value:'tn', label:'True Neutral' },
  { value:'cn', label:'Chaotic Neutral' },
  { value:'le', label:'Lawful Evil' },
  { value:'ne', label:'Neutral Evil' },
  { value:'ce', label:'Chaotic Evil' },
];

function mod(score) {
  return Math.floor((score - 10) / 2);
}

function signed(n) {
  return n >= 0 ? '+' + n : String(n);
}

function Dnd5eSheet() {
  const data = Polyhedral.sheet.getData();

  const level = data.level || 1;
  const profBonus = Math.ceil(level / 4) + 1;

  const scores = {};
  const mods = {};
  ABILITIES.forEach(a => {
    scores[a] = data[a] || 10;
    mods[a] = mod(scores[a]);
  });

  const ac = data.ac || (10 + mods.dex);
  const hp = data.hp || 0;
  const maxHp = data.maxHp || 10;
  const tempHp = data.tempHp || 0;
  const hitDiceLeft = data.hitDiceLeft || level;
  const deathSuccesses = data.deathSuccesses || 0;
  const deathFailures = data.deathFailures || 0;

  function skillMod(skill) {
    const fieldKey = 'prof_' + skill.name.toLowerCase().replace(/\s/g, '_');
    const base = mods[skill.ability];
    return data[fieldKey] ? base + profBonus : base;
  }

  return (
    <Sheet>
      {/* Identity */}
      <Section>
        <Row gap={16}>
          <Portrait field="portrait" size="lg" />
          <Column gap={4} style={{ flex: 1 }}>
            <EditableText field="name" label="Character Name" placeholder="Name..." />
            <Row gap={8}>
              <EditableText field="race" label="Race" placeholder="Race..." />
              <EditableText field="class" label="Class" placeholder="Class..." />
            </Row>
            <Row gap={8}>
              <EditableNumber field="level" label="Level" min={1} max={20} />
              <EditableText field="background" label="Background" placeholder="Background..." />
              <Select field="alignment" label="Alignment" options={ALIGNMENTS} />
            </Row>
          </Column>
        </Row>
      </Section>

      {/* Quick Reference */}
      <Section>
        <Row gap={12}>
          <StatBlock label="Prof Bonus" value={signed(profBonus)} />
          <StatBlock label="AC" value={ac} />
          <StatBlock label="Initiative" value={signed(mods.dex)} />
          <StatBlock label="Speed" value={data.speed || "30 ft"} />
          <StatBlock label="Passive Perception" value={10 + mods.wis + (data.prof_perception ? profBonus : 0)} />
        </Row>
      </Section>

      {/* Ability Scores */}
      <Section title="Ability Scores">
        <Grid columns={6} gap={8}>
          {ABILITIES.map(a => (
            <AbilityScore key={a} name={a.toUpperCase()} score={scores[a]} />
          ))}
        </Grid>
        <Row gap={8}>
          {ABILITIES.map(a => (
            <EditableNumber key={a} field={a} label={a.toUpperCase()} min={1} max={30} />
          ))}
        </Row>
      </Section>

      {/* Saving Throws */}
      <Section title="Saving Throws">
        <Grid columns={3} gap={4}>
          {ABILITIES.map(a => {
            const profField = 'save_' + a;
            const bonus = data[profField] ? mods[a] + profBonus : mods[a];
            return (
              <Row key={a} gap={4}>
                <Checkbox field={profField} label={ABILITY_LABELS[a] + ' ' + signed(bonus)} />
                <DiceButton notation={'1d20' + signed(bonus)} mode="chat">
                  Roll
                </DiceButton>
              </Row>
            );
          })}
        </Grid>
      </Section>

      {/* Skills */}
      <Section title="Skills">
        <Grid columns={2} gap={4}>
          {SKILLS.map(skill => {
            const fieldKey = 'prof_' + skill.name.toLowerCase().replace(/\s/g, '_');
            const bonus = skillMod(skill);
            return (
              <Row key={skill.name} gap={4}>
                <Checkbox field={fieldKey} label={skill.name + ' (' + skill.ability.toUpperCase() + ') ' + signed(bonus)} />
                <DiceButton notation={'1d20' + signed(bonus)} mode="chat">
                  Roll
                </DiceButton>
              </Row>
            );
          })}
        </Grid>
      </Section>

      {/* Hit Points */}
      <Section title="Hit Points">
        <HealthBar current={hp} max={maxHp} />
        <Row gap={8}>
          <EditableNumber field="hp" label="Current HP" min={0} max={999} />
          <EditableNumber field="maxHp" label="Max HP" min={1} max={999} />
          <EditableNumber field="tempHp" label="Temp HP" min={0} max={999} />
        </Row>
      </Section>

      {/* Death Saves & Hit Dice */}
      <Section title="Death Saves & Hit Dice">
        <Row gap={16}>
          <Column gap={4}>
            <ResourceTrack
              label="Successes"
              current={deathSuccesses}
              max={3}
              onToggle={() => {
                const next = deathSuccesses >= 3 ? 0 : deathSuccesses + 1;
                Polyhedral.sheet.setData({ deathSuccesses: next });
              }}
            />
            <ResourceTrack
              label="Failures"
              current={deathFailures}
              max={3}
              onToggle={() => {
                const next = deathFailures >= 3 ? 0 : deathFailures + 1;
                Polyhedral.sheet.setData({ deathFailures: next });
              }}
            />
          </Column>
          <Column gap={4}>
            <ResourceTrack
              label="Hit Dice"
              current={hitDiceLeft}
              max={level}
              onToggle={() => {
                const next = hitDiceLeft > 0 ? hitDiceLeft - 1 : level;
                Polyhedral.sheet.setData({ hitDiceLeft: next });
              }}
            />
            <EditableText field="hitDieType" label="Hit Die" placeholder="e.g. d10" />
          </Column>
        </Row>
      </Section>

      {/* Attacks */}
      <Section title="Attacks">
        <Row gap={8}>
          <Column gap={4} style={{ flex: 1 }}>
            <EditableText field="atk1Name" label="Attack 1" placeholder="Weapon..." />
            <Row gap={4}>
              <DiceButton notation={'1d20' + signed(mods.str + profBonus)}>Hit</DiceButton>
              <EditableText field="atk1Dmg" label="Damage" placeholder="1d8+3" />
            </Row>
          </Column>
          <Column gap={4} style={{ flex: 1 }}>
            <EditableText field="atk2Name" label="Attack 2" placeholder="Weapon..." />
            <Row gap={4}>
              <DiceButton notation={'1d20' + signed(mods.dex + profBonus)}>Hit</DiceButton>
              <EditableText field="atk2Dmg" label="Damage" placeholder="1d6+2" />
            </Row>
          </Column>
        </Row>
      </Section>

      {/* Spellcasting */}
      <Section title="Spellcasting">
        <Row gap={12}>
          <Select field="spellAbility" label="Spellcasting Ability" options={[
            { value:'int', label:'Intelligence' },
            { value:'wis', label:'Wisdom' },
            { value:'cha', label:'Charisma' },
          ]} />
          <StatBlock label="Spell Save DC" value={8 + profBonus + (mods[data.spellAbility] || 0)} />
          <StatBlock label="Spell Attack" value={signed(profBonus + (mods[data.spellAbility] || 0))} />
        </Row>
        <Column gap={4}>
          {[1,2,3,4,5].map(lvl => (
            <ResourceTrack
              key={lvl}
              label={'Level ' + lvl + ' Slots'}
              current={data['slots' + lvl] || 0}
              max={data['slotsMax' + lvl] || 0}
              onToggle={() => {
                const cur = data['slots' + lvl] || 0;
                const mx = data['slotsMax' + lvl] || 0;
                const next = cur > 0 ? cur - 1 : mx;
                Polyhedral.sheet.setData({ ['slots' + lvl]: next });
              }}
            />
          ))}
          <Row gap={8}>
            {[1,2,3,4,5].map(lvl => (
              <EditableNumber key={lvl} field={'slotsMax' + lvl} label={'Lvl ' + lvl + ' Max'} min={0} max={9} />
            ))}
          </Row>
        </Column>
      </Section>

      {/* Features, Equipment, Notes */}
      <Section title="Features & Traits">
        <EditableText field="features" placeholder="Class features, racial traits, feats..." />
      </Section>

      <Section title="Equipment">
        <Row gap={8}>
          <EditableNumber field="cp" label="CP" min={0} />
          <EditableNumber field="sp" label="SP" min={0} />
          <EditableNumber field="ep" label="EP" min={0} />
          <EditableNumber field="gp" label="GP" min={0} />
          <EditableNumber field="pp" label="PP" min={0} />
        </Row>
        <EditableText field="equipment" placeholder="Weapons, armor, gear..." />
      </Section>

      <Section title="Notes">
        <EditableText field="notes" placeholder="Backstory, allies, quest notes..." />
      </Section>

      {/* Misc */}
      <Section>
        <Row gap={8}>
          <Checkbox field="inspiration" label="Inspiration" />
          <EditableNumber field="xp" label="Experience Points" min={0} />
        </Row>
      </Section>
    </Sheet>
  );
}

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

← Getting Started · SDK Reference · CLI Guide