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:
- Theme tokens ā
config { theme { ... } }for system-wide defaults, and a per-fieldtheme: { ... }override. Covered on this page. - Sidecar stylesheets ā
sheetStyles/globalStylesSCSS files for bespoke atmosphere (web fonts, grain, shadows, chat cards) a token can't express. Below. 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.
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 ā
| Token | Sets | Notes |
|---|---|---|
primary: #hex | The system's lead color | Paints the field highlight edge; also the default for the per-document color picker. |
secondary: #hex | Picker seed | Default for the secondary color picker (whole-sheet). |
tertiary: #hex | Picker seed | Default for the tertiary color picker (whole-sheet). |
background: #hex | The sheet surface color | Paints the whole sheet (window + Vuetify app + page texture). |
text: #hex | Body text color | Propagates 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.
| Block | Properties | Styles |
|---|---|---|
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:
border { width: 2, radius: 6 } // 2px, 6px
width { min: 120px, max: 240px } // explicit pxNote
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.)
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:
// 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:
| Tokens | Allowed 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.
// ā 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.
// 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:
| Tokens | row / column | section |
|---|---|---|
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.)
// ā
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:
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:
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:
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, ā¦)
}| Option | Scoped under | Use for |
|---|---|---|
sheetStyles | .<id>.vue-application | The 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:
// 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.
| Element | Always-on classes | Named class | Notes |
|---|---|---|---|
| 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>. |
// 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 columnNote
<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:
<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 keyword | Type class | Notes |
|---|---|---|
number | .isdl-number | A calculated/derived number additionally puts .calculated-number on its inner input. |
string | .isdl-string | |
html | .isdl-html | The ProseMirror rich-text editor block. |
boolean | .isdl-boolean | |
attribute | .isdl-attribute | The 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-money | Denomination 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-track | Individual pips carry .damage-box--filled / .damage-box--empty. |
bonuses | .isdl-bonuses | |
resistances | .isdl-resistances | |
rollVisualizer | .isdl-roll-visualizer | Sub-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) ā
| Class | Targets |
|---|---|
.isdl-field | Every 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 visibility | CSS 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 |
// 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 ā
| Class | Targets |
|---|---|
.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-row | A row container (<v-row>). Rows are unnamed ā no per-row class. |
.isdl-column | A column container (<v-col>). Columns are unnamed. |
.single-wide / .double-wide | Width hint applied to most field roots ā .double-wide fields (choices, resistances, etc.) span two grid columns. |
.flexrow / .flexcol | Generic horizontal / vertical flex helpers used inside composite fields. |
.tabs-window | The tab content window (<v-tabs-window>) on a sheet. |
.tabs-container | A single tab's content pane (<v-tabs-window-item>). Carries data-tab / data-type attributes you can also select on. |
.resource-card | The 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-wrapper | The editing surface inside an html / paragraph field. |
Datatable classes ā
| Class | Targets |
|---|---|
.isdl-datatable | A generated DataTable (table, inventory, effects, pinned lists). |
.datatable | Legacy alias also present on datatable roots. |
.datatable-drop-zone | The drop target wrapping a table/inventory field. The universal .isdl-field-<name> marker merges onto this <div>. |
.isdl-inventory | An inventory field's container. |
.isdl-image-action | A 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:
| Class | Targets |
|---|---|
.isdl-journal-section | A journal section wrapper. |
.isdl-journal-entry | A 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-entry | A keyword entry (extends .isdl-journal-entry). |
.isdl-damage-type-entry | A damage-type entry (extends .isdl-journal-entry). |
.isdl-status-effect-entry | A 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:
| Class | Targets |
|---|---|
.standard-chat-card / .chat-card | The generated chat card root. |
.card-header / .chat-header | The card's header band. |
.dice-roll / .dice-result / .dice-total / .dice-info | The 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 ā
// 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:
| Token | CSS 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, anduserColorslive - Fields ā the per-field params that accept
theme: - Custom Code & Styles ā the
custom.css/custom.mjsescape hatch and the full styling-tier breakdown