Theme authoringUniversal Theming Dimensions

Universal Theming Dimensions

Traditional theme development requires per-rune CSS for every rune in the ecosystem. A recipe's difficulty badge, a character's status badge, and a work item's priority badge all need separate styles — even though they're conceptually the same thing.

Universal theming dimensions solve this. A handful of semantic data attributes describe what something is, not how it should look. The identity transform emits these attributes automatically from the rune config. A theme writes generic CSS rules targeting these attributes, and every rune — core, community, or custom — is styled.

Overview

DimensionAttributeValuesControlsDeclared by
Meta typedata-meta-typestatus, category, quantity, temporal, tag, idTypography hints (monospace, tabular-nums)MetaField.metaType (or legacy StructureEntry.metaType)
Sentimentdata-meta-sentimentpositive, negative, caution, neutralBadge / value colorMetaField.sentimentMap (or legacy StructureEntry.sentimentMap)
Zone layoutdata-zone-layoutbar, definition-listGeometric shape of a metadata block (flex row, dt/dd grid)A block's layout primitive in blocks (SPEC-080)
Densitydata-densityfull, compact, minimalSpacing and detail levelRuneConfig.defaultDensity + context
Sectiondata-sectionheader, preamble, title, description, body, footer, mediaStructural anatomyRuneConfig.sections
Mediadata-mediaportrait, cover, thumbnail, hero, iconImage treatmentRuneConfig.mediaSlots
Checklistdata-checkedchecked, unchecked, active, skippedCheckbox list itemsRuneConfig.checklist
Sequencedata-sequencenumbered, connected, plainOrdered item indicatorsRuneConfig.sequence
Statedata-stateopen, closed, active, inactive, selected, disabledInteractive statesBehavior scripts
Surface(class-based)card, inline, banner, insetContainer treatmentTheme only

The first three style metadata content. Meta type and sentiment describe the field itself; zone layout describes the geometric shape around groups of fields. The remaining dimensions style the rune's structure, content, and behavior.

Type-vs-layout split. data-meta-type is typography only — it controls monospace for id / code, tabular nums for quantity / temporal, primary color for id, and nothing else. The geometric shape (chip pill, plain text, def-list cell) comes from data-zone-layout and the universal .rf-badge class. The same field renders as a chip in one block and as plain text in another with no per-field config change — the field's metaType and decorations decide its intrinsic shape, the block's layout primitive decides the surrounding geometry.


Metadata dimensions

Metadata badges — status indicators, categories, durations, tags — appear across dozens of runes. The metadata system provides three dimensions so themes can style every badge generically.

Declaring metadata — metaFields, blocks, layout (SPEC-080)

Runes declare their meta-bearing fields via the metaFields manifest on RuneConfig. Each entry is pure data — no rendering hints. Named blocks project fields from the manifest into a layout primitive, and the layout map places every child explicitly. See Blocks & layout for the full model.

Recipe: {
  block: 'recipe',
  modifiers: {
    difficulty: { source: 'meta', default: 'medium' },
    prepTime: { source: 'meta' },
    servings: { source: 'meta' },
    tags: { source: 'meta', noBemClass: true },
  },
  metaFields: {
    prepTime:   { metaType: 'temporal', label: 'Prep',     tag: 'time', condition: 'prepTime' },
    servings:   { metaType: 'quantity', label: 'Serves',   condition: 'servings' },
    difficulty: { metaType: 'category', label: 'Difficulty',
                  sentimentMap: { easy: 'positive', medium: 'neutral', hard: 'caution' } },
    tags:       { metaType: 'tag',      label: 'Tags',     condition: 'tags', splitOn: ',' },
  },
  blocks: {
    metadata: { fields: ['prepTime', 'servings', 'difficulty'], layout: 'definition-list' },
    tags:     { fields: ['tags'], layout: 'bar' },
  },
  layout: { root: ['eyebrow', 'title', 'blurb', 'metadata', 'tags', 'body'] },
}

MetaField fields

