Skip to content

Custom Code & Styles ​

ISDL's declarative language covers the vast majority of what a Foundry system needs, and the inline @js{} escape hatch handles small gaps inside your logic. But some things live outside what ISDL generates — registering world settings, adding a button to the token HUD, integrating a third-party module like Dice So Nice, or shipping an extra sheet.

For exactly these cases, every generated system ships two hand-editable files that the generator will never overwrite:

FilePurpose
system/<id>-custom.mjsNative Foundry JavaScript — hooks, settings, classes, anything
css/<id>-custom.cssCustom CSS for tweaks the generated styles don't cover

<id> is the id from your config block. For config EZD6 { id = "ezd6" } the files are ezd6-custom.mjs and ezd6-custom.css.

my-system/
ā”œā”€ā”€ system.json
ā”œā”€ā”€ system/
│   ā”œā”€ā”€ ezd6-main.mjs      ← generated, overwritten every build
│   └── ezd6-custom.mjs    ← YOURS, never overwritten
└── css/
    ā”œā”€ā”€ ezd6.css           ← generated, overwritten every build
    └── ezd6-custom.css    ← YOURS, never overwritten

They are never overwritten ​

On the first generation, ISDL creates each file with a small placeholder. On every later generation it checks whether the file already exists and, if so, leaves it completely untouched. Your code is safe across regenerations.

The default <id>-custom.mjs looks like this:

kotlin
// Write your custom code and hooks here. This file will not be overwritten by the generator.

Hooks.once("init", () => {});

Warning

Because the file is only created when it's missing, deleting it causes the next generation to recreate the empty stub — taking your code with it. Keep it in source control.

How the JavaScript file loads ​

<id>-custom.mjs is registered in system.json as a real ES module, loaded right after the generated entry point:

kotlinonc
"esmodules": [
  "system/ezd6-main.mjs",
  "system/ezd6-custom.mjs"
]

Because it's a normal module loaded at system startup, every Foundry hook is available at the correct time. Register init-time things inside Hooks.once("init", …), and runtime things in the relevant hook:

kotlin
Hooks.once("init", () => {
    // settings, document/sheet registration, CONFIG tweaks
});

Hooks.once("ready", () => {
    // anything that needs the world fully loaded
});

This is the key difference from @js{}: the escape hatch runs inside a generated method with no stable contract, while custom.mjs is a stable, supported, top-level module with the full Foundry API at your disposal.

Hot reload ​

Both files are wired into the system's hot-reload configuration (mjs and css are reloadable). CSS edits and many runtime changes apply instantly. Changes that run at init (settings, sheet/class registration) take effect on the next world reload (F5), since init only fires once per load.

Common uses ​

  • System settings — game.settings.register(...) (world/client toggles, choices, numbers)
  • Secondary / alternate sheets — a "mini" sheet registered with makeDefault: false
  • UI hooks — renderTokenHUD, getActorSheetHeaderButtons, renderChatLog, etc.
  • Third-party integration — Dice So Nice, Drag Ruler, and friends
  • Global helpers / macro API — expose functions players can call from macros
  • Anything in the Foundry API that ISDL doesn't model declaratively

Example — register a setting ​

kotlin
Hooks.once("init", () => {
    game.settings.register("ezd6", "showToHit", {
        name: "Show To-Hit on Sheet",
        hint: "Display the character's to-hit value on the character sheet.",
        scope: "world",     // "world" (GM-set, shared) or "client" (per user)
        config: true,       // show it in Foundry's Settings UI
        type: Boolean,
        default: true
    });
});

// Read it anywhere:
const show = game.settings.get("ezd6", "showToHit");

Example — expose a macro helper ​

kotlin
Hooks.once("ready", () => {
    game.ezd6 = game.ezd6 ?? {};
    // Players can now call game.ezd6.rollHeroDie() from a macro
    game.ezd6.rollHeroDie = (actor = canvas.tokens.controlled[0]?.actor) => {
        return new Roll("1d6").toMessage({ speaker: ChatMessage.getSpeaker({ actor }) });
    };
});

Example — add a button to the Token HUD ​

kotlin
Hooks.on("renderTokenHUD", (hud, html, data) => {
    const actor = hud.object?.actor;
    if (!actor) return;

    const button = document.createElement("div");
    button.classList.add("control-icon");
    button.dataset.tooltip = "Roll Hero Die";
    button.innerHTML = `<i class="fa-solid fa-dice-d6"></i>`;
    button.addEventListener("click", () => game.ezd6?.rollHeroDie(actor));

    // The exact container/DOM shape varies between Foundry versions
    // (jQuery in v12, HTMLElement in v13) — consult the hook's docs for your version.
    (html instanceof HTMLElement ? html : html[0]).querySelector(".col.right")?.appendChild(button);
});

Example — register a secondary sheet ​

ISDL registers one generated sheet per document type as the default. To offer an additional sheet (e.g. a compact "mini" sheet), define a sheet class and register it with makeDefault: false:

kotlin
Hooks.once("init", () => {
    // MiniCharacterSheet extends Foundry's actor sheet application —
    // see the Foundry API docs for the base class in your version.
    Actors.registerSheet("ezd6", MiniCharacterSheet, {
        types: ["character"],
        makeDefault: false,
        label: "EZD6 Mini Sheet"
    });
});

Example — custom CSS ​

css
/* css/ezd6-custom.css */
.ezd6 .strikes-tracker {
    border: 2px solid #c0392b;
    border-radius: 6px;
}

Note

Generated styles are scoped under your system id (e.g. .ezd6). Scope your custom rules the same way to avoid leaking styles into other systems or core Foundry UI.

Tip

For most styling — colors, fonts, borders, sizing — reach for the higher source-side layers first: declarative theme { } tokens and a sidecar SCSS file. See Theming & Styles. custom.css is the last resort for tweaks those can't reach.

When to reach for what — escape-hatch tiers ​

ISDL gives you three layers. Prefer the highest one that can do the job:

  1. Declarative ISDL — fields, actions, roll(...), chat, on <hook> handlers, functions. Generated, validated, and localized for you. Always prefer this.
  2. @js{} inline escape hatch — a few lines of JS inside an action when ISDL can't express something small. No stable contract; use sparingly.
  3. custom.mjs / custom.css — native, top-level, regeneration-safe code for everything outside the generated layer (settings, extra sheets, UI hooks, module integration). A supported, stable surface — the right home for larger or system-wide customization.

See also ​

  • Advanced Logic — functions, hooks, and the declarative features to exhaust before dropping to native code
  • Logic Reference — the inline @js{} escape hatch
  • Config — where your system id (and therefore the file names) comes from
  • Command Line Interface — regenerating your system (your custom files survive it)