Repository navigation
design components
Audience: module authors building UI that matches Blacksmith.
The shared component kit: what each component is called, which stylesheet defines it, and the structure its CSS requires.
Tokens are on the Design tokens page. Naming rules and file organization are on the Design patterns page.
Class names below are the contract. Where a component's markup structure is load-bearing -- where getting the element types or nesting wrong yields a control that renders unstyled or does not respond to clicks -- the required structure is stated and the live template is cited. For everything else, read the cited template rather than copying a snippet from here.
The card system is the most widely reused surface. styles/cards-common-layout.css defines structure,
styles/cards-common-themes.css defines color only.
Root element is .blacksmith-card (styles/cards-common-layout.css:41) plus one theme class. Themes are
matched generically by .blacksmith-card[class*="theme-"] (:63), so a theme class is required for
themed styling to apply.
| Region | Class | Defined at |
|---|---|---|
| Header | .card-header |
cards-common-layout.css:82 |
| Section header | .section-header |
cards-common-layout.css:110 |
| Section subheader | .section-subheader |
cards-common-layout.css:127 |
| Body content | .section-content |
cards-common-layout.css:148 |
| Dark section | .section-dark |
cards-common-layout.css:156 |
| Button row | .blacksmith-card-buttons |
cards-common-layout.css:306 |
| Button | .card-button |
cards-common-layout.css:320 |
| Data table | .section-table |
cards-common-layout.css:258 |
| Table label cell | .row-label |
cards-common-layout.css:269 |
| Table value cell | .row-content |
cards-common-layout.css:290 |
| Actor block | .container-user |
cards-common-layout.css:570 |
| Actor portrait | .token-image |
cards-common-layout.css:578 |
Collapsible sections carry state classes: .section-content.collapsed (:165),
.card-header:not(.collapsed) (:94), and .card-header.collapsible (:238).
The nine themes are defined at styles/cards-common-themes.css:11, 29, 48, 65, 82, 100, 118, 136, 154.
That file sets color only -- no radius, spacing, or typography -- so a new theme needs nothing but color
declarations. Themed cards inherit the base radius; theme classes set no radius of their own.
Chat buttons are identified by a state class and a domain-specific data attribute, not by a generic
data-action. See templates/cards-common.hbs:247 for the live shape.
Twelve tiling background classes at styles/cards-common-layout.css:510-521, backed by
images/tiles/*.webp. The shared rule sets both background-repeat: repeat (:522) and
background-size: cover (:523); cover wins, so these render scaled rather than tiled.
.token-background-themecolor (:525-527) is transparent and defers to the theme.
Defined in styles/menubar.css, template templates/menubar.hbs.
The bar is position: fixed at z-index: 100 (styles/menubar.css:111, 115), offset by
--blacksmith-menubar-interface-offset (:16). Container is .blacksmith-menubar-container, which gains
.has-secondary when a secondary bar is present (:110, 106). Tools live in
.blacksmith-menubar-middle-tools (:173), with .menubar-overflow (:301) handling spill. State
classes are .button-active, .tool-readonly, .tool-active (:211, 212, 244).
Dividers are a modifier class on a single element, not a wrapper with a child. Write
<div class="menu-divider line"></div> -- see templates/menubar.hbs:36. The style variants are
.menu-divider.line, .dotted, .double, .groove, .ridge (styles/menubar.css:189, 192, 195, 198, 201). A nested-child structure produces an unstyled divider.
Secondary bars are .blacksmith-menubar-secondary with a data-bar-type attribute
(templates/menubar.hbs:94-95), holding .secondary-bar-toolbar (styles/menubar.css:622) and zones
.secondary-bar-zone{,-left,-middle,-right} (:633, 639, 643).
Within a zone, items are wrapped in a group rather than placed directly:
.secondary-bar-group-container, -banner, -banner-text, -items, and -divider
(templates/partials/menubar-secondary-default.hbs:6, 8, 9, 12, 91). Items are .secondary-bar-item,
which takes .active (:74). There are four item kinds -- text, image, progressbar, and balancebar. The
image kind puts .secondary-bar-item-image on the <img> itself (:81), not the wrapper; text values use
.secondary-bar-item-value (:26, styled at styles/menubar.css:727). Progressbar and balancebar have
their own sub-element sets at templates/partials/menubar-secondary-default.hbs:34-46 and :49-70.
Menubar tokens (--blacksmith-menubar-*) are declared locally in styles/menubar.css:16, 21, not in
styles/vars.css.
The unified header is templates/partials/unified-header.hbs, styled in styles/window-skillcheck.css.
It contains one .cpb-dialog-header-content block (:5) holding the icon circle, title section, and
controls. The circle renders the actor's portrait when the caller passes one and the diceIcon glyph
otherwise, so the roll window shows a face and the skill check dialog shows a d20.
| Element | Class | Defined at |
|---|---|---|
| Sticky wrapper | .cpb-dialog-header-sticky |
window-skillcheck.css:42 |
| Header | .cpb-dialog-header |
window-skillcheck.css:50 |
| Content block | .cpb-dialog-header-content |
window-skillcheck.css:63 |
| Icon circle | .cpb-dialog-dice-icon |
window-skillcheck.css:72 |
| Portrait in the circle | .cpb-dialog-dice-icon .cpb-actor-portrait |
window-skillcheck.css:141 |
| Title section | .cpb-dialog-title-section |
window-skillcheck.css:95 |
| Main title | .cpb-dialog-main-title |
window-skillcheck.css:110 |
| Subtitle | .cpb-dialog-subtitle |
window-skillcheck.css:148 |
| Actor name in subtitle | .cpb-subtitle-actor |
window-skillcheck.css:156 |
| Controls | .cpb-dialog-controls |
window-skillcheck.css:166 |
Title and subtitle are <div> elements, not heading or paragraph tags
(templates/partials/unified-header.hbs:25, 26). The subtitle carries the actor's name ahead of the roll
type, in .cpb-subtitle-actor.
.cpb-actor-name no longer appears in header markup. It is now only the skill check dialog's actor-list
item (templates/window-skillcheck.hbs:53), styled unscoped at styles/window-skillcheck.css:636. That
selector being unscoped used to reach the dialog header as well, which is worth remembering if header
markup ever reintroduces the class.
Defined at styles/window-template.css:213-259. Variants are the base button plus secondary and critical
modifiers. The shared rule sets padding: 8px 12px, border-radius: 4px, uppercase text,
font-family: inherit, font-size: 1.0em, display: inline-flex, gap: 6px, and
border: 1px solid var(--blacksmith-window-border-color) (:216-224). Secondary and critical each
override border-color (:236, :242), so the border is not uniform across variants. Disabled state is
opacity: 0.6; cursor: not-allowed. No width is set. A shared button class carrying a fixed pixel
width stretched the primary action across a flex footer and squeezed the secondary one onto two lines; the
rule was removed on 2026-08-08, by which point four stylesheets were already resetting it to auto.
Buttons mount into the actionBarLeft and actionBarRight regions
(templates/window-template.hbs:59, 62).
To make buttons fill their footer, put blacksmith-actions-fill on the container — the action bar, or
a window's own footer div. It works on any flex footer, and the window template's action bar gets an extra
rule because its buttons sit one level deeper inside the left and right groups.
The modifier is container-level on purpose: filling is a property of a footer's layout, not of a button.
The per-button version of the same idea is what the deleted width: 300px was, and it failed because a
width belonging to one button squeezes whatever sits beside it. It uses flex: 1 1 0 rather than flex: 1
so buttons come out equal rather than divided in proportion to their label lengths, with min-width: 0 so
a long label ellipsises instead of widening the row.
.blacksmith-tabs / .blacksmith-tab with .is-active, driven by data-action="selectTab" and
data-value (styles/window-tabs.css:23, 30, 54, 60). A disabled tab is opacity: 0.4; cursor: not-allowed (:70-73). The usage comment at styles/window-tabs.css:5-21 carries the current
markup.
Defined in styles/window-form-controls.css.
Toggle switch -- .blacksmith-toggle with -input, -slider, -row, -label (:23, 32, 39, 76, 82). 46x24px, 22px thumb travel (:26-27, 67), accent-colored border when checked (:63). This is the
current toggle for new windows; it lives in the shared stylesheet and consumes tokens.
Range slider -- .blacksmith-slider wrapping an optional icon and value span, with webkit and moz
thumb pseudo-elements (:153-230).
Badge / tag pill -- base plus four variants (:100, 114, 121, 127, 133). The base uses
var(--blacksmith-radius-pill, 16px) (:107).
A second, older toggle ships alongside the current one: .cpb-toggle-container, .cpb-toggle-label,
.cpb-toggle, .cpb-toggle-slider (styles/window-skillcheck.css:241, 249, 257, 271). It is 35x14px,
uses no tokens, and remains in use at templates/partials/unified-header.hbs:33-59 and
templates/window-skillcheck.hbs:76-80. Prefer .blacksmith-toggle for new work.
Its structure is load-bearing. The CSS selector is .cpb-toggle input:checked + .cpb-toggle-slider
(styles/window-skillcheck.css:298), which requires the slider to be the checkbox's immediate sibling,
and the wrapper is a <div> -- so the click target is the slider's for attribute, which needs a matching
id on the input:
<div class="cpb-toggle-container">
<span class="cpb-toggle-label">Cinematic</span>
<div class="cpb-toggle">
<input type="checkbox" name="isCinematic" id="isCinematic">
<label for="isCinematic" class="cpb-toggle-slider"></label>
</div>
</div>Wrapping in a <label> or using a <span> slider yields a toggle that does not respond to clicks. Live
markup: templates/partials/unified-header.hbs:35-38.
styles/window-list.css defines .blacksmith-list, -row, -row-img, -row-img-lg, -row-main,
-row-title, -row-meta, -row-action, and .blacksmith-list-empty (:26-116), with .is-active
(:50). Base thumbnail is 36px (:57-59); -row-img-lg only overrides width and height to 48px
(:67-70), so it is applied in addition to -row-img, not instead of it.
styles/window-panels.css defines five structural classes and eight variant modifiers (:26-125). Each
variant reads its matching --blacksmith-variant-* border and background pair (:87-125). This is
distinct from .blacksmith-window-section; see the source comment at :9.
Detail rows and stat tiles are at :146-233. The detail label is flex: 0 0 110px (:160). A stat tile
is flex: 1 1 0 with min-width: 3rem (:205-206); its label is 0.65em uppercase accent (:221-225)
and its value is 1.2em at font-weight: 900 in rgba(232, 232, 232, 0.98) (:231-233).
.blacksmith-body-columns with -2, -3, or -4 (styles/window-template.css:318-352), placed inside
.blacksmith-window-template-body (templates/window-template.hbs:50). Column widths default to 1fr
and are overridable per column via --blacksmith-body-col-N. The 1px gap reads as a divider only
because children set background: var(--blacksmith-window-background, #232323) (:328) over the parent
background -- that background is load-bearing for anyone reimplementing the layout. Children set
overflow: hidden.
.blacksmith-entity-list containing .blacksmith-entity rows, each a <label> wrapping a native radio or
checkbox plus .blacksmith-entity-img, .blacksmith-entity-body (-name, -type, -reason), and optional
.blacksmith-entity-badges (styles/entity-list.css). Selection styling follows :has(input:checked)
rather than a JS-applied class, so it cannot drift from the inputs. Surfaces read --blacksmith-tool-* with
fallbacks, so a list inside a Tool window inherits that shell. Markup is generated — see api-entity-list.md.
.blacksmith-quantity-split — a three-column grid of .blacksmith-quantity-split-value (Give), a native
input[type="range"], and a second value box (Keep), with .blacksmith-quantity-split-caption labels
beneath (styles/window-form-controls.css). Keep is always max - give. Same Tool-variable inheritance as
the entity list. Markup is generated — see api-quantity-split.md.
.tooltip-item wrapping .tooltiptext (styles/window-common.css:485, 499). 250px wide, positioned
above via bottom: 100% (:676-680), 0.75s delay (:663-667), #3b3b3b on #ffffff (:500-501).
Optional .tooltip-label (:491) and .tooltip-divider (:494) structure the content.
.panel-drop-zone containing .drop-zone-content with an icon (styles/common.css:86, 101, 109). The
.dragover state sets background-color: rgba(0, 100, 0, 0.2) and border-color: #0f0 (:96-98).
Overlay is .blacksmith-pins-overlay with data-hidden (styles/pins.css:19, 31), created at
scripts/pins-renderer.js:83-84. Each pin is .blacksmith-pin (styles/pins.css:36) containing
.blacksmith-pin-icon (scripts/pins-renderer.js:358).
data-shape on the pin takes circle, square, rectangle, or none (styles/pins.css:107, 115, 120).
data-icon-type takes fa, text, or image and belongs on the icon child, not the pin
(styles/pins.css:145, 160, 170). data-no-shadow suppresses the drop shadow (:101). A pin carries its
own hidden state at .blacksmith-pin[data-hidden="true"] (:351), separate from the overlay's.
Permission is blacksmithAccess (gm, private, public) and visibility is blacksmithVisibility
(visible, hidden) -- scripts/pins-renderer.js:1239-1316. The GM indicator is
.blacksmith-pin-gm-indicator (:245-253), showing user-shield for GM-only and user-pen for private
(scripts/pin-permission-icons.js:7-8).
Sizing is written inline as width/height at scripts/pins-renderer.js:510-511. The per-pin custom
properties that stylesheets actually read are --pin-stroke-color, --pin-stroke-px, and
--gm-indicator-size-px (:514-519), plus --blacksmith-pin-icon-size-ratio (styles/pins.css:9).
--pin-size-px is set at :512 but read by no stylesheet, so overriding it has no effect.
.combat-timer-container > .combat-timer-progress > .combat-timer-bar, plus .combat-timer-text
(styles/timer-combat.css:2, 19, 33, 43; templates/timer-combat.hbs:1-4). The text element is a <div>.
State classes are applied by JavaScript at runtime, not present in the template: .high, .medium,
.low, .expired on the bar (:92-95). The progress element carries its own states --
.expired (:98), .player-turn (:101), .other-player-turn (:160) -- and the container takes
.hidden (:14). Turn overlays are .combat-timer-end-turn-overlay (:156) and
.combat-timer-hurry-overlay (:160).
styles/cards-parts.css, part templates/parts/part-gauge.hbs.
A discrete ten-tick scale rather than two proportional segments: ten equal segments, five red then five
green, with a triangular marker placed along the track at the measured position and an icon flanking each
end. It is composed as a gauge -- documentation/api/api-chatcards.md lists the fields -- and a stats
card nests one inside each party member's subject.
This was previously eleven hand-written divs in styles/cards-stats.css with a template of its own. Both
were deleted when the stats cards moved to parts. The gauge part was built from them, so the geometry
carried over rather than being reinvented.
- Ai assistance
- Applicationv2 traps
- CSS traps
- dnd5e conditions
- Documentation standard
- Token actor identity
- Campaign
- Canvas
- Chatcards
- Compendiums
- Contextmenu
- Core
- Create journal entry
- Dialog
- Active Effects
- Entity list
- Equiplocations
- Geography
- GM request
- Gmnotes
- Health
- Hookmanager
- Importer
- Inventory
- Menubar
- Notes
- Party
- Pins
- Quantity split
- Requestroll
- Rolls
- Scene config
- Sockets
- Stats
- Tags
- Toast
- Tokens
- Toolbar
- Window
- Worldclock