Configuration Reference

Theme configuration is declarative. Instead of writing transform logic, you describe what each rune should produce — which BEM block it maps to, what modifiers it reads, what structural elements to inject — and the identity transform engine handles the rest.

ThemeConfig

The top-level configuration object passed to createTransform().

interface ThemeConfig {
  prefix: string;       // BEM prefix, e.g., 'rf' → .rf-hint
  tokenPrefix: string;  // CSS variable prefix, e.g., '--rf' → --rf-color-text
  icons: Record<string, Record<string, string>>;  // SVG icons by group and variant
  runes: Record<string, RuneConfig>;               // Per-rune configuration
}
FieldDescription
prefixPrepended to all BEM class names. 'rf' produces .rf-hint, .rf-hint--note, .rf-hint__icon
tokenPrefixConvention for CSS custom property naming. Used by documentation and tooling, not the engine itself
iconsSVG strings organized by group and variant. Structural groups (e.g., hint) are used by StructureEntry.icon config to inject icons into rune headers. The global group is used by the {% icon %} content rune to resolve author-chosen icons by name
runesMaps rune typeof values to their transform configuration

RuneConfig

Each entry in runes describes how a single rune type is transformed. All fields except block are optional.

block

The BEM block name, without the prefix. This is the only required field.

Grid: { block: 'grid' }
// → class="rf-grid"

The engine always produces the block class (.rf-grid) and sets data-rune="grid" on the root element.

modifiers

Reads values from meta tags or HTML attributes and produces BEM modifier classes plus data attributes.

modifiers: {
  hintType: { source: 'meta', default: 'note' },
  align: { source: 'meta', default: 'center' },
}
PropertyDescription
source'meta' reads from child <meta property="name" content="value"> tags. 'attribute' reads from the element's own attributes
defaultFallback value when no meta tag or attribute is found

For each modifier with a resolved value, the engine:

  1. Adds a BEM modifier class: .rf-hint--note
  2. Sets a data attribute: data-hint-type="note" (camelCase → kebab-case)
  3. Removes the consumed meta tag from the output

Example: The Hint rune with hintType: 'warning':

<!-- Before transform -->
<div typeof="Hint">
  <meta property="hintType" content="warning">
  <p>Be careful!</p>
</div>

<!-- After transform -->
<div class="rf-hint rf-hint--warning" typeof="Hint" data-hint-type="warning" data-rune="hint">
  <!-- meta tag consumed, structural elements injected -->
  <p>Be careful!</p>
</div>

contextModifiers

Adds a BEM modifier class when the rune is nested inside a specific parent rune. The key is the parent's typeof value; the value is the modifier suffix.

// Hint config
contextModifiers: { 'Hero': 'in-hero', 'Feature': 'in-feature' }

// When Hint is inside a Hero:
// class="rf-hint rf-hint--note rf-hint--in-hero"

This enables context-aware styling — a Hint inside a Hero can be more compact, a CTA inside Pricing can be left-aligned.

staticModifiers

BEM modifier classes that are always applied, regardless of meta tag values. Useful for rune variants that share a block but have fixed differences.

// FeaturedTier shares the 'tier' block but always gets --featured
FeaturedTier: { block: 'tier', staticModifiers: ['featured'] }
// → class="rf-tier rf-tier--featured"

structure

Injects new HTML elements into the rune's output — headers, icons, badges, labels. This is the most powerful declarative feature.

Each entry is keyed by name and defines a StructureEntry:

structure: {
  header: {
    tag: 'div',
    before: true,
    children: [
      { tag: 'span', ref: 'icon', icon: { group: 'hint', variant: 'hintType' } },
      { tag: 'span', ref: 'title', metaText: 'hintType' },
    ],
  },
}

This injects a header div before the rune's content, containing an icon span and a title span. See StructureEntry below for the full API.

contentWrapper

Wraps all content children (non-structural) in a container element.

// Recipe config
contentWrapper: { tag: 'div', ref: 'content' }

