Theme Configuration Reference (v1.1)

January 24, 2026 · View on GitHub

Table of Contents

Overview

Theme configuration v1.1 introduces role-based styling with inheritance, making themes more maintainable and consistent. All themes automatically inherit from a built-in @base theme that provides sensible defaults.

Key Features:

  • Role-based styling: Define reusable styles
  • Inheritance: Styles and elements can inherit from other styles
  • Mode operations: Add/remove modes instead of replacing
  • Multiple inheritance: Merge properties from multiple parent roles
  • Built-in defaults: All undefined styles fall back to @base theme

Best Practices:

💡 Define generic styles first, specific elements only when necessary

For maximum compatibility with future versions:

  1. Start with styles - Define colors and modes in the [styles] section using role names
  2. Let elements inherit - Elements automatically inherit from their default style roles
  3. Override sparingly - Only define elements in [elements] when you need element-specific styling

Why? Future versions may add new elements. If you define generic styles (e.g., primary, secondary, accent), new elements will automatically pick up your theme's colors. If you only define specific elements, new elements will fall back to the default @base colors, potentially breaking your theme's visual consistency.

Example - Good (future-proof):

[styles]
primary = { foreground = "#E5C07B" }
accent = { foreground = "#98C379" }
# New elements using these roles will automatically inherit your colors

Example - Less ideal:

[elements]
time = { foreground = "#E5C07B" }
key = { foreground = "#98C379" }
# New elements won't inherit these colors

Supported Formats:

  • TOML (.toml) - Recommended
  • YAML (.yaml, .yml)
  • JSON (.json)

Theme Locations:

