Skip to content

Theming & Styles ​

ISDL lets you control a system's look from the source .isdl file — no hand-editing generated CSS. You declare a small vocabulary of style tokens and ISDL compiles them into the sheet. There are three layers, from simplest to most powerful:

  1. Theme tokens — config { theme { ... } } for system-wide defaults, and a per-field theme: { ... } override. Covered on this page.
  2. Sidecar stylesheets — sheetStyles / globalStyles SCSS files for bespoke atmosphere (web fonts, grain, shadows, chat cards) a token can't express. Below.
  3. custom.css — the regeneration-safe escape hatch for anything else. See Custom Code & Styles.

Tip

Reach for the highest layer that does the job. Many systems only need a theme { } block.


The theme { } block ​

Declared inside config. Every token compiles to a --isdl-* CSS custom property and is applied with a sensible fallback, so an unthemed system looks exactly as it did before — tokens only change what you set.

kotlin
config Noir {
    id = "blackwater-files"
    label = "Blackwater Files"

    theme {
        // palette roots
        primary: #c9a227,
        background: #16130f,
        text: #d6cfbf,

        // grouped component blocks
        border  { color: #3a3128, radius: 0 },
        font    { family: "'Special Elite', monospace" },
        heading { color: #c9a227, font: "'Oswald', sans-serif", transform: "uppercase" }
    }
}

The vocabulary is a hybrid: flat palette roots for the colors used everywhere, plus grouped component blocks whose inner names are plain CSS sub-properties.

Palette roots ​

TokenSetsNotes
primary: #hexThe system's lead colorPaints the field highlight edge; also the default for the per-document color picker.
secondary: #hexPicker seedDefault for the secondary color picker (whole-sheet).
tertiary: #hexPicker seedDefault for the tertiary color picker (whole-sheet).
background: #hexThe sheet surface colorPaints the whole sheet (window + Vuetify app + page texture).
text: #hexBody text colorPropagates through every component.

Grouped component blocks ​

Each block styles one target; the inner names are the plain CSS sub-property, so the block name supplies the prefix.

BlockPropertiesStyles
border { }color: #hex, width: <px>, radius: <px>The field wrapper's border + corner rounding.
font { }family: "<stack>", size: <px>The body font and base text size.
heading { }color: #hex, font: "<stack>", size: <px>, transform: "<css>"Card titles and the document name. transform is a CSS value like "uppercase".
disabledText { }color: #hex, size: <px>Play-mode (read-only) field text. See Readable play mode.
width { }min: <px>, max: <px>A field wrapper's min/max width. Per-field only — see Scope.
height { }min: <px>, max: <px>A field wrapper's min/max height. Per-field only.

Sizes & units ​

Size-like values accept a bare number (treated as pixels) or an explicit px literal:

isdl
border { width: 2, radius: 6 }        // 2px, 6px
width  { min: 120px, max: 240px }      // explicit px

Note

Sizes are pixel-based today. Other CSS units (%, em, vw) are not yet supported in tokens — use a sidecar stylesheet if you need them.

Punctuation ​

Entries are comma-separated, exactly like every other parameter list in ISDL — both the top-level tokens and the entries inside a group. (No trailing comma after the last entry.)

isdl
theme {
    primary: #c9a227,
    border  { color: #3a3128, width: 2, radius: 6 }
}

The one bit of latitude is optional: a group may be written with or without a colon before its brace, so both border { … } and border: { … } parse — use whichever reads best.


Per-field overrides ​

Any field can carry a theme: { } param that overrides the theme for that field only, using the same grouped vocabulary:

isdl
// Luck gets a blood-red edge, beating the global amber — on this field alone.
attribute Luck(theme: { primary: #8b1e1e })

// A wider, bordered, taller number field.
number Health(theme: {
    primary: #c0392b,
    border:  { color: #c0392b, width: 3px, radius: 8px },
    width:   { min: 120px, max: 240px },
    height:  { min: 40px }
})

A per-field value cascades over the global default automatically.

Token scope ​

The theme { } and theme: { } blocks share one grammar, but not every token makes sense in both places. ISDL validates this and reports an error if you cross the line:

TokensAllowed in config { theme }Allowed in field theme:
primary, borderāœ…āœ…
width, heightāŒ (a field-sizing concept)āœ…
secondary, tertiary, background, text, font, heading, disabledTextāœ…āŒ (whole-sheet concepts)

The rule of thumb: a token works per-field only if it visibly affects an individual field. Sheet-wide chrome (body font, surface, headings, the color pickers) is config-only; field sizing (width/height) is field-only.

isdl
// āŒ error: 'width' sizes an individual field, not the whole system
config T { theme { width { min: 100px } } }

// āŒ error: 'background' is a whole-sheet token with no per-field effect
number Foo(theme: { background: #222222 })

Theming layout containers ​

row, column, and section accept a theme: { } too — for sizing and box chrome on the container itself, using the same vocabulary. Unlike field and global themes (which compile to inheriting --isdl-* variables), a container's theme compiles to a plain inline style="…" of real CSS on that element, so it sizes the container without leaking into the fields nested inside it.

isdl
// A constrained row, a sized column, and a bordered section card.
row(theme: { width: { max: 800px }, height: { min: 120px } }) {

    section Vitals(theme: {
        width:  { max: 640px },
        border: { color: #c9a227, width: 2px, radius: 12px },
        background: #1a1410,
        text:   #f0e6d2
    }) {
        column(theme: { width: { min: 80px, max: 160px } }) {
            number Health
        }
    }
}

Each container accepts a different slice of the vocabulary — and ISDL validates it:

Tokensrow / columnsection
width, heightāœ…āœ…
borderāœ…āœ…
background, textāŒāœ…
primary, font, heading, disabledText, ā€¦āŒāŒ

A row and a column take sizing (width / height) and a border — enough to size a band of fields and box it off. A section is a visible card, so it additionally takes the fill that paints that card — background and text. (A section's chrome lands on its inner <v-card>; see Generated class names.)

Note

A border on a row or column only renders if you give it a width — border { color: #c9a227 } alone sets the color but no visible line. (A section already has an outlined card border, so color alone recolors it.)

isdl
// āœ… a bordered, sized row
row(theme: { width: { max: 800px }, border: { color: #c9a227, width: 2px, radius: 8px } }) { … }

// āŒ error: 'background' is a section fill, not a row/column token
row(theme: { background: #1a1410 }) { … }

// āŒ error: 'font' is a whole-sheet token, not a section chrome token
section S(theme: { font: { family: "Roboto" } }) { … }

Readable play mode ​

In Play mode most fields are disabled (read-only) by default unless marked otherwise so values can't be edited by accident. ISDL keeps that text fully readable by default — a solid dark on-field color at full opacity, rather than the faded gray Vuetify uses (which is hard to read). The "you can't edit this" cue comes from the missing border and caret, not from washed-out text.

Recolor or resize it with the disabledText { } block:

isdl
theme {
    disabledText { color: #555555, size: 13 }
}

Per-document color pickers ​

By default, each actor/item sheet has a Setup Colors button letting players pick their own primary / secondary / tertiary colors (stored per document). Those pickers default to your theme's palette values, and a player's pick overrides the theme for their sheet.

For an art-directed system whose look should be fixed, turn the pickers off in config:

isdl
config Noir {
    id = "blackwater-files"
    userColors = false   // no per-document color pickers; the theme is the law
}

Sidecar stylesheets ​

When a token can't express what you want — a loaded web font, film grain, a vignette, hard shadows, a chat-card tweak — point config at one or both sidecar .scss files that live next to your .isdl. They differ only in scope:

isdl
config Noir {
    id = "blackwater-files"
    sheetStyles  = "noir.scss"          // scoped to the character sheets
    globalStyles = "noir-global.scss"   // scoped to the whole system (chat cards, dialogs, …)
}
OptionScoped underUse for
sheetStyles.<id>.vue-applicationThe generated character sheets — field tweaks, sheet atmosphere, display type.
globalStyles.<id>Every surface the system renders — sheets and chat cards, dialogs, datatables, prompts. The only way to style non-sheet surfaces like chat cards.

ISDL compiles each with the same Sass pipeline it uses internally and auto-scopes every rule under the relevant selector, so a careless rule can't leak into other systems or core Foundry UI. Leading @use / @import (e.g. a Google Font) are hoisted to the top so they load correctly.

Your sidecar can reference the theme tokens via their CSS variables:

scss
// noir.scss — bespoke atmosphere the tokens can't express
@import url('https://fonts.googleapis.com/css2?family=Special+Elite&display=swap');

.window-content {
    // film grain over the themed surface
    &::after {
        content: "";
        position: absolute; inset: 0; pointer-events: none;
        background: radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,.75) 100%);
    }
}

// reuse the theme's lead color for a bespoke rule
.section .v-card-title {
    letter-spacing: 0.18em;
    border-bottom: 1px solid var(--isdl-primary);
}

Generated class names ​

Every element ISDL renders carries stable, predictable classes so a sidecar stylesheet or custom.css can target it without inspecting the generated HTML.

ElementAlways-on classesNamed classNotes
Field (any type).isdl-field .isdl-<type> .isdl-visibility-<value>.isdl-field-<name>All four land on the same root node. .isdl-field is what the theme tokens consume; .isdl-<type> is the field kind; .isdl-field-<name> is the per-field hook; .isdl-visibility-<value> reflects declared visibility.
Section.isdl-section.isdl-section-<name>On the outer grid cell (<v-col>). The visible box is the inner <v-card> — a section's theme: and borders paint the card, so target .isdl-section-<name> .v-card for box chrome. Also still carries the legacy .section class.
Row.isdl-row—Rows are unnamed, so there's no per-row class. On the <v-row>.
Column.isdl-column—Columns are unnamed. On the <v-col>.
scss
// sheetStyles.scss — target by stable class, no generated-markup guesswork
.isdl-field-health { font-weight: 700; }          // one specific field
.isdl-field { letter-spacing: 0.01em; }           // every field

.isdl-section-attributes .v-card {                // one section's visible box
    box-shadow: 0 0 12px var(--isdl-primary, #0003);
}
.isdl-row    { gap: 0.5rem; }                      // every row
.isdl-column { align-content: start; }            // every column

Note

<name> is the ISDL name lowercased, with no spaces (names are single identifiers). A field MaxHealth → .isdl-field-maxhealth; a section Status Effects isn't possible (names can't contain spaces), but StatusEffects → .isdl-section-statuseffects.

How all four classes land on one element ​

All four classes are merged onto the same DOM node via Vue attribute fall-through onto the single-root component. So a gmOnly number XP renders one element with all of:

html
<div class="isdl-field isdl-number isdl-field-xp isdl-visibility-gmOnly single-wide"> … </div>

Target .isdl-field-xp, .isdl-number, and .isdl-visibility-gmOnly on the same node — not as ancestor/descendant.

These classes are not the same thing as the theme: { } tokens. The tokens are the high-level, validated, cross-version way to style; the classes are the low-level escape hatch for when a token doesn't exist for what you want. Prefer tokens; reach for the classes from a sidecar when you must.


CSS Class Reference ​

This is the exhaustive set of stable classes the generator emits. Use it as the lookup when a sidecar stylesheet or custom.css needs to target something a token can't express. Every class below is emitted by the code generator, not by you — they're safe to target because they survive regeneration.

Field type classes ​

Every field's root element carries an isdl-<type> class corresponding to its ISDL keyword. The left column is the keyword you write in .isdl.

ISDL keywordType classNotes
number.isdl-numberA calculated/derived number additionally puts .calculated-number on its inner input.
string.isdl-string
html.isdl-htmlThe ProseMirror rich-text editor block.
boolean.isdl-boolean
attribute.isdl-attributeThe inner box (box-style only) carries .isdl-attribute-box. Also carries .no-mod when the attribute has no modifier.
tracker.isdl-tracker
resource.isdl-resource
image.isdl-image
money.isdl-moneyDenomination breakdown also carries .isdl-money-denominations.
choice<string>.isdl-string-choice
choices<string>.isdl-string-choices
choice<DamageType>.isdl-damage-type-choice
damageTrack.isdl-damage-trackIndividual pips carry .damage-box--filled / .damage-box--empty.
bonuses.isdl-bonuses
resistances.isdl-resistances
rollVisualizer.isdl-roll-visualizerSub-parts: .isdl-roll-visualizer__header, __label, __avg, __empty, __footer, __approx.
measuredTemplate.isdl-measured-template
macro.isdl-macro
pips.isdl-paperdoll
die.isdl-die
dice.isdl-dice
date.isdl-date
time.isdl-time
datetime.isdl-datetime
document<X> (single link).isdl-document-link
choice<X> (document).isdl-document-choice
choices<X> (document).isdl-document-choices
table<X>.isdl-table
inventory<X>.isdl-inventory
pinned<X>.isdl-pinned
parent-property reference.isdl-parent-property-reference
self-property reference.isdl-self-property-reference

Per-field-NAME marker (every field) ​

ClassTargets
.isdl-fieldEvery field of every type. This is the selector the theme tokens themselves consume.
.isdl-field-<name>One specific field. <name> is the ISDL field name lowercased (number MaxHealth → .isdl-field-maxhealth). This is also where a per-field theme: override lands.

Visibility classes ​

Every field's root also carries an isdl-visibility-* class reflecting its declared visibility in the ISDL source. This is a static label — runtime show/hide is still handled by v-if/:disabled. Use it to give visual cues (e.g. a purple border on GM-only fields) without conditional logic in your stylesheet.

ISDL visibilityCSS class
(none declared).isdl-visibility-default
edit modifier or visibility: edit.isdl-visibility-edit
readonly modifier or visibility: readonly.isdl-visibility-readonly
gmOnly modifier or visibility: gmOnly.isdl-visibility-gmOnly
gmEdit modifier or visibility: gmEdit.isdl-visibility-gmEdit
play modifier or visibility: play.isdl-visibility-play
hidden modifier.isdl-visibility-hidden
locked modifier or visibility: locked.isdl-visibility-locked
unlocked modifier or visibility: unlocked.isdl-visibility-unlocked
secret modifier or visibility: secret.isdl-visibility-secret
visibility: { … } (method block).isdl-visibility-dynamic
scss
// Mark GM-only fields with a subtle purple left border
.isdl-visibility-gmOnly {
    border-left: 3px solid #9b59b6;
}

// Dim fields that only appear in edit mode when the sheet is in play mode
.isdl-visibility-edit {
    opacity: 0.5;
}

Structural / layout classes ​

ClassTargets
.isdl-section (+ legacy .section)A section's outer grid cell (<v-col>). The visible box is the inner <v-card> — target .isdl-section-<name> .v-card for box chrome.
.isdl-section-<name>One named section, name lowercased.
.isdl-rowA row container (<v-row>). Rows are unnamed — no per-row class.
.isdl-columnA column container (<v-col>). Columns are unnamed.
.single-wide / .double-wideWidth hint applied to most field roots — .double-wide fields (choices, resistances, etc.) span two grid columns.
.flexrow / .flexcolGeneric horizontal / vertical flex helpers used inside composite fields.
.tabs-windowThe tab content window (<v-tabs-window>) on a sheet.
.tabs-containerA single tab's content pane (<v-tabs-window-item>). Carries data-tab / data-type attributes you can also select on.
.resource-cardThe outlined card wrapping a resource bar. (.tracker-card is styled by the same rule but is a reserved selector — current resource/tracker fields render through .isdl-tracker.)
.prose-mirror-wrapperThe editing surface inside an html / paragraph field.

Datatable classes ​

ClassTargets
.isdl-datatableA generated DataTable (table, inventory, effects, pinned lists).
.datatableLegacy alias also present on datatable roots.
.datatable-drop-zoneThe drop target wrapping a table/inventory field. The universal .isdl-field-<name> marker merges onto this <div>.
.isdl-inventoryAn inventory field's container.
.isdl-image-actionA clickable item-image cell in a table; .isdl-image-action-overlay and .isdl-image-action-btn are its hover overlay and button.

Auto-generated journal / reference classes ​

The keyword, damage-type, and status-effect journal pages the generator builds use their own family of classes. Style these to theme the auto-documentation a system ships with:

ClassTargets
.isdl-journal-sectionA journal section wrapper.
.isdl-journal-entryA single journal entry; sub-parts: .isdl-journal-header, -icon, -title, -badge, -description, -condition, -condition-label, -technical-info, -death-note, -info-box, -info-title, -info-list.
.isdl-keyword-entryA keyword entry (extends .isdl-journal-entry).
.isdl-damage-type-entryA damage-type entry (extends .isdl-journal-entry).
.isdl-status-effect-entryA status-effect entry (extends .isdl-journal-entry); .isdl-status-effect-image is its icon.

Chat-card classes ​

Chat cards are not part of the sheet, so they can only be styled through globalStyles (see Sidecar stylesheets). They mostly use standard Foundry classes plus a few generated ones:

ClassTargets
.standard-chat-card / .chat-cardThe generated chat card root.
.card-header / .chat-headerThe card's header band.
.dice-roll / .dice-result / .dice-total / .dice-infoThe dice-result block of a roll card.

State / modifier classes ​

ISDL toggles edit-mode, disabled, and locked behaviour through reactive component props in JavaScript, not by adding CSS classes — so there is no .edit-mode, .disabled, or .locked class to target on a field's live state. The only state-ish classes are the component-internal ones noted above: .calculated-number (a number whose value is derived), .no-mod (an attribute with no modifier), .active (an engaged damage modifier in a chat card), and .damage-box--filled / .damage-box--empty (damage-track pips). To react to play vs. edit mode, theme via the disabledText { } token instead.

For declared visibility, use the isdl-visibility-* classes above — these are the stable CSS hooks for "this field is designed to be GM-only" etc.

Worked example ​

scss
// sheetStyles.scss — theme by stable class, no generated-markup guesswork
.isdl-field-xp {                       // one specific field, by name
    font-weight: 700;
    letter-spacing: 0.04em;
}

.isdl-number .v-field {                // every number field's input
    border-radius: 10px;
}

.isdl-section-attributes .v-card {     // one section's visible card box
    box-shadow: 0 0 12px var(--isdl-primary, #0003);
}

.section .v-card-title {               // every section header
    border-bottom: 1px solid var(--isdl-primary, #888);
}

.isdl-datatable {                      // every generated table
    border-radius: 6px;
}

CSS variable reference ​

Every token compiles to a --isdl-* custom property on the sheet root (global) or the .isdl-field-<name> wrapper (per-field). These are the names to reference from a sidecar stylesheet or custom.css:

TokenCSS variable
primary--isdl-primary
secondary / tertiary--isdl-secondary / --isdl-tertiary
background--isdl-background
text--isdl-text
border { color }--isdl-border
border { width }--isdl-border-width
border { radius }--isdl-radius
font { family }--isdl-font
font { size }--isdl-font-size
heading { color / font / size / transform }--isdl-heading-color / -font / -size / -transform
disabledText { color / size }--isdl-disabled-color / --isdl-disabled-size
width { min / max }--isdl-width-min / --isdl-width-max
height { min / max }--isdl-height-min / --isdl-height-max

Every field also carries two stable classes you can target: .isdl-field (on every field) and .isdl-field-<name> (the specific field, lowercased).

Note

A token's variable only holds the value you set in theme { }. The color tokens --isdl-primary, --isdl-text, and --isdl-disabled-color are always defined (they carry a sensible default until themed); the rest are undefined until set, and the generated styles supply their default via a var(token, fallback) fallback. So when you reference a token from a sidecar or custom.css, include your own fallback for the ones that may be unset — e.g. var(--isdl-border, #ccc).


See also ​

  • Config — where theme, styles, and userColors live
  • Fields — the per-field params that accept theme:
  • Custom Code & Styles — the custom.css / custom.mjs escape hatch and the full styling-tier breakdown