// Output: recipe's structural header comes first, then content is wrapped
// <div class="rf-recipe">
//   <div class="rf-recipe__meta">...</div>       ← structure (before)
//   <div class="rf-recipe__content">              ← contentWrapper
//     <ul>ingredients...</ul>
//     <ol>steps...</ol>
//   </div>
// </div>

The ref value becomes the element's data-name, which the engine converts to a BEM element class (rf-recipe__content).

autoLabel

Maps child element tag names (or property attribute values) to data-name attributes. The engine then adds BEM element classes for these.

// AccordionItem config
autoLabel: { name: 'header' }

// A child with tag name "name" gets data-name="header"
// → class="rf-accordion-item__header"
// Details config
autoLabel: { summary: 'summary' }

// The <summary> child gets data-name="summary"
// → class="rf-details__summary"

styles

Maps modifier values to CSS custom properties or inline style declarations. Useful when CSS needs dynamic values that can't be expressed as BEM classes.

Simple form — sets a CSS custom property:

styles: { columns: '--sb-columns' }
// With columns=3: style="--sb-columns: 3"

Template form — interpolates the value into a CSS property:

styles: {
  columns: {
    prop: 'grid-template-columns',
    template: 'repeat({}, 1fr)'
  }
}
// With columns=3: style="grid-template-columns: repeat(3, 1fr)"

Multiple style entries produce semicolon-separated values. Existing inline styles on the tag are preserved.

defaultDensity

Controls how much detail a rune shows by default. The engine emits data-density on the root element.

Accordion: { block: 'accordion', defaultDensity: 'full' }
Details: { block: 'details', defaultDensity: 'compact' }
Breadcrumb: { block: 'breadcrumb', defaultDensity: 'minimal' }
ValueBehavior
'full'All sections visible, generous spacing (default)
'compact'Descriptions truncated, secondary metadata hidden
'minimal'Title and primary metadata only

Resolution order: author attribute > rendering context > config default > 'full'. The engine automatically applies compact inside Grid/Bento/Gallery and minimal inside backlog/decision-log contexts.

See Dimensions for the full CSS patterns and density interactions.

sections

Maps structural ref names (data-name values) to standard section roles. The engine emits data-section on matching elements, enabling generic layout styling.

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

Available roles: 'header', 'preamble', 'title', 'description', 'body', 'footer', 'media'.

See Dimensions for role descriptions and CSS.

mediaSlots

Maps ref names to media treatment types. The engine emits data-media on matching elements.

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

Available types: 'portrait', 'cover', 'thumbnail', 'hero', 'icon'.

See Dimensions for treatment descriptions and CSS.

checklist

When true, the engine scans <li> text for checkbox markers ([x], [ ], [>], [-]), strips the marker, and emits data-checked on the element.

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

See Dimensions for marker values and CSS.

sequence

Ordered list style. The engine emits data-sequence on <ol> elements within the rune.

Steps: { block: 'steps', sequence: 'connected' }
ValueVisual treatment
'numbered'Counter circles
'connected'Vertical line with dots
'plain'No visual indicators

Use sequenceDirection to control orientation:

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

See Dimensions for CSS patterns.

parent

Groups a rune under a parent rune in the block editor palette. Does not affect rendering.

AccordionItem: { block: 'accordion-item', parent: 'Accordion' }

defaultWidth

Sets the default page grid width for the rune. The engine emits data-width on the root element.

Hero: { block: 'hero', defaultWidth: 'full' }
ValueBehavior
'content'Standard content width (default)
'wide'Wider than content, narrower than full
'full'Edge-to-edge

childDensity

Imposes a density level on all nested runes. Useful for runes that display children in a compact context.

Grid: { block: 'grid', childDensity: 'compact' }
Backlog: { block: 'backlog', childDensity: 'minimal' }

editHints

Declares how named sections are edited in the block editor. Keys are data-name values; values are edit mode hints.