FieldTypeDescription
metaType'status' | 'category' | 'quantity' | 'temporal' | 'tag' | 'id' | 'code'Semantic kind; drives intrinsic render shape and typography. Emits data-meta-type
sentimentMapRecord<string, 'positive' | 'negative' | 'caution' | 'neutral'>Maps the field's resolved value to a sentiment. Emits data-meta-sentiment when matched — color only, never changes shape
labelstringHuman-readable label. Rendered as <dt> in a definition-list, and as link / icon text where applicable; bar fields are unlabelled
conditionstringField renders only when the named modifier has a truthy value
renderWhenEmptybooleanLoosen condition to test presence instead of truthiness — the field renders when its modifier is defined, even if the value is "". Lets an empty-but-present value still project a block (e.g. codegroup title="" renders the window chrome without a filename)
hrefstringRender the field as a link; the named modifier holds the URL. Renders bare (no chip)
rating{ total?: string }Render the field as a rating widget; the value is the filled count, total names the modifier holding the max (default 5)
icon{ group: string }Decorate with a leading icon; the field's value selects the glyph within group
tagstringElement tag override. Default span; use time for temporal fields so the engine emits <time datetime="…">…</time>
splitOnstringTreat the value as a delimited collection — split on this character, render one element per item. Used for tags-style fields
transform'duration' | 'uppercase' | 'capitalize'Value transform applied before rendering

Legacy StructureEntry fields

StructureEntry children may still declare metadata inline via metaType / sentimentMap — the engine sets the matching data-meta-* attributes. Prefer metaFields for new runes; it's the supported projection path.

Removed in v0.18.0 (WORK-313): the slots array + slot-based assembly, and the automatic universal .rf-badge class on meta-typed StructureEntry children. A StructureEntry that should render as a chip must now set its own class via attrs. The before/after structure assembly itself is unchanged.

FieldTypeDescription
metaType'status' | 'category' | 'quantity' | 'temporal' | 'tag' | 'id'Same semantics as MetaField.metaType
sentimentMapRecord<string, 'positive' | 'negative' | 'caution' | 'neutral'>Same semantics as MetaField.sentimentMap

How the engine emits them

When a field renders as a chip (metaType is status, category, or tag), the engine emits class="rf-badge" plus the meta attributes:

<!-- A recipe badge for difficulty="easy" -->
<span class="rf-badge"
      data-meta-type="category"
      data-meta-sentiment="positive">easy</span>

For bare values (id, quantity, temporal, code, or no metaType), the chip class is omitted and only typography hints carry through:

<!-- A complexity value in a def-list <dd> -->
<dd data-meta-type="quantity">moderate</dd>

When no sentimentMap is provided, or the current value has no mapping, data-meta-sentiment is omitted and the value renders neutrally.

Meta type CSS

Each type gets typography only — monospace for id / code, tabular-nums for quantity / temporal, primary color for id, nothing else for the rest. Geometry (chip pill, definition list grid, bar row) lives on [data-zone-layout] selectors.

[data-meta-type="id"],
[data-meta-type="code"] {
  font-family: var(--rf-font-mono, monospace);
}

[data-meta-type="id"] {
  color: var(--rf-color-primary);
}

[data-meta-type="quantity"],
[data-meta-type="temporal"] {
  font-variant-numeric: tabular-nums;
}

The chip primitive (universal .rf-badge) supplies the pill shape, sentiment-tinted background, and compact padding. Layout selectors supply the surrounding arrangement (bar row, def-list grid).

Sentiment CSS

Sentiment maps to color through a --meta-color custom property:

