Theming
View SourceColor schemes and styling for terminal and LiveView apps.
Terminal Theming
Inline colors
The View DSL accepts colors directly via fg: and bg::
text("Hello", fg: :cyan) # Named ANSI color
text("Warning", fg: :yellow, style: [:bold]) # Bold yellow
text("Custom", fg: {255, 107, 174}) # RGB tuple
text("256-color", fg: 198) # 256-color palette index
text("Hex color", fg: "#ff6bae") # Hex stringAvailable named colors: :black, :red, :green, :yellow, :blue, :magenta, :cyan, :white.
Color by status
Pattern match to return colors based on data:
defp severity_color(:critical), do: :red
defp severity_color(:warning), do: :yellow
defp severity_color(:info), do: :cyan
defp severity_color(_), do: :white
# Usage:
text(message, fg: severity_color(level))Terminal capability detection
Raxol auto-detects terminal color support and downsamples:
- Truecolor (24-bit): RGB tuples and hex strings render exactly
- 256-color: RGB is mapped to the nearest 256-color value
- 16-color: Mapped to the closest ANSI color
- Mono: All colors stripped, styling preserved (bold, underline)
Raxol.Core.ColorSystem.Adaptive.adapt_color_safe/1 handles this transparently.
Synthwave '84 palette example
The flagship demo uses Synthwave '84 Soft mapped to ANSI:
# Consistent color language across your app
defp accent, do: :cyan # Titles, active elements
defp highlight, do: :magenta # Key hints, borders
defp warn, do: :yellow # Warnings, headers
defp ok, do: :green # Success, healthy
defp err, do: :red # Errors, critical
# Usage:
text(" DASHBOARD ", fg: accent(), style: [:bold])
text(" q:quit Tab:switch ", fg: highlight())
text("CPU: #{pct}%", fg: if(pct > 90, do: err(), else: ok()))Themed panels
Create a reusable panel helper:
defp panel(title, opts \\ []) do
border = Keyword.get(opts, :border, :single)
active = Keyword.get(opts, :active, false)
children = Keyword.get(opts, :children, [])
box style: %{border: (if active, do: :double, else: border), flex: 1} do
column do
[text(" #{title} ", fg: :cyan, style: [:bold]) | children]
end
end
endTheme System
ThemeManager
Register and switch themes at runtime:
# Register a custom theme
Raxol.UI.Theming.ThemeManager.register_theme(:ocean, %{
name: "Ocean",
colors: %{
primary: {64, 196, 255},
secondary: {0, 255, 157},
background: {15, 25, 45},
foreground: {200, 220, 240},
error: {255, 85, 85},
warning: {255, 200, 0}
},
component_styles: %{
button: %{fg: {64, 196, 255}, bold: true},
text_input: %{border: :single, fg: {200, 220, 240}},
tree: %{fg: {200, 220, 240}}
}
})
# Switch theme
Raxol.UI.Theming.ThemeManager.set_theme(:ocean)Component-level theming
Widgets read theme styles from the render context:
# In your component render:
def render(state, context) do
theme = context[:theme] || %{}
theme_style = Raxol.UI.Theming.Theme.component_style(theme, :button)
base_style = Map.merge(theme_style, state.style || %{})
# ...render with base_style...
endPseudo-state styles
Themes can define styles for :focus, :active, and :disabled states:
component_styles: %{
button: %{
fg: :cyan,
focus: %{fg: :white, bg: :blue, bold: true},
active: %{fg: :black, bg: :cyan},
disabled: %{fg: :white, dim: true}
}
}The FocusHelper module resolves the correct style based on widget state.
Built-in themes
Themes are stored as JSON in priv/themes/:
ls priv/themes/
# default.json, nord.json, dracula.json, ...
LiveView Theming
When using the LiveView bridge, themes apply via CSS classes:
<.live_component
module={Raxol.LiveView.TerminalComponent}
id="terminal"
buffer={@buffer}
theme={:nord}
/>Built-in LiveView themes: :nord, :dracula, :solarized_dark, :solarized_light, :monokai.
Custom CSS theme
.terminal.theme-custom {
background-color: #1a1a2e;
color: #e0e0e0;
}
.terminal.theme-custom .fg-cyan { color: #40c4ff; }
.terminal.theme-custom .fg-magenta { color: #ff6bae; }
.terminal.theme-custom .fg-green { color: #00ff9d; }
.terminal.theme-custom .fg-yellow { color: #ffd700; }
.terminal.theme-custom .fg-red { color: #ff5555; }
.terminal.theme-custom .cursor {
background-color: #40c4ff;
}Dynamic theme switching
def handle_event("change_theme", %{"theme" => theme}, socket) do
{:noreply, assign(socket, theme: String.to_existing_atom(theme))}
endColor Palettes
Popular palettes mapped to ANSI for quick reference:
Nord
red: #bf616a, green: #a3be8c, yellow: #ebcb8b, blue: #81a1c1, magenta: #b48ead, cyan: #88c0d0
Dracula
red: #ff5555, green: #50fa7b, yellow: #f1fa8c, blue: #bd93f9, magenta: #ff79c6, cyan: #8be9fd
Tokyo Night
red: #f7768e, green: #9ece6a, yellow: #e0af68, blue: #7aa2f7, magenta: #ad8ee6, cyan: #449dab
Catppuccin Mocha
red: #f38ba8, green: #a6e3a1, yellow: #f9e2af, blue: #89b4fa, magenta: #f5c2e7, cyan: #94e2d5
Accessibility
High contrast
Use maximum contrast ratios. Avoid relying on color alone:
# Bad: color is the only indicator
text("OK", fg: :green)
text("FAIL", fg: :red)
# Good: text + color
text("[OK] Passed", fg: :green)
text("[!!] FAILED", fg: :red, style: [:bold])WCAG contrast checking
Ensure foreground/background pairs meet WCAG AA (4.5:1 ratio):
# High contrast pairs that work everywhere:
text("...", fg: :white, bg: :black) # 21:1
text("...", fg: :black, bg: :green) # ~5.5:1
text("...", fg: :black, bg: :cyan) # ~8.6:1
text("...", fg: :black, bg: :yellow) # ~10.2:1Examples
examples/demo.exs-- Flagship demo using Synthwave '84 mapped to ANSIexamples/advanced/color_system_demo.ex-- Color system with adaptive downsampling
Next Steps
- Building Apps -- TEA patterns and recipes
- SSH Deployment -- Serve apps over SSH
- Performance -- 60fps techniques