Hint: {
  block: 'hint',
  editHints: { icon: 'none', title: 'inline' },
}
HintBehavior
'inline'Inline text editing
'link'Link editing
'code'Code editing
'image'Image picker
'icon'Icon picker
'none'Not editable (generated content)

rootAttributes

Extra attributes set on the root element unconditionally.

Sandbox: { block: 'sandbox', rootAttributes: { 'data-interactive': 'true' } }

projection

Declarative tree reshaping — hide, group, or relocate children before structural injection. This is an advanced feature for runes that need to rearrange their content tree.

Comparison: {
  block: 'comparison',
  projection: {
    hide: ['meta'],
    group: { name: 'items', match: 'ComparisonItem' },
    relocate: { target: 'header', ref: 'title' },
  },
}

slots

Ordered slot names for structure assembly. When present, structure entries are assigned to slots and rendered in slot order, enabling predictable output regardless of config declaration order.

Recipe: {
  block: 'recipe',
  slots: ['meta', 'media', 'content'],
  structure: {
    meta: { tag: 'div', slot: 'meta', order: 0, ... },
    media: { tag: 'div', slot: 'media', order: 1, ... },
  },
}

Advanced modifier options

Modifiers support additional fields beyond source and default:

PropertyDescription
noBemClassWhen true, skips BEM modifier class generation — only the data attribute is emitted
valueMapMaps modifier values before emitting (e.g., { 'GET': 'read', 'POST': 'write' })
mapTargetCustom data attribute name for the mapped value (instead of the default)
modifiers: {
  method: {
    source: 'meta',
    default: 'GET',
    valueMap: { GET: 'read', POST: 'write', PUT: 'write', DELETE: 'danger' },
    mapTarget: 'intent',
  },
}
// Emits: data-method="GET" data-intent="read"

postTransform

A programmatic escape hatch that runs after all declarative processing. Receives the fully transformed node and resolved modifier values.

postTransform(node, ctx) {
  return {
    ...node,
    attributes: {
      ...node.attributes,
      'data-custom': `cols-${ctx.modifiers.columns}`,
    },
  };
}

The ctx object contains:

PropertyTypeDescription
modifiersRecord<string, string>All resolved modifier values
parentTypestring | undefinedThe parent rune's typeof value, if nested
warning

Use declarative config first. postTransform is for edge cases that truly can't be expressed with modifiers, structure, or styles. It makes the config harder to analyze statically — the inspect and audit tools can't introspect programmatic transforms.

StructureEntry

Defines an element to inject into the rune's output via the structure config.

interface StructureEntry {
  tag: string;                // HTML tag name ('div', 'span', 'a', etc.)
  ref?: string;               // Sets data-name (overrides the structure key)
  before?: boolean;           // Insert before content children (default: after)
  children?: (string | StructureEntry)[];  // Nested entries or text

  // Content
  metaText?: string;          // Inject resolved modifier value as text
  icon?: { group: string; variant: string };  // Icon from config.icons

  // Conditional
  condition?: string;         // Only render if named modifier is truthy
  conditionAny?: string[];    // Only render if any named modifier is truthy

  // Text transforms
  transform?: 'duration' | 'uppercase' | 'capitalize';
  textPrefix?: string;        // Prepend to metaText value
  textSuffix?: string;        // Append to metaText value

  // Labels
  label?: string;             // Emits <span data-meta-label>Label</span> child
  labelHidden?: boolean;      // Label visually hidden but accessible (sr-only)

  // Metadata dimensions (see Dimensions page)
  metaType?: 'status' | 'category' | 'quantity' | 'temporal' | 'tag' | 'id';
  metaRank?: 'primary' | 'secondary';
  sentimentMap?: Record<string, 'positive' | 'negative' | 'caution' | 'neutral'>;

  // Slot assignment
  slot?: string;             // Assign to a named slot (see RuneConfig.slots)
  order?: number;            // Sort order within the slot

  // Repetition
  repeat?: {                 // Generate N copies of this element
    count: { fromModifier: string };  // Modifier providing the count
    filled?: { fromModifier: string }; // Modifier providing how many are "filled"
  };

