← 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
  • ImageUpload 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, ImageUpload, Heading } = Polyhedral.components;

function CharacterSheet() {
  return (
    <Sheet>
      <Section>
        <Row gap={16}>
          <ImageUpload 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 fantasy 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,
  ImageUpload, 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 FantasySheet() {
  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}>
          <ImageUpload 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(<FantasySheet />);

← Getting Started · SDK Reference · CLI Guide