OSLocation
macOS~/.config/hl/themes/*.{toml,yaml,yml,json}
Linux~/.config/hl/themes/*.{toml,yaml,yml,json}
Windows%USERPROFILE%\AppData\Roaming\hl\themes\*.{toml,yaml,yml,json}

Theme Structure

A v1 theme consists of six sections:

version = "1.1"                    # Required: Theme version
tags = ["dark", "256color"]        # Optional: Theme metadata

[styles]                           # Optional: Reusable style definitions
# Define reusable styles with role names as keys

[elements]                         # Optional: Element-specific styles
# Override default element styles

[levels]                          # Optional: Per-level element overrides
# Override element styles for specific log levels

[indicators]                      # Optional: Status indicator styles
# Define indicator appearance

Minimal Valid Theme

The simplest valid v1 theme:

version = "1.1"

This theme inherits everything from @base.

Style Inheritance

Styles in the @base theme form an inheritance hierarchy. By default, all custom themes inherit these relationships:

Role Inheritance Diagram

You can override any style's inheritance using the style property. For example, to make warning inherit from error:

[styles.warning]
style = "error"
foreground = "yellow"  # Override error's red with yellow

Sections

Version

Required. Must be "1.1" for the current version.

version = "1.1"

Supported versions:

  • "1.1" - Current version, adds unknown level support
  • "1.0" - Previous version

Tags

Optional. Theme classification metadata.

tags = ["dark", "256color"]

Available tags:

  • dark - Dark theme
  • light - Light theme
  • 16color - Optimized for 16-color terminals
  • 256color - Optimized for 256-color terminals
  • truecolor - Optimized for 24-bit color terminals

Tags can be combined (e.g., ["dark", "light"] means compatible with both).

Styles

Optional. Define reusable styles with role names as keys.

💡 Tip: Define your theme colors here first! Elements inherit from these styles by default, ensuring consistency and better forward compatibility. The more you use generic style roles instead of specific element overrides, the better your theme will adapt to future versions.

All 19 predefined role names:

Role NameDefault Purpose
defaultImplicit base for all roles
primaryMain content styling
secondarySecondary/dimmed content
strongEmphasized content
mutedDe-emphasized content
accentHighlighted content
accent-secondarySecondary highlights
messageLog message text
keyKey names in key-value pairs
valueValues in key-value pairs
syntaxSyntax elements (arrays, objects)
statusStatus indicators
levelLog level styling
unknownUnrecognized log level (v1.1+)
traceTrace level specific
debugDebug level specific
infoInfo level specific
warningWarning level specific
errorError level specific

Example:

[styles]
primary = { foreground = "#E0E0E0", modes = ["-faint"] }
secondary = { style = "primary", modes = ["faint"] }
warning = { style = "primary", foreground = "yellow", modes = ["bold"] }
error = { style = "primary", foreground = "bright-red", modes = ["bold"] }

Elements

Optional. Define styles for specific log elements.

💡 Tip: Only override elements when you need element-specific styling that differs from the inherited style roles. Most themes can achieve their look using just the [styles] section, letting elements inherit naturally.

Elements in the @base theme inherit from style roles. The diagram below shows the default inheritance relationships:

Element Inheritance Diagram

Legend:

  • Solid arrows - Style inheritance (element inherits from style role)
  • Dashed arrows - Parent-inner relationship (inner element inherits from parent element by default)

All 28 predefined elements:

CategoryElements
Inputinput, input-number, input-number-inner, input-name, input-name-inner
Metadatatime, level, level-inner, logger, logger-inner, caller, caller-inner
Messagemessage, message-delimiter, field, key, ellipsis
Valuesarray, object, string, number, boolean, boolean-true, boolean-false, null

Example:

[elements]
message = { style = "primary", modes = ["bold"] }
time = { style = "secondary" }
level-inner = { style = "level" }
key = { style = "accent", modes = ["italic"] }
string = { style = "value", foreground = "#98C379" }

Levels

Optional. Override element styles per log level. Supports six log levels:

  • unknown - Unrecognized log level (v1.1+)
  • trace - Trace level
  • debug - Debug level
  • info - Info level
  • warning - Warning level
  • error - Error level

Example:

[levels.warning]
level-inner = { style = ["level", "warning"] }
message = { style = ["message", "warning"] }

[levels.error]
level-inner = { style = ["level", "error"], modes = ["reverse"] }
message = { style = ["message", "error"] }
time = { style = "error" }  # Even time gets error color

Indicators

Optional. Define styles for status indicators (used with --follow mode).

[indicators.sync]
synced = { text = " " }
failed = { 
  text = "!", 
  inner.style = { style = ["status", "warning"], modes = ["bold"] } 
}

Structure:

  • text - The indicator character/text
  • outer.prefix - Text before outer wrapper
  • outer.suffix - Text after outer wrapper
  • outer.style - Style for outer wrapper
  • inner.prefix - Text before inner wrapper
  • inner.suffix - Text after inner wrapper
  • inner.style - Style for inner wrapper

Property Reference

Colors

Three formats supported:

1. ANSI Basic Colors

Named colors (case-sensitive):

foreground = "red"
background = "bright-white"

Available: default, black, red, green, yellow, blue, magenta, cyan, white, bright-black, bright-red, bright-green, bright-yellow, bright-blue, bright-magenta, bright-cyan, bright-white

2. ANSI Extended (256-color palette)

Integer from 0-255:

foreground = 139      # Tan
background = 235      # Dark gray

3. RGB (True color)

Hex format #RRGGBB:

foreground = "#E5C07B"
background = "#282C34"

Note: Hex letters (A-F) are case-insensitive.

Modes

Text rendering modes (case-sensitive):

  • bold - Bold text
  • faint - Dimmed text
  • italic - Italic text
  • underline - Underlined text
  • slow-blink - Slow blinking
  • rapid-blink - Rapid blinking
  • reverse - Reversed colors
  • conceal - Hidden text
  • crossed-out - Strikethrough

Mode Operations (v1 feature)

Add mode:

modes = ["bold"]           # Same as ["+bold"]
modes = ["+bold"]          # Explicit add

Remove inherited mode:

modes = ["-faint"]         # Remove faint mode

Combine operations:

modes = ["-faint", "bold", "italic"]  # Remove faint, add bold and italic

Conflict resolution (last occurrence wins):

modes = ["+bold", "-bold"]    # Bold is removed
modes = ["-bold", "+bold"]    # Bold is added

Style Field

Reference one or more parent styles (by role name) for inheritance.

Single inheritance:

[styles.warning]
style = "primary"
foreground = "yellow"

Multiple inheritance:

[styles.accent-secondary]
style = ["accent", "secondary"]

When multiple role names are listed, properties are merged left-to-right (later values override earlier ones), then explicit properties override all inherited ones.

Advanced Topics

Inheritance Resolution

The complete resolution order for an element:

  1. Start with element from @base theme
  2. Merge with base element from user theme
  3. Merge with level-specific element (if applicable)
  4. Resolve style field recursively (up to 64 levels)
  5. Apply explicit element properties (override role properties)
  6. Apply parent→inner element inheritance

Example:

# User theme
version = "1.0"

[styles]
warning = { foreground = "yellow" }

[elements]
level-inner = { modes = ["bold"] }

[levels.warning]
level-inner = { style = ["level", "warning"] }

For warning-level level-inner:

  1. Start with @base elements.level-inner
  2. Merge user elements.level-inner (adds bold mode)
  3. Merge levels.warning.level-inner (adds style reference)
  4. Resolve style references: ["level", "warning"] → yellow foreground
  5. Apply explicit properties (bold mode already applied)
  6. Final result: yellow foreground, bold mode

Mode Operations

Mode operations modify inherited modes instead of replacing them:

[styles]
primary = { modes = ["bold", "italic"] }
secondary = { style = "primary", modes = ["-italic", "faint"] }

Result for secondary:

  • Inherits: bold, italic
  • Removes: italic
  • Adds: faint
  • Final: bold, faint

Multiple Inheritance

Merge properties from multiple styles (by referencing multiple role names):

[styles]
accent = { foreground = "green" }
secondary = { modes = ["faint"] }
accent-secondary = { style = ["accent", "secondary"] }

Resolution of accent-secondary style:

  1. Start with accent style → foreground: green
  2. Merge secondary style → adds modes: faint
  3. Final: foreground: green, modes: faint

Parent-Inner Elements

Certain elements form parent-inner pairs where the inner element renders inside the parent's styling scope:

  • level / level-inner
  • logger / logger-inner
  • caller / caller-inner
  • input-number / input-number-inner
  • input-name / input-name-inner
  • boolean / boolean-true, boolean-false

Default behavior: Inner elements without explicit styles inherit the parent's resolved style.

Override behavior:

[elements]
level = { foreground = "cyan" }
level-inner = { foreground = "white", modes = ["bold"] }

The inner element's explicit properties override the parent's.

Complete Examples

Minimal Custom Theme

version = "1.1"
tags = ["dark"]

[styles]
warning = { foreground = "yellow" }
error = { foreground = "red" }

Medium Complexity Theme

version = "1.1"
tags = ["dark", "256color"]

[styles]
primary = { foreground = 250, modes = ["-faint"] }
secondary = { style = "primary", modes = ["faint"] }
accent = { foreground = 114 }
warning = { style = "accent", foreground = 214 }
error = { style = "accent", foreground = 196 }

[elements]
message = { style = "primary", modes = ["bold"] }
time = { style = "secondary" }
key = { style = "accent" }
string = { foreground = 114 }
number = { foreground = 170 }

[levels.warning]
level-inner = { style = "warning", modes = ["reverse"] }

[levels.error]
level-inner = { style = "error", modes = ["reverse", "bold"] }
message = { style = "error" }
version = "1.1"
tags = ["dark", "truecolor"]

# Define semantic styles with role names
[styles]
default = {}
primary = { foreground = "#E5C07B", modes = ["-faint"] }
secondary = { style = "primary", foreground = "#5C6370", modes = ["faint"] }
strong = { style = "primary", modes = ["bold"] }
accent = { foreground = "#98C379" }
accent-secondary = { style = "accent", modes = ["faint"] }

# Define message and value styles
message = { style = "strong", foreground = "#E06C75" }
key = { style = "accent", modes = ["italic"] }
value = { style = "primary" }
syntax = { style = "accent" }

# Define level-specific styles
warning = { style = "primary", foreground = "#E5C07B" }
error = { style = "primary", foreground = "#E06C75" }

# Map elements to styles (by role name)
[elements]
time = { style = "secondary" }
level = { style = "secondary" }
level-inner = { style = "primary" }
logger = { style = "accent-secondary" }
caller = { style = "secondary", modes = ["italic"] }
message = { style = "message" }
key = { style = "key" }
string = { style = "value", foreground = "#98C379" }
number = { style = "value", foreground = "#D19A66" }
boolean = { style = "value", foreground = "#56B6C2" }
null = { style = "secondary", foreground = "#C678DD" }
array = { style = "syntax" }
object = { style = "syntax" }

# Level-specific overrides
[levels.warning]
level-inner = { style = ["primary", "warning"], modes = ["reverse"] }
message = { style = ["message", "warning"] }

[levels.error]
level-inner = { style = ["primary", "error"], modes = ["reverse", "bold"] }
message = { style = ["message", "error"] }
time = { style = "error" }

# Indicators
[indicators.sync]
synced = { text = " " }
failed = { 
  text = "!", 
  inner.style = { style = "warning", modes = ["bold"] } 
}

Migration from v0

v0 themes are still supported but v1 offers more powerful features.

Version 1.1 Changes

Added in v1.1:

  • Support for unknown style role - used for unrecognized log levels
  • Support for unknown in levels section for styling entries with unrecognized levels

Migration: If you have a v1.0 theme, simply update the version:

version = "1.1"

Entries with unrecognized levels will use default styling from @base theme (muted style). To customize:

version = "1.1"

[styles]
unknown.foreground = "bright-black"  # Custom styling for unrecognized levels

[levels.unknown]
level-inner.style = ["level", "unknown"]

Simple Migration from v0

v0 theme:

elements:
  message:
    foreground: "bright-white"
    modes: ["bold"]
  time:
    foreground: "bright-black"

v1 equivalent:

version = "1.1"

[elements]
message = { foreground = "bright-white", modes = ["bold"] }
time = { foreground = "bright-black" }

DRY with Styles

v0 (repetitive):

elements:
  message:
    foreground: "#E0E0E0"
    modes: ["bold"]
  string:
    foreground: "#E0E0E0"
  number:
    foreground: "#E0E0E0"

v1 (DRY with styles):

version = "1.1"

[styles]
primary = { foreground = "#E0E0E0" }

[elements]
message = { style = "primary", modes = ["bold"] }
string = { style = "primary" }
number = { style = "primary" }

Mode Semantics

v0: Child modes replace parent modes entirely.

v1: Child modes modify parent modes (add/remove operations).

v0 behavior:

levels:
  warning:
    level:
      modes: ["bold"]  # Replaces all inherited modes

v1 equivalent:

[levels.warning.level]
modes = ["bold"]  # Adds bold to inherited modes

# To replicate v0 replacement behavior, remove all first:
# modes = ["-faint", "-italic", "bold"]  # Explicit replacement

Key Differences

Featurev0v1
Version fieldOptional (missing = v0)Required ("1.0")
Styles sectionNot supported18 predefined role names
InheritanceParent-inner elements onlyStyles + elements
Mode operationsReplace (override)Add/remove (+/-)
Multiple inheritanceNoYes (style = ["role1", "role2"])
@base themeNo defaultsInherits all defaults

Tip: Start with a minimal v1 theme and override only what you need. The @base theme provides sensible defaults for everything!