  // Attributes
  attrs?: Record<string, string | { fromModifier: string }>;
}

Structural injection example

The Api rune demonstrates most structure features:

Api: {
  block: 'api',
  contentWrapper: { tag: 'div', ref: 'body' },
  modifiers: {
    method: { source: 'meta', default: 'GET' },
    path: { source: 'meta' },
    auth: { source: 'meta' },
  },
  structure: {
    header: {
      tag: 'div', before: true,
      children: [
        { tag: 'span', ref: 'method', metaText: 'method' },
        { tag: 'code', ref: 'path', metaText: 'path' },
        { tag: 'span', ref: 'auth', metaText: 'auth', condition: 'auth' },
      ],
    },
  },
}

This produces:

<div class="rf-api rf-api--GET" data-method="GET" data-rune="api">
  <div data-name="header" class="rf-api__header">
    <span data-name="method" class="rf-api__method">GET</span>
    <code data-name="path" class="rf-api__path">/api/users</code>
    <!-- auth span only present if auth modifier has a value -->
  </div>
  <div data-name="body" class="rf-api__body">
    <!-- content children -->
  </div>
</div>

Conditional elements

Use condition to only inject an element when a modifier has a truthy value:

{ tag: 'span', ref: 'auth', metaText: 'auth', condition: 'auth' }
// Only renders if the 'auth' modifier is present

Use conditionAny to render when at least one of several modifiers is present:

{
  tag: 'div', ref: 'meta', before: true,
  conditionAny: ['prepTime', 'cookTime', 'servings', 'difficulty'],
  children: [...]
}
// Only renders if any of those modifiers has a value

Dynamic attributes

The attrs field sets attributes on the injected element. Values can be literal strings or references to modifier values:

{
  tag: 'a', ref: 'register', condition: 'url',
  attrs: { href: { fromModifier: 'url' } },
  children: ['Register'],
}
// → <a href="https://example.com" class="rf-event__register">Register</a>

Repeated elements

The repeat field generates multiple copies of a structure element — useful for star ratings, progress dots, and similar patterns.

{
  tag: 'span', ref: 'star',
  repeat: {
    count: { fromModifier: 'maxRating' },
    filled: { fromModifier: 'rating' },
  },
}
// With maxRating=5, rating=3:
// Produces 5 <span> elements, first 3 with data-filled="true"

Each generated element gets data-filled="true" or data-filled="false" based on whether its index is less than the filled count.

Text transforms

Built-in transforms applied to metaText values:

TransformInputOutput
durationPT1H30M1h 30m
uppercasegetGET
capitalizewarningWarning
{ tag: 'span', ref: 'meta-item', metaText: 'prepTime',
  transform: 'duration', textPrefix: 'Prep: ' }
// With prepTime="PT45M": "Prep: 45m"

Labels

The label field emits a separate <span data-meta-label> child element before the value text. This enables independent styling — themes can make labels thin and muted, or hide them entirely.

{ tag: 'span', ref: 'meta-item', metaText: 'prepTime',
  label: 'Prep:', metaType: 'temporal', metaRank: 'primary' }

Set labelHidden: true to make the label visually hidden but accessible to screen readers (sr-only pattern). Use this for values that are self-explanatory, like ID badges.

Metadata dimensions

Three fields enable generic cross-rune badge styling. When present, the engine emits data-meta-* attributes on the generated element. Themes style these attributes generically instead of writing per-rune badge CSS.

FieldAttribute emittedDescription
metaTypedata-meta-typeVisual shape — 'status', 'category', 'quantity', 'temporal', 'tag', 'id'
metaRankdata-meta-rankProminence — 'primary' (full size) or 'secondary' (smaller, faded)
sentimentMapdata-meta-sentimentMaps modifier values to colors — 'positive', 'negative', 'caution', 'neutral'
{ tag: 'span', ref: 'badge', metaText: 'difficulty',
  metaType: 'category', metaRank: 'primary',
  sentimentMap: { easy: 'positive', medium: 'neutral', hard: 'caution' } }

