Skip to content
fvtt-sheets logo

Foundry VTT Sheets

fvtt-sheets

This skill should be used when creating or extending ActorSheet/ItemSheet classes, implementing getData or _prepareContext, binding events with activateListeners, handling drag/drop, or migrating from ApplicationV1 to ApplicationV2. Covers both legacy V1 and modern V2 patterns.

SKILL.md

Full skill instructions

Foundry VTT Sheets

Domain: Foundry VTT Module/​System Development Status: Production-Ready Last Updated: 2026-01-04

Overview

Document sheets (ActorSheet, ItemSheet) are the primary UI for interacting with game entities. Foundry supports two patterns: legacy ApplicationV1 (until V16) and modern ApplicationV2 (V12+).

When to Use This Skill

  • Creating custom character or item sheets
  • Extending existing sheet classes
  • Adding interactivity (rolls, item management)
  • Implementing drag/​drop functionality
  • Migrating V1 sheets to V2

V1 vs V2 Quick Comparison

AspectV1 (Legacy)V2 (Modern)
Configstatic get defaultOptions()static DEFAULT_OPTIONS
DatagetData()async _prepareContext()
EventsactivateListeners(html)static actions + _onRender()
TemplatesSingle templateMulti-part PARTS system
Re-renderFull sheetPartial by part
SupportUntil V16Current standard

ApplicationV1 Sheets

Basic Structure

export class MyActorSheet extends ActorSheet {
  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      classes: ["my-system", "sheet", "actor"],
      template: "systems/​my-system/​templates/​actor-sheet.hbs",
      width: 600,
      height: 600,
      tabs: [{
        navSelector: ".sheet-tabs",
        contentSelector: ".sheet-body",
        initial: "description"
      }],
      dragDrop: [{
        dragSelector: ".item-list .item",
        dropSelector: null
      }]
    });
  }

  // Dynamic template based on actor type
  get template() {
    return `systems/​my-system/​templates/​actor-${this.actor.type}-sheet.hbs`;
  }
}

getData() - Preparing Template Context

getData() {
  const context = super.getData();
  const actorData = this.actor.toObject(false);

  // Add data to context
  context.system = actorData.system;
  context.flags = actorData.flags;
  context.items = actorData.items;

  // Organize items by type
  context.weapons = context.items.filter(i => i.type === "weapon");
  context.spells = context.items.filter(i => i.type === "spell");

  // Enrich HTML (sync in V1)
  context.enrichedBio = TextEditor.enrichHTML(
    this.actor.system.biography,
    { secrets: this.actor.isOwner, async: false }
  );

  return context;
}

Key Points:

  • Context has NO automatic relation to document data
  • Everything template needs MUST be explicitly added
  • {{system.hp.value}} reads from context
  • name="system.hp.value" writes to document

activateListeners() - Event Binding

activateListeners(html) {
  // ALWAYS call super first
  super.activateListeners(html);

  // Skip if not editable
  if (!this.isEditable) return;

  // Roll handlers
  html.on("click", ".rollable", this._onRoll.bind(this));

  // Item management
  html.on("click", ".item-create", this._onItemCreate.bind(this));
  html.on("click", ".item-edit", this._onItemEdit.bind(this));
  html.on("click", ".item-delete", this._onItemDelete.bind(this));
}

async _onRoll(event) {
  event.preventDefault();
  const element = event.currentTarget;
  const { rollType, formula, label } = element.dataset;

  const roll = new Roll(formula, this.actor.getRollData());
  await roll.evaluate();
  roll.toMessage({
    speaker: ChatMessage.getSpeaker({ actor: this.actor }),
    flavor: label
  });
}

async _onItemCreate(event) {
  event.preventDefault();
  const type = event.currentTarget.dataset.type;
  await this.actor.createEmbeddedDocuments("Item", [{
    name: `New ${type.capitalize()}`,
    type: type
  }]);
}

async _onItemDelete(event) {
  event.preventDefault();
  const li = $(event.currentTarget).closest(".item");
  const item = this.actor.items.get(li.data("itemId"));
  await item.delete();
  li.slideUp(200, () => this.render(false));
}

Drag & Drop (V1)

// Automatic via defaultOptions
static get defaultOptions() {
  return foundry.utils.mergeObject(super.defaultOptions, {
    dragDrop: [{
      dragSelector: ".item-list .item",
      dropSelector: null
    }]
  });
}

