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.
A minimal character overview with portrait, name fields, ability scores, and combat stats. Good starting point for any system.
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 />);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.
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 />);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.
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 />);