When difficulty="easy", the engine emits:

<span data-meta-type="category" data-meta-rank="primary"
      data-meta-sentiment="positive" data-difficulty="easy">
  easy
</span>

See Universal Theming Dimensions for the full CSS system.

mergeThemeConfig

Combines the base config with theme-specific overrides.

import { baseConfig } from '@refrakt-md/runes';
import { mergeThemeConfig } from '@refrakt-md/transform';

const myConfig = mergeThemeConfig(baseConfig, {
  // Override the BEM prefix (optional)
  prefix: 'my',

  // Add icon SVGs (merged by group)
  icons: {
    hint: {
      note: '<svg>...</svg>',
      warning: '<svg>...</svg>',
    },
  },

  // Override specific rune configs (shallow merge per rune)
  runes: {
    Hint: {
      // This replaces only the 'modifiers' field of the Hint config
      // All other fields (block, contextModifiers, structure) are preserved
      modifiers: {
        hintType: { source: 'meta', default: 'info' }, // different default
      },
    },
  },
});

Merge behavior:

FieldStrategy
prefixOverride replaces base
tokenPrefixOverride replaces base
iconsShallow merge by group (override groups replace base groups)
runesPer-rune shallow merge (override fields replace base fields for that rune)
note

Per-rune merge is shallow — if you override modifiers for a rune, you replace the entire modifiers object, not individual entries within it. This is intentional: it keeps the merge behavior predictable and avoids deep-merge surprises.

Real-world examples

Simple rune: Grid

Minimal config — just a block name. CSS handles everything.

Grid: { block: 'grid' }

Modifier rune: Hint

Reads a modifier, adds context-awareness, injects a header with icon and title.

Hint: {
  block: 'hint',
  modifiers: { hintType: { source: 'meta', default: 'note' } },
  contextModifiers: { 'Hero': 'in-hero', 'Feature': 'in-feature' },
  structure: {
    header: {
      tag: 'div', before: true,
      children: [
        { tag: 'span', ref: 'icon', icon: { group: 'hint', variant: 'hintType' } },
        { tag: 'span', ref: 'title', metaText: 'hintType' },
      ],
    },
  },
}

Complex rune: Recipe

Multiple modifiers, conditional structure, content wrapper, text transforms, and metadata dimensions.

Recipe: {
  block: 'recipe',
  contentWrapper: { tag: 'div', ref: 'content' },
  defaultDensity: 'full',
  sections: { meta: 'header', title: 'title', content: 'body' },
  modifiers: {
    prepTime: { source: 'meta' },
    cookTime: { source: 'meta' },
    servings: { source: 'meta' },
    difficulty: { source: 'meta', default: 'medium' },
  },
  structure: {
    meta: {
      tag: 'div', before: true,
      conditionAny: ['prepTime', 'cookTime', 'servings', 'difficulty'],
      children: [
        { tag: 'span', ref: 'meta-item', metaText: 'prepTime',
          transform: 'duration', label: 'Prep:', condition: 'prepTime',
          metaType: 'temporal', metaRank: 'primary' },
        { tag: 'span', ref: 'meta-item', metaText: 'cookTime',
          transform: 'duration', label: 'Cook:', condition: 'cookTime',
          metaType: 'temporal', metaRank: 'primary' },
        { tag: 'span', ref: 'meta-item', metaText: 'servings',
          label: 'Serves:', condition: 'servings',
          metaType: 'quantity', metaRank: 'primary' },
        { tag: 'span', ref: 'badge', metaText: 'difficulty',
          condition: 'difficulty',
          metaType: 'category', metaRank: 'primary',
          sentimentMap: { easy: 'positive', medium: 'neutral', hard: 'caution' } },
      ],
    },
  },
}

The metaType, metaRank, and sentimentMap fields enable generic badge styling — no rune-specific CSS needed for these badges. The sections map and defaultDensity enable generic layout and density-responsive behavior. See Dimensions for the full system.