Styling & Themes

Wijjit renders every element via a style resolver that behaves much like CSS. Understanding how styles cascade and how to override them lets you craft polished TUIs without hand-tuning ANSI escape codes.

Building blocks

Theme lookup flow

  1. Each element passes a base class string when it draws, e.g. ctx.style_resolver.resolve_style(self, "button"). The resolver also folds in any user CSS classes from element.classes.

  2. StyleResolver fetches theme.get_style("button") and merges the returned Style.

  3. If the element is focused/hovered/disabled/selected, additional pseudo-class entries (button:focus) are merged.

  4. Inline overrides (resolve_style(self, "button", inline_overrides={"fg_color": (255, 0, 0)})) are merged last.

Changing the theme

Create a theme and pass it to the renderer:

from wijjit.styling.theme import Theme
from wijjit.styling.style import Style

neon = Theme("neon", {
    "frame": Style(fg_color=(0, 255, 200)),
    "frame:focus": Style(fg_color=(255, 105, 180), bold=True),
    "button": Style(bg_color=(255, 0, 128), fg_color=(0, 0, 0)),
    "button:hover": Style(bg_color=(255, 85, 170)),
})

# Register the custom Theme object, then activate it by name
app.renderer.theme_manager.register_theme(neon)
app.renderer.theme_manager.set_theme("neon")

The built-in themes ("default", "dark", "light", "high_contrast") are already registered, so switching to one of those is a single call:

app.renderer.theme_manager.set_theme("dark")

Themes can be swapped at runtime (e.g., toggle between light/dark). Elements re-render automatically because the theme change marks all dirty regions.

Overrides in templates

Templates style elements by class, not by inline colours. Give an element a class and add a matching entry to your theme:

{% button action="delete" class="danger" %}Delete{% endbutton %}
app.renderer.theme_manager.register_theme(Theme("myapp", {
    "danger": Style(bg_color=(200, 0, 0), fg_color=(255, 255, 255)),
    "danger:focus": Style(bg_color=(255, 60, 60), fg_color=(0, 0, 0), bold=True),
}))
app.renderer.theme_manager.set_theme("myapp")

Dotted class names (class="toolbar.button") let you reuse theme entries in a BEM-like hierarchy; consistent naming helps manage large design systems.

Note

A few tags do take an attribute literally named style, but it selects a visual preset, not a colour — {% button style="brackets" %}, {% progressbar style="gradient" %}, {% sparkline style="line" %}. It does not accept a Style dict. Inline colour overrides exist only in the Python API, via resolve_style(..., inline_overrides={...}) inside a custom element’s render_to.

Dynamic styling

  • Derive colors from state – swap the class attribute based on a theme preference stored in state, and define both classes in the theme.

  • Toggle pseudo-classes manually – custom elements can set self.focused/self.hovered to trigger :focus/:hover styles.

  • Dark/light switching – store the current theme name in state.theme and call app.renderer.theme_manager.set_theme(state.theme) whenever it changes (register any custom Theme objects once at startup so their names are known).

Built-in style classes

The theme system provides comprehensive style classes for all elements. Here are the key ones:

Frame styles:

frame              # Base frame content
frame:focus        # Focused frame content
frame.border       # Frame border characters
frame.border:focus # Focused frame border

Scrollbar styles:

scrollbar          # Base scrollbar
scrollbar:focus    # Scrollbar when parent is focused
scrollbar.track    # Scrollbar track character
scrollbar.track:focus
scrollbar.thumb    # Scrollbar thumb (position indicator)
scrollbar.thumb:focus

Modal styles (separate from frame for customization):

modal              # Modal content background
modal:focus        # Focused modal
modal.border       # Modal border
modal.border:focus # Focused modal border
modal.text         # Modal text content
modal.backdrop     # Semi-transparent backdrop

Input styles:

input              # Text input base
input:focus        # Focused input
input:disabled     # Disabled input
input.placeholder  # Placeholder text (dim gray)

Button styles:

button             # Base button
button:focus       # Focused button
button:hover       # Hovered button (mouse)
button:disabled    # Disabled button

Global focus color

You can set a global focus color that overrides all element focus styles:

# Set a consistent focus color for all elements
app.config['FOCUS_COLOR'] = (255, 128, 0)  # Orange

# Or use the default cyan
app.config['FOCUS_COLOR'] = (0, 255, 255)

# Reset to theme defaults
app.config['FOCUS_COLOR'] = None

This is particularly useful for:

  • Accessibility – ensure high-visibility focus indicators

  • Branding – match focus color to your application’s accent color

  • Consistency – same focus color across light/dark themes

Scrollbar theming

Scrollbars in frames and text areas use dedicated styles that respond to focus state:

from wijjit.styling.theme import Theme
from wijjit.styling.style import Style

custom_theme = Theme("custom", {
    # Track (the background line)
    "scrollbar.track": Style(fg_color=(60, 60, 60)),
    "scrollbar.track:focus": Style(fg_color=(80, 80, 80)),

    # Thumb (the position indicator)
    "scrollbar.thumb": Style(fg_color=(150, 150, 150)),
    "scrollbar.thumb:focus": Style(fg_color=(0, 255, 255)),  # Cyan when focused
})

Scrollbars can be hidden while keeping scroll functionality:

{% frame scrollable=True show_scrollbar=False %}
    Content scrolls but no scrollbar visible
{% endframe %}

Placeholder text styling

Input placeholders are styled separately from input text, appearing dimmed:

"input.placeholder": Style(fg_color=(128, 128, 128), dim=True)

This is automatically applied when an input shows placeholder text (when empty).

Advanced theming tips

  • Namespaces – use dotted class names (stats.card.header) to mimic BEM-style hierarchies.

  • Share palettes – define helper functions that return consistent Style objects for colors (primary, danger, info). Store them centrally to avoid drifting shades.

  • Animations – while ANSI animations are limited, you can alternate between two styles on a timer (update state + app.refresh_interval) to simulate blinking cursors or progress pulses.

Testing & verification

  • Snapshot tests (via syrupy) catch accidental style regressions—render a view, capture the buffer, and compare against expected output.

  • Use make -C docs html to rebuild documentation whenever style-related APIs change; link to theme references for contributors.

  • Check contrast ratios manually if you target accessibility; terminals vary widely in brightness/contrast.

Where to go next

  • Browse examples/advanced/preferences_demo.py for a live settings palette with theme + typography toggles.

  • Pair styling knowledge with Components to craft cohesive layouts, and revisit State Management to drive theme toggles from user settings.