[data-meta-sentiment="positive"] { --meta-color: var(--rf-color-success, #10b981); }
[data-meta-sentiment="negative"] { --meta-color: var(--rf-color-danger, #ef4444); }
[data-meta-sentiment="caution"]  { --meta-color: var(--rf-color-warning, #f59e0b); }
[data-meta-sentiment="neutral"]  { --meta-color: var(--rf-color-muted, #64748b); }

/* Colored dot indicator */
[data-meta-sentiment]::before {
  content: '';
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--meta-color);
}

The --meta-color property cascades into the type rules. A status pill with data-meta-sentiment="positive" gets a green dot because --meta-color resolves to --rf-color-success.

Zone layout CSS

The two layout primitives carry their own geometry:

/* Horizontal flex row of fields, each in its intrinsic shape */
[data-zone-layout="bar"] {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
}

/* A field opting into align: 'end' is pushed (with everything after it) right */
[data-zone-layout="bar"] [data-align="end"] {
  margin-left: auto;
}

/* wrap: false keeps the row on one line */
[data-zone-layout="bar"][data-wrap="false"] {
  flex-wrap: nowrap;
}

/* Stacked dt/dd pairs flowing into multi-column at wider widths */
[data-zone-layout="definition-list"] {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.75rem 1.5rem;
}

@media (min-width: 48rem) {
  [data-zone-layout="definition-list"] {
    grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  }
}

See Blocks & layout for the complete DOM contract each primitive emits.

Labels

MetaField (or legacy StructureEntry) entries can include a label field. In definition-list it becomes the <dt>; in a bar it's ignored (bar fields are unlabelled, eyebrow-style, by contract). It is also used as the text for href links and icon-decorated fields.

prepTime: { metaType: 'temporal', label: 'Prep', tag: 'time', condition: 'prepTime' }

For legacy StructureEntry-only paths, labelHidden: true makes the label visually hidden but accessible to screen readers — useful when the value is self-explanatory (like an ID badge where "WORK-042" doesn't need a visible "ID:" prefix).


Structural dimensions

Density

Controls how much detail a rune shows. Three levels:

ValueUse caseWhat's visible
fullDedicated pageAll sections, generous spacing
compactGrid cell, cardDescriptions truncated (2 lines), secondary metadata hidden
minimalList view, backlog rowTitle and primary metadata only

Declared on RuneConfig:

Accordion: { block: 'accordion', defaultDensity: 'full' }
Details: { block: 'details', defaultDensity: 'compact' }
Breadcrumb: { block: 'breadcrumb', defaultDensity: 'minimal' }

Resolution order: author attribute > rendering context > config default > 'full'

The engine automatically applies context densities: runes inside a Grid or Bento get compact; runes inside a backlog or decision-log get minimal.

CSS example:

[data-density="full"] { --rune-padding: var(--rf-spacing-md); }
[data-density="compact"] { --rune-padding: var(--rf-spacing-sm); }
[data-density="minimal"] { --rune-padding: var(--rf-spacing-xs); }

/* Compact: truncate descriptions */
[data-density="compact"] [data-section="description"] {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Minimal: hide everything except title and primary metadata */
[data-density="minimal"] [data-section="description"],
[data-density="minimal"] [data-section="body"],
[data-density="minimal"] [data-section="footer"] {
  display: none;
}

Density interacts with other dimensions — compact hides secondary metadata, minimal hides media slots.

Sections

Maps structural elements to standard anatomical roles. The engine emits data-section on elements whose data-name matches a key in the sections map.

Declared on RuneConfig:

Budget: {
  block: 'budget',
  sections: { header: 'header', title: 'title', footer: 'footer' },
}

Hero: {
  block: 'hero',
  sections: {
    preamble: 'preamble',
    headline: 'title',
    blurb: 'description',
    content: 'body',
  },
}

Roles and their CSS:

RoleTypical contentDefault styling
headerChrome row (badges, status)Flex wrap, gap
preambleIntro block (eyebrow + headline + blurb)Flex column
titlePrimary heading1.5rem, bold, scales with density
descriptionSecondary textMuted color, truncated in compact
bodyMain contentStandard line height
footerActions, linksFlex wrap, top border
mediaImages, videoStandard margin
[data-section="header"] {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}

[data-section="title"] {
  font-size: 1.5rem;
  font-weight: 700;
  line-height: 1.2;
}

[data-section="footer"] {
  display: flex;
  flex-wrap: wrap;
  gap: var(--rf-spacing-sm);
  border-top: 1px solid var(--rf-color-border);
}

Media slots

Maps image and media elements to treatment types.

Declared on RuneConfig:

Figure: {
  block: 'figure',
  mediaSlots: { media: 'cover' },
}

Slot types:

ValueTreatmentSize
portraitCircular crop, 1:1 aspect5rem default
coverFull-width, top-rounded100% width
thumbnailSmall fixed square3rem default
heroFull-width responsive100% width
iconSmall contained2rem default
[data-media="portrait"] {
  border-radius: var(--rf-radius-full);
  aspect-ratio: 1 / 1;
  object-fit: cover;
  width: var(--media-portrait-size, 5rem);
}

[data-media="cover"] {
  width: 100%;
  object-fit: cover;
  border-radius: var(--rf-radius-md) var(--rf-radius-md) 0 0;
}

Density interactions: compact reduces portrait size to 3rem; minimal hides all media.

Checklist

Detects checkbox markers in list items and styles them.

Declared on RuneConfig:

Work: { block: 'work', checklist: true }

When checklist: true, the engine scans <li> text for markers and emits data-checked:

Markerdata-checked valueMeaning
[x]checkedCompleted
[ ]uncheckedNot done
[>]activeIn progress
[-]skippedSkipped

The marker text is stripped from the output. CSS provides visual indicators:

[data-checked]::before {
  content: '';
  position: absolute;
  width: 1rem;
  height: 1rem;
  border-radius: var(--rf-radius-sm);
  border: 2px solid var(--rf-color-border);
}

[data-checked="checked"]::before {
  background: var(--rf-color-success);
  border-color: var(--rf-color-success);
}

[data-checked="active"]::before {
  border-color: var(--rf-color-primary);
  background: var(--rf-color-primary);
}

[data-checked="skipped"] {
  text-decoration: line-through;
  color: var(--rf-color-muted);
}

Sequence

Styles ordered lists with visual indicators.

Declared on RuneConfig:

Steps: { block: 'steps', sequence: 'connected' }
HowTo: { block: 'howto', sequence: 'numbered' }
ValueVisual treatment
numberedCounter circles to the left of each item
connectedVertical line with dots (or horizontal with data-sequence-direction="horizontal")
plainNo visual indicators

Optional direction control:

Timeline: {
  block: 'timeline',
  sequence: 'connected',
  sequenceDirection: { fromModifier: 'direction', default: 'vertical' },
}

Interactive state

Set by behavior scripts at runtime, not by rune config. The engine doesn't emit these — @refrakt-md/behaviors toggles data-state on interactive runes.

ValueEffect
openShow body/content with expand animation
closedHide body/content
activeBottom border + primary color (tabs, toggles)
inactiveTransparent border + muted color
selectedLight background overlay + primary outline
disabledFaded (0.4 opacity), non-interactive
[data-state="closed"] > [class*="__body"],
[data-state="closed"] > [class*="__content"] {
  display: none;
}

[data-state="open"] > [class*="__body"],
[data-state="open"] > [class*="__content"] {
  display: block;
  animation: rf-expand 0.2s ease-out;
}

Surface

Surface is theme-only — runes don't declare their surface type. Which runes render as cards, banners, or inline elements is a visual design decision that belongs to the theme.

Lumina groups runes into four surface types:

SurfaceTreatmentExample runes
CardBackground, radius, paddingrecipe, character, event, api, howto
InlineNo visual boundary, vertical padding onlyhint, details, sidenote, nav, breadcrumb
BannerFull-width paddinghero, cta, feature, steps, bento
InsetBackground, radius, paddingcodegroup, mockup, diagram, chart, gallery
/* Card surface */
.rf-recipe, .rf-character, .rf-event, .rf-api, .rf-howto {
  background: var(--rf-color-surface);
  border-radius: var(--rf-radius-md);
  padding: var(--rune-padding, var(--rf-spacing-md));
}

/* Inline surface */
.rf-hint, .rf-details, .rf-sidenote, .rf-nav {
  padding: var(--rune-padding, var(--rf-spacing-sm)) 0;
}

All surfaces consume the --rune-padding variable set by the density dimension, so padding automatically scales when density changes.


Dimension interactions

Dimensions compose naturally. Key interactions:

  • Density x Sections — compact truncates descriptions to 2 lines; minimal hides description, body, and footer
  • Density x Metadata — compact and minimal can hide a metadata block entirely or collapse a definition-list block to a bar
  • Density x Media — compact shrinks portraits; minimal hides all media
  • Density x Sequence — compact tightens spacing; minimal removes indicators
  • Surface x Density — surfaces consume --rune-padding which density sets
  • Sentiment x Meta type — sentiment sets --meta-color which type rules consume for dot color, border color, etc.

CSS file organization

Dimension CSS lives in a dedicated directory, separate from per-rune CSS:

styles/
├── dimensions/
   ├── metadata.css      # data-meta-type, data-meta-sentiment, data-zone-layout
   ├── density.css        # data-density
   ├── sections.css       # data-section
   ├── state.css          # data-state
   ├── media.css          # data-media
   ├── surfaces.css       # Surface type groupings
   ├── checklist.css      # data-checked
   └── sequence.css       # data-sequence
└── runes/
    ├── hint.css           # Per-rune overrides (only when needed)
    └── ...

Import dimension CSS in your theme's entry point:

/* index.css */
@import './tokens/base.css';
@import './tokens/dark.css';
@import './styles/dimensions/metadata.css';
@import './styles/dimensions/density.css';
@import './styles/dimensions/sections.css';
@import './styles/dimensions/state.css';
@import './styles/dimensions/media.css';
@import './styles/dimensions/surfaces.css';
@import './styles/dimensions/checklist.css';
@import './styles/dimensions/sequence.css';
@import './styles/runes/hint.css';
/* ... per-rune CSS for overrides */

The dimension layer handles the generic baseline. Per-rune CSS files only need to cover rune-specific styling that the dimensions don't handle (e.g., Hint's colored left border, Nav's tree layout).


Community package benefits

A plugin author declares dimensions on their rune config and gets themed automatically:

// @refrakt-community/wine
WineTasting: {
  block: 'wine-tasting',
  defaultDensity: 'full',
  modifiers: {
    vintage: { source: 'meta' },
    rating: { source: 'meta' },
    varietal: { source: 'meta' },
  },
  metaFields: {
    vintage:  { metaType: 'temporal', label: 'Vintage', tag: 'time' },
    rating:   { metaType: 'quantity', label: 'Rating',
                sentimentMap: { '90+': 'positive', '80-89': 'neutral', '<80': 'caution' } },
    varietal: { metaType: 'tag',      label: 'Varietal' },
  },
  blocks: {
    metadata: { fields: ['vintage', 'rating', 'varietal'], layout: 'definition-list' },
  },
  layout: { root: ['title', 'metadata', 'body'] },
  mediaSlots: { label: 'thumbnail' },
}

Without any theme-specific CSS for this rune:

  • The vintage renders as bare temporal text (tabular nums) in its <dd>
  • The rating renders as bare quantity text, tinted by its sentiment color
  • The varietal renders as a tag chip (.rf-badge)
  • Sections get standard layout (header flex-row, title bold, body standard)
  • The label image gets thumbnail treatment
  • In a grid, density drops to compact automatically

Dark mode

Dimension CSS references design tokens (--rf-color-success, --rf-color-border, etc.). When dark mode tokens override these values, all dimension styling updates automatically. No dimension-specific dark mode CSS needed.

/* Light mode tokens */
:root {
  --rf-color-success: #10b981;
  --rf-color-danger: #ef4444;
  --rf-color-warning: #f59e0b;
}

/* Dark mode overrides — dimensions adapt automatically */
[data-theme="dark"] {
  --rf-color-success: #34d399;
  --rf-color-danger: #f87171;
  --rf-color-warning: #fbbf24;
}