// Override handlers as needed
_onDragStart(event) {
  const li = event.currentTarget;
  const item = this.actor.items.get(li.dataset.itemId);
  event.dataTransfer.setData("text/​plain", JSON.stringify(item.toDragData()));
}

async _onDrop(event) {
  const data = TextEditor.getDragEventData(event);

  if (data.type === "Item") {
    return this._onDropItem(event, data);
  }
}

async _onDropItem(event, data) {
  if (!this.actor.isOwner) return false;
  const item = await Item.implementation.fromDropData(data);

  // Prevent dropping on self
  if (this.actor.uuid === item.parent?.uuid) return;

  return this.actor.createEmbeddedDocuments("Item", [item.toObject()]);
}

Tab Navigation (V1)

<!-- Template structure -->
<nav class="sheet-tabs">
  <a class="item" data-tab="description">Description</​a>
  <a class="item" data-tab="items">Items</​a>
</​nav>

<section class="sheet-body">
  <div class="tab" data-group="primary" data-tab="description">
    <!-- Description content -->
  </​div>
  <div class="tab" data-group="primary" data-tab="items">
    <!-- Items content -->
  </​div>
</​section>

ApplicationV2 Sheets

Basic Structure

class MyActorSheet extends foundry.applications.api.HandlebarsApplicationMixin(
  foundry.applications.sheets.ActorSheetV2
) {
  static DEFAULT_OPTIONS = {
    classes: ["my-system", "sheet", "actor"],
    tag: "form",
    window: {
      resizable: true
    },
    position: {
      width: 600,
      height: 600
    },
    actions: {
      rollSkill: this.#onRollSkill,
      createItem: this.#onCreateItem,
      deleteItem: this.#onDeleteItem
    }
  }

  static PARTS = {
    header: {
      template: "systems/​my-system/​templates/​actor/​header.hbs"
    },
    tabs: {
      template: "templates/​generic/​tab-navigation.hbs"
    },
    description: {
      template: "systems/​my-system/​templates/​actor/​description.hbs",
      scrollable: [""]
    },
    items: {
      template: "systems/​my-system/​templates/​actor/​items.hbs",
      scrollable: [""]
    }
  }

  static TABS = {
    primary: {
      tabs: [
        { id: "description" },
        { id: "items" }
      ],
      labelPrefix: "MYSYS.TAB",
      initial: "description"
    }
  }
}

_prepareContext() - Async Data Preparation

async _prepareContext(options) {
  const context = await super._prepareContext(options);

  // Add tabs
  context.tabs = this._prepareTabs(this.tabGroups.primary);

  // Add system data
  context.system = this.document.system;

  // Organize items
  context.weapons = this.document.items.filter(i => i.type === "weapon");
  context.spells = this.document.items.filter(i => i.type === "spell");

  // Enrich HTML (MUST be async in V2)
  context.enrichedBio = await TextEditor.enrichHTML(
    this.document.system.biography,
    { async: true, relativeTo: this.document }
  );

  return context;
}

async _preparePartContext(partId, context) {
  switch (partId) {
    case "description":
    case "items":
      context.tab = context.tabs[partId];
      break;
  }
  return context;
}

Static Actions (V2 Event Handling)

static DEFAULT_OPTIONS = {
  actions: {
    rollSkill: this.#onRollSkill,
    createItem: this.#onCreateItem,
    deleteItem: this.#onDeleteItem
  }
}

// Action handlers MUST be static with # prefix
static #onRollSkill(event, target) {
  // 'this' is the application instance
  // 'target' is the clicked element
  const skillId = target.dataset.skillId;
  const skill = this.document.system.skills[skillId];

  const roll = new Roll("1d20 + @mod", { mod: skill.value });
  roll.evaluate().then(r => {
    r.toMessage({
      speaker: ChatMessage.getSpeaker({ actor: this.document }),
      flavor: `${skill.label} Check`
    });
  });
}

static async #onCreateItem(event, target) {
  const type = target.dataset.type;
  await this.document.createEmbeddedDocuments("Item", [{
    name: `New ${type.capitalize()}`,
    type: type
  }]);
}

static async #onDeleteItem(event, target) {
  const itemId = target.closest("[data-item-id]").dataset.itemId;
  const item = this.document.items.get(itemId);
  await item.delete();
}

Template usage:

<button type="button" data-action="rollSkill" data-skill-id="athletics">
  Roll Athletics
</​button>

Tab Navigation (V2)

Four required elements:

1. Static PARTS with tab templates 2. Static TABS configuration 3. Prepare tabs in _prepareContext 4. Set tab in _preparePartContext

