Note: This guide was originally made for the DCC system, so some terms will mention DCC and you should use your own package's terms instead.
V12 vs V13 HTML Parameter Change:
html parameters in hooks and activateListeners() are jQuery objectshtml parameters are plain DOM elements (no jQuery methods)Required V13 Conversions:
// V12 jQuery-style (BREAKS in V13)
html.find('.my-button').click(handler)
html.html('<p>Content</p>')
html.addClass('active')
// V13 DOM-style (V13 compatible)
html.querySelector('.my-button').addEventListener('click', handler)
html.innerHTML = '<p>Content</p>'
html.classList.add('active')
*CRITICAL: V13 introduces CSS Layers, which is good, but means that your existing styling that relied Foundry styles is probably broken.
To fix this, you need to implement your own theming using CSS variables and layers. See "Implementing Themes" section below for details on how to do this.
---
## Basic ApplicationV2 Migration
### 1. Change Class Inheritance
**For Actor Sheets:**
```javascript
// V1 (Old)
class DCCActorSheet extends FormApplication {
// V2 (New)
const { HandlebarsApplicationMixin } = foundry.applications.api
const { ActorSheetV2 } = foundry.applications.sheets
class DCCActorSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
For Dialogs and Config Applications:
// V1 (Old)
class MyDialog extends FormApplication {
// V2 (New)
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api
class MyDialog extends HandlebarsApplicationMixin(ApplicationV2) {
// V1 Pattern (Old)
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ['dcc', 'sheet', 'actor'],
template: 'systems/dcc/templates/actor-sheet.html',
width: 600,
height: 600,
resizable: true
})
}
// V2 Pattern (New)
/** @inheritDoc */
static DEFAULT_OPTIONS = {
classes: ['dcc', 'sheet', 'actor', 'themed', 'theme-light'], // ⚠️ themed classes are workaround only
position: {
width: 600,
height: 600
},
window: {
resizable: true,
title: 'DCC.ActorSheetTitle' // Just the localization key
}
}
// V1 Pattern (Old)
getData() {
const context = super.getData()
context.data = this.object.system
context.flags = this.object.flags
return context
}
// V2 Pattern (New)
async _prepareContext(options) {
const context = await super._prepareContext(options)
context.system = this.actor.system // Note: this.actor for ActorSheetV2
context.flags = this.actor.flags // or this.document for ApplicationV2
return context
}
/** @inheritDoc */
static PARTS = {
form: {
template: 'systems/dcc/templates/dialog-actor-config.html'
}
}
CRITICAL: Dialogs and forms MUST include tag: 'form' in DEFAULT_OPTIONS:
static DEFAULT_OPTIONS = {
tag: 'form', // REQUIRED for dialogs and forms
form: {
submitOnChange: false,
closeOnSubmit: true
}
}
Template Requirements: When using tag: 'form', templates should NOT contain <form> elements:
<!-- WRONG: Nested forms are invalid HTML -->
<form class="config-dialog">
<!-- content -->
</form>
<!-- CORRECT: Use section or div -->
<section class="config-dialog">
<!-- content -->
</section>
Pattern 1: Static Form Handler (Recommended)
static DEFAULT_OPTIONS = {
tag: 'form',
form: {
handler: MyClass.#onSubmitForm,
closeOnSubmit: true,
submitOnChange: false
}
}
/**
* Handle form submission
* @this {MyClass}
* @param {SubmitEvent} event
* @param {HTMLFormElement} form
* @param {FormDataExtended} formData
*/
static async #onSubmitForm(event, form, formData) {
event.preventDefault()
await this.document.update(formData.object) // Note: formData.object
}
When you need to edit items inline on an actor sheet (e.g., weapons, equipment, spells), you must handle these updates separately from the main actor update. This is because ApplicationV2's form validation will strip out nested item data.
Template Pattern:
{{!-- Use "items" prefix for inline item edits --}}
<li class="weapon" data-item-id="{{weapon._id}}">
<input type="text"
name="items.{{weapon._id}}.name"
value="{{weapon.name}}"/>
<input type="checkbox"
name="items.{{weapon._id}}.system.equipped"
{{checked weapon.system.equipped}}/>
<input type="text"
name="items.{{weapon._id}}.system.damage"
value="{{weapon.system.damage}}"/>
</li>
Implementation Pattern (actor-sheet.js:1025-1066):
/** @override */
_processFormData(event, form, formData) {
// Extract the raw form data object BEFORE validation strips out items
const expanded = foundry.utils.expandObject(formData.object)
// Handle items separately if they exist
if (expanded.items) {
// Store for later processing
this._pendingItemUpdates = Object.entries(expanded.items).map(([id, itemData]) => ({
_id: id,
...itemData
}))
// Remove from the expanded object
delete expanded.items
// Flatten and replace the existing formData.object properties
const flattened = foundry.utils.flattenObject(expanded)
// Clear existing object and repopulate (since we can't reassign)
for (const key in formData.object) {
delete formData.object[key]
}
Object.assign(formData.object, flattened)
}
// Call parent with modified formData
return super._processFormData(event, form, formData)
}
/** @override */
async _processSubmitData(event, form, formData) {
// Process the actor data normally
const result = await super._processSubmitData(event, form, formData)
// Now handle any pending item updates
if (this._pendingItemUpdates?.length > 0) {
await this.document.updateEmbeddedDocuments('Item', this._pendingItemUpdates)
delete this._pendingItemUpdates // Clean up
}
return result
}
Key Points:
items.{{id}}.property naming convention in templates_processFormData to extract item data before validationthis._pendingItemUpdates_processSubmitData to apply item updates after actor updateCRITICAL: ApplicationV2 constructor takes ALL parameters in a single options object:
// V1 (FormApplication) - WRONG for V2
new DCCActorConfig(this.actor, { position: { ... } })
// V2 (ApplicationV2) - CORRECT
new DCCActorConfig({
document: this.actor, // Document goes INSIDE options
position: { ... }
})
// In your ApplicationV2 class
get document() {
return this.options.document // Document comes from options
}
// Usage in methods
async _prepareContext(options) {
const context = await super._prepareContext(options)
context.actor = this.document
return context
}
V1 Pattern (Old):
activateListeners(html) {
super.activateListeners(html)
html.find('.ability-label').click(this._onRollAbilityCheck.bind(this))
html.find('.item-create').click(this._onItemCreate.bind(this))
html.find('.item-delete').click(this._onItemDelete.bind(this))
}
_onRollAbilityCheck(event) {
event.preventDefault()
const ability = event.currentTarget.dataset.ability
this.actor.rollAbilityCheck(ability)
}
V2 Pattern (New):
static DEFAULT_OPTIONS = {
actions: {
rollAbilityCheck: this.#rollAbilityCheck,
itemCreate: this.#itemCreate,
itemDelete: this.#itemDelete
}
}
/**
* Handle ability check rolls
* @this {DCCActorSheet}
* @param {PointerEvent} event
* @param {HTMLElement} target
*/
static async #rollAbilityCheck(event, target) {
event.preventDefault()
const ability = target.dataset.ability
await this.actor.rollAbilityCheck(ability)
}
V1 HTML (Old):
<span class="ability-label" data-ability="str">Strength</span>
<button class="item-create" data-type="weapon">Create Weapon</button>
V2 HTML (New):
<span data-action="rollAbilityCheck" data-ability="str">Strength</span>
<button data-action="itemCreate" data-type="weapon">Create Weapon</button>
Key Points:
data-action value must match the key in the actions objectdata-* attributes are passed to the handler via target parameter.item-create - use semantic classes for styling onlyV1 Pattern (Old):
<img src="{{img}}" data-edit="img" alt="Portrait">
V2 Pattern (New):
<img src="{{img}}" data-action="editImage" data-field="img" alt="Portrait">
static DEFAULT_OPTIONS = {
actions: {
editImage: this.#onEditImage
}
}
static async #onEditImage(event, target) {
const field = target.dataset.field || "img"
const current = foundry.utils.getProperty(this.document, field)
const fp = new foundry.applications.apps.FilePicker({
type: "image",
current: current,
callback: (path) => this.document.update({ [field]: path })
})
fp.render(true)
}
V1 Pattern (Old):
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
tabs: [{
navSelector: '.tabs',
contentSelector: '.tab-content',
initial: 'character'
}]
})
}
V2 Pattern (New):
static TABS = {
sheet: {
tabs: [
{ id: 'character', group: 'sheet', label: 'DCC.Character' },
{ id: 'equipment', group: 'sheet', label: 'DCC.Equipment' },
{ id: 'notes', group: 'sheet', label: 'DCC.Notes' }
],
initial: 'character'
}
}
static PARTS = {
tabs: {
template: 'systems/dcc/templates/actor-partial-tabs.html'
},
character: {
template: 'systems/dcc/templates/actor-partial-pc-common.html'
},
equipment: {
template: 'systems/dcc/templates/actor-partial-pc-equipment.html'
},
notes: {
template: 'systems/dcc/templates/actor-partial-pc-notes.html'
}
}
CRITICAL: Tab templates MUST have proper structure:
{{!-- Individual Tab Template --}}
<section class="tab {{tabs.character.id}} {{tabs.character.cssClass}}"
data-tab="{{tabs.character.id}}"
data-group="{{tabs.character.group}}">
{{!-- Tab content here --}}
<div class="character-details">
<!-- form fields, etc. -->
</div>
</section>
Required Elements:
tab CSS classdata-tab="{{tabs.tabId.id}}" and data-group="{{tabs.tabId.group}}"{{tabs.tabId.cssClass}} for dynamic CSS classes<section> or <div> (never <form> inside a form)For applications where tabs vary based on document type:
_getTabsConfig(group) {
const tabs = foundry.utils.deepClone(super._getTabsConfig(group))
// Modify tabs based on document properties
if (this.document.type === 'weapon') {
tabs.tabs.push({ id: 'combat', group: 'sheet', label: 'DCC.Combat' })
}
return tabs
}
Problem: If parent elements have display: grid or display: flex, ALL tabs will be visible at once.
Solution: Never set display properties on elements that directly contain tabs:
/* WRONG - Breaks tab switching */
.sheet-body { display: grid; }
/* CORRECT - Apply to inner elements */
.tab-content-wrapper { /* no display here */ }
.character-grid { display: grid; }
CRITICAL INSIGHT: FoundryVTT V13 has a two-tier drag/drop system:
.draggable setup with basic item/effect supportIf you're extending ActorSheetV2, you get these features automatically:
const { ActorSheetV2 } = foundry.applications.sheets
class MyActorSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
// ActorSheetV2 automatically provides:
// - DragDrop setup with '.draggable' selector in _onRender()
// - Permission checks via _canDragStart/_canDragDrop (checks isEditable)
// - Basic item dragging for elements with data-item-id
// - Active effect dragging for elements with data-effect-id
// - Item sorting within the same actor via _onSortItem
// - Document drop handling with delegation to _onDropItem, _onDropActiveEffect, etc.
}
What ActorSheetV2 gives you for free:
draggable are automatically draggabledata-item-id create proper item drag datadata-effect-id create proper effect drag data_onSortItemisEditable propertyYou need manual drag/drop setup when:
ApplicationV2 (not ActorSheetV2).draggableV1 Pattern (Old - Automatic):
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
dragDrop: [{ dragSelector: '.item', dropSelector: '.item-list' }]
})
}
// Framework automatically created and bound DragDrop handlers
V2 ActorSheetV2 Pattern (Automatic for Basic Items):
const { ActorSheetV2 } = foundry.applications.sheets
class MyActorSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
// No drag/drop setup needed for basic items!
// Just add class="draggable" and data-item-id to your templates
}
Template for ActorSheetV2 Auto Drag/Drop:
<!-- ActorSheetV2 handles this automatically -->
<li class="item draggable" data-item-id="{{item._id}}">
{{item.name}}
</li>
<!-- Active effects work automatically too -->
<li class="effect draggable" data-effect-id="{{effect._id}}">
{{effect.name}}
</li>
V2 ApplicationV2 Pattern (Manual Setup Required):
const { DragDrop } = foundry.applications.ux
class MyAppV2 extends HandlebarsApplicationMixin(ApplicationV2) {
#dragDrop
static DEFAULT_OPTIONS = {
dragDrop: [{
dragSelector: '[data-drag="true"]',
dropSelector: '.drop-zone'
}]
}
constructor(options = {}) {
super(options)
this.#dragDrop = this.#createDragDropHandlers()
}
#createDragDropHandlers() {
return this.options.dragDrop.map((d) => {
d.permissions = {
dragstart: this._canDragStart.bind(this),
drop: this._canDragDrop.bind(this)
}
d.callbacks = {
dragstart: this._onDragStart.bind(this),
dragover: this._onDragOver.bind(this),
drop: this._onDrop.bind(this)
}
return new DragDrop(d)
})
}
_onRender(context, options) {
this.#dragDrop.forEach((d) => d.bind(this.element))
}
_canDragStart(selector) {
return this.document.isOwner && this.isEditable
}
_canDragDrop(selector) {
return this.document.isOwner && this.isEditable
}
_onDragOver(event) {
// Optional: handle dragover events if needed
}
}
ApplicationV2 calls drag handlers with different signatures than v1:
// ✅ Correct V2 signature - single event parameter
_onDragStart(event) {
const element = event.currentTarget
// Implementation
}
// ❌ Old v1 signature - two parameters (not used by ApplicationV2)
_onDragStart(event, target) {
// This signature is not used by the framework
}
If your drag handlers need to call static helper methods, make sure they're public:
// ✅ Good - Public static method accessible from instance methods
static findDataset(element, attribute) {
while (element && !(attribute in element.dataset)) {
element = element.parentElement
}
return element?.dataset[attribute] || null
}
// ❌ Bad - Private static method not accessible from instances
static #findDataset(element, attribute) {
// Can't call this from _onDragStart instance method
}
// Usage in drag handler
_onDragStart(event) {
const itemId = MyClass.findDataset(event.currentTarget, 'itemId')
}
Add data-drag="true" to draggable elements:
<ol class="items">
{{#each items}}
<li data-drag="true" data-item-id="{{this.id}}" data-drag-action="weapon">
{{this.name}}
</li>
{{/each}}
</ol>
The DCC system uses a hybrid approach that extends ActorSheetV2's automatic features with custom drag types:
class DCCActorSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
#dragDrop
static DEFAULT_OPTIONS = {
// Custom selectors override ActorSheetV2 defaults
dragDrop: [{
dragSelector: '[data-drag="true"]', // Custom selector
dropSelector: '.item-list, .weapon-list, .armor-list, .skill-list'
}]
}
constructor(options = {}) {
super(options)
this.#dragDrop = this.#createDragDropHandlers()
}
_onRender(context, options) {
// Manual setup replaces ActorSheetV2's automatic setup
this.#dragDrop.forEach((d) => d.bind(this.element))
}
// Custom drag handler for DCC-specific drag types
_onDragStart(event) {
const li = event.currentTarget
if (!li.dataset.drag) return
const dragAction = li.dataset.dragAction
let dragData = null
switch (dragAction) {
case 'ability': {
// Custom DCC ability check macro creation
const abilityId = DCCActorSheet.findDataset(event.currentTarget, 'ability')
dragData = {
type: 'Ability',
actorId: this.actor.id,
data: { abilityId }
}
break
}
case 'spellCheck': {
// Custom DCC spell check macro creation
const ability = DCCActorSheet.findDataset(event.currentTarget, 'ability')
dragData = {
type: 'Spell Check',
actorId: this.actor.id,
data: { ability }
}
break
}
case 'weapon': {
// Falls back to ActorSheetV2 item handling if needed
const itemId = DCCActorSheet.findDataset(event.currentTarget, 'itemId')
const weapon = this.actor.items.get(itemId)
if (weapon) {
dragData = Object.assign(weapon.toDragData(), {
dccType: 'Weapon',
actorId: this.actor.id
})
}
break
}
}
if (dragData) {
if (this.actor.isToken) dragData.tokenId = this.actor.token.id
event.dataTransfer.setData('text/plain', JSON.stringify(dragData))
}
}
_onDrop(event) {
const data = foundry.applications.ux.TextEditor.getDragEventData(event)
if (!data) return false
// Convert DCC-specific drops back to standard Item drops
if (data.type === 'DCC Item') {
data.type = 'Item'
}
// Delegate to ActorSheetV2's built-in drop handling
return super._onDrop?.(event)
}
}
.draggable class - no setup neededActorSheetV2 provides these methods you can customize:
// Permission checks (default: checks this.isEditable)
_canDragStart(selector) { return this.isEditable }
_canDragDrop(selector) { return this.isEditable }
// Drag start (default: handles items with data-item-id, effects with data-effect-id)
_onDragStart(event) { /* creates item/effect drag data */ }
// Drop handling (default: delegates to _onDropDocument)
_onDrop(event) { /* processes drop data, calls hooks */ }
// Document drop routing (default: routes to specific handlers)
_onDropDocument(event, document) { /* routes by document type */ }
// Item drops (default: creates item on actor or sorts within actor)
_onDropItem(event, item) { /* handles item creation/sorting */ }
// Item sorting (default: uses Foundry's integer sort algorithm)
_onSortItem(event, item) { /* reorders items within actor */ }
This hybrid approach allows you to leverage ActorSheetV2's built-in functionality while adding system-specific features.
FoundryVTT V13 provides a hierarchy of v2 application classes:
ApplicationV2 (Base)
├── DocumentSheetV2 (Document-specific)
│ └── ActorSheetV2 (Actor-specific)
└── DialogV2 (Modal dialogs)
Use for: Custom applications, configuration dialogs, tools
Provides:
Use for: Item sheets, journal entry sheets, other document types
Extends ApplicationV2 with:
isVisible, isEditable)Use for: Actor character sheets
Extends DocumentSheetV2 with:
.draggable selectordata-item-id) and effects (data-effect-id)Use for: User prompts, confirmations, input collection
Extends ApplicationV2 with:
confirm(), prompt(), input(), wait())// For actor sheets - use ActorSheetV2
class MyActorSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
// Gets automatic drag/drop, actor-specific features
}
// For item sheets - use DocumentSheetV2
class MyItemSheet extends HandlebarsApplicationMixin(DocumentSheetV2) {
// Gets document handling, no automatic drag/drop
}
// For configuration dialogs - use ApplicationV2
class MyConfigDialog extends HandlebarsApplicationMixin(ApplicationV2) {
// Gets basic application features only
}
// For user prompts - use DialogV2
class MyPrompt extends DialogV2 {
// Gets modal behavior, button handling
}
V12 Pattern (Deprecated):
{{editor corruptionHTML target="system.class.corruption" engine="prosemirror" button=true editable=editable}}
V13 Pattern (Required):
{{#if editable}}
<prose-mirror
name="system.class.corruption"
button="true"
editable="{{editable}}"
toggled="false"
value="{{system.class.corruption}}">
{{{corruptionHTML}}}
</prose-mirror>
{{else}}
{{{corruptionHTML}}}
{{/if}}
async _prepareContext(options) {
const context = await super._prepareContext(options)
// Enrich content for display
context.corruptionHTML = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
this.document.system.class.corruption,
{
secrets: this.document.isOwner,
relativeTo: this.document
}
)
return context
}
CRITICAL: Cannot use CONFIG values in static field initializers:
// ❌ BROKEN - CONFIG.DCC is undefined during static initialization
static PARTS = {
form: {
template: CONFIG.DCC.templates.rollModifierDialog // ERROR
}
}
// ✅ WORKING - Hard-coded template path
static PARTS = {
form: {
template: 'systems/dcc/templates/dialog-roll-modifiers.html'
}
}
Before (V1):
class DCCActorConfig extends FormApplication {
static get defaultOptions() {
const options = super.defaultOptions
options.template = 'systems/dcc/templates/dialog-actor-config.html'
options.width = 380
return options
}
get title() {
return `${this.object.name}: ${game.i18n.localize('DCC.SheetConfig')}`
}
getData() {
const data = this.object
data.config = CONFIG.DCC
return data
}
async _updateObject(event, formData) {
await this.object.update(formData)
}
}
After (V2):
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api
class DCCActorConfig extends HandlebarsApplicationMixin(ApplicationV2) {
/** @inheritDoc */
static DEFAULT_OPTIONS = {
classes: ['dcc', 'sheet', 'actor-config'],
tag: 'form',
position: { width: 380, height: 'auto' },
window: { title: 'DCC.SheetConfig', resizable: false },
form: {
handler: DCCActorConfig.#onSubmitForm,
closeOnSubmit: true
}
}
/** @inheritDoc */
static PARTS = {
form: {
template: 'systems/dcc/templates/dialog-actor-config.html'
}
}
get document() {
return this.options.document
}
get title() {
return `${this.document.name}: ${game.i18n.localize('DCC.SheetConfig')}`
}
async _prepareContext(options) {
const context = await super._prepareContext(options)
context.config = CONFIG.DCC
context.actor = this.document
return context
}
static async #onSubmitForm(event, form, formData) {
event.preventDefault()
await this.document.update(formData.object)
}
}
Key Changes:
See the complete examples in the original sections above for detailed implementations.