<!-- Tab content template - MUST include data-group, data-tab, and {{tab.cssClass}} -->
<div class="tab-content {{tab.cssClass}}" data-group="primary" data-tab="description">
  <!-- Content -->
</​div>

Drag & Drop (V2)

ActorSheetV2 provides automatic drag/​drop for items. Just use:

<li class="item draggable" data-item-id="{{item._id}}">
  <!-- Item content -->
</​li>

For base ApplicationV2, manual setup required:

#dragDrop;

constructor(options = {}) {
  super(options);
  this.#dragDrop = this.options.dragDrop.map(d => {
    d.permissions = {
      dragstart: this._canDragStart.bind(this),
      drop: this._canDragDrop.bind(this)
    };
    d.callbacks = {
      dragstart: this._onDragStart.bind(this),
      drop: this._onDrop.bind(this)
    };
    return new foundry.applications.ux.DragDrop(d);
  });
}

_onRender(context, options) {
  this.#dragDrop.forEach(d => d.bind(this.element));
}

Common Pitfalls

1. Forgetting super.activateListeners()

// WRONG - breaks base functionality
activateListeners(html) {
  html.on("click", ".rollable", this._onRoll.bind(this));
}

// CORRECT
activateListeners(html) {
  super.activateListeners(html);
  html.on("click", ".rollable", this._onRoll.bind(this));
}

2. Context Binding Issues

// WRONG - loses 'this' context
html.on("click", ".rollable", this._onRoll);

// CORRECT
html.on("click", ".rollable", this._onRoll.bind(this));

3. Memory Leaks from Global Listeners

// WRONG - binds globally on every render
activateListeners(html) {
  super.activateListeners(html);
  $(document).on("click", this._onClick.bind(this));
}

// CORRECT - namespace and unbind first
activateListeners(html) {
  super.activateListeners(html);
  $(document).off("click.mysheet").on("click.mysheet", this._onClick.bind(this));
}

// Clean up on close
close(options) {
  $(document).off("click.mysheet");
  return super.close(options);
}

4. V2 Static Action Mistakes

// WRONG - action handler isn't static
static DEFAULT_OPTIONS = {
  actions: {
    roll: this._onRoll  // Error!
  }
}

// CORRECT - use static private method
static DEFAULT_OPTIONS = {
  actions: {
    roll: this.#onRoll
  }
}

static #onRoll(event, target) {
  // ...
}

5. V2 Partial Re-render Hook Multiplication

// PROBLEM - element added multiple times
Hooks.on("renderMySheet", (app, html, data) => {
  html.append("<div class='custom'></​div>");
});

// SOLUTION - check if exists
Hooks.on("renderMySheet", (app, html, data) => {
  if (!html.querySelector(".custom")) {
    html.append("<div class='custom'></​div>");
  }
});

6. Form Data Type Mismatches

<!-- WRONG - saves as string -->
<input type="text" name="system.level" value="{{system.level}}"/>

<!-- CORRECT - saves as number -->
<input type="text" name="system.level" value="{{system.level}}" data-dtype="Number"/>

<!-- Checkbox must use checked helper -->
<input type="checkbox" name="system.equipped" {{checked system.equipped}}/>

7. Async in V1 vs V2

// V1 - getData is sync, use async: false
getData() {
  context.enrichedBio = TextEditor.enrichHTML(bio, { async: false });
  return context;
}

// V2 - _prepareContext is async, use async: true
async _prepareContext(options) {
  context.enrichedBio = await TextEditor.enrichHTML(bio, { async: true });
  return context;
}

Implementation Checklist

V1 Sheet

  • Extend ActorSheet or ItemSheet
  • Define static get defaultOptions() with template, classes, tabs
  • Implement getData() returning context object
  • Call super.activateListeners(html) first
  • Check this.isEditable before binding edit controls
  • Use .bind(this) for all event handlers
  • Clean up global event listeners in close()

V2 Sheet

  • Extend ActorSheetV2 with HandlebarsApplicationMixin
  • Define static DEFAULT_OPTIONS with tag: "form"
  • Define static PARTS for each template section
  • Define static TABS if using tabs
  • Implement async _prepareContext() with await super._prepareContext()
  • Implement _preparePartContext() for tab data
  • Use static actions with # prefix handlers
  • Use .draggable class and data-item-id for drag/​drop

References


Last Updated: 2026-01-04 Status: Production-Ready Maintainer: ImproperSubset

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View