Rofi RASI Theming
Overview
RASI (Rofi Advanced Style Information) is a CSS-inspired theming system for rofi 2.0+. It provides a fixed widget tree with box-model styling, global variables, calc(), @media queries, and linear gradients. Think CSS circa 2008 + custom properties + calc() applied to a box-packing layout engine. No animations, no transforms, no hover states.
When to Use
- User wants to create or modify a
.rasi theme
- Porting a color scheme / design to rofi
- Debugging layout or styling issues in rofi
- Building creative layouts (grid launchers, decorated panels, Spotlight-style bars)
Widget Tree
Fixed hierarchy — you cannot invent widget types, but you CAN restructure with children: and inject custom widgets via naming prefixes.
window
+-- overlay
+-- mainbox [vertical box]
+-- inputbar [horizontal box]
| +-- prompt [textbox]
| +-- textbox-prompt-colon [textbox]
| +-- entry [textbox]
| +-- case-indicator [textbox]
| +-- num-filtered-rows [textbox]
| +-- textbox-num-sep [textbox]
| +-- num-rows [textbox]
| +-- textbox-current-entry [textbox]
| +-- icon-current-entry [icon]
+-- message [container]
| +-- textbox [textbox]
+-- listview [list container]
| +-- scrollbar
| +-- element [box, repeated per row]
| +-- element-icon [icon]
| +-- element-index [textbox]
| +-- element-text [textbox]
+-- mode-switcher [horizontal box]
+-- button [textbox, per mode]
Error variant: window -> error-message [vbox] -> textbox
Custom Widget Prefixes
Create custom widgets by naming convention — rofi infers the type:
| Prefix |
Type |
Key properties |
textbox-* |
Read-only text |
content, markup |
icon-* |
Icon display |
filename, size, tint |
button-* |
Clickable text |
content, action |
| Any other |
Box container |
children, orientation, spacing |
mainbox {
children: [ textbox-header, inputbar, textbox-divider, listview ];
}
textbox-header {
content: " Applications";
background-color: @accent;
text-color: @bg;
padding: 12px;
font: "GeistMono Nerd Font Bold 14";
}
textbox-divider {
content: "";
background-color: @accent;
padding: 2px; /* colored line separator */
}
Box Model
Identical to CSS. Per-side shorthand works for margin, padding, border, border-radius:
padding: 10px; /* all sides */
padding: 10px 20px; /* vertical horizontal */
padding: 10px 20px 30px; /* top horizontal bottom */
padding: 10px 20px 30px 40px; /* top right bottom left */
border: 2px solid; /* width + style (solid|dash) */
Properties Quick Reference
Window
| Property |
Type |
Example |
font |
Pango string |
"GeistMono Nerd Font 12" |
transparency |
String |
"real", "background", "screenshot", "path.png" |
location |
Position |
center, north, south east, etc. |
anchor |
Position |
Anchor point on the window |
fullscreen |
Boolean |
|
width |
Distance |
800px, 50%, calc(50% min 800px) |
x-offset, y-offset |
Distance |
|
Universal (all widgets)
| Property |
Type |
Notes |
enabled |
Boolean |
Show/hide |
background-color |
Color |
RGBA, hex, named |
background-image |
Image |
linear-gradient(), url() |
text-color |
Color |
|
font |
Pango string |
Per-widget override |
border |
Border |
2px solid, per-side shorthand |
border-color |
Color |
|
border-radius |
Distance |
Per-corner shorthand, 999px for pill |
margin |
Distance |
Per-side shorthand |
padding |
Distance |
Per-side shorthand |
cursor |
Keyword |
default, pointer, text |
Box / Container
| Property |
Type |
Notes |
orientation |
Keyword |
horizontal, vertical |
spacing |
Distance |
Gap between children |
children |
List |
Widget names to pack |
Textbox / Entry / Prompt
| Property |
Type |
Notes |
content / str |
String |
Display text (textbox-* only) |
markup |
Boolean |
Enable Pango markup |
highlight |
Style + Color |
Matched-text styling |
vertical-align |
Real 0.0-1.0 |
|
horizontal-align |
Real 0.0-1.0 |
|
text-transform |
Keyword |
uppercase, lowercase, none |
expand |
Boolean |
Consume remaining space |
placeholder |
String |
Empty-state text |
placeholder-color |
Color |
|
blink |
Boolean |
Cursor blink |
cursor-width |
Distance |
|
cursor-color |
Color |
|
text-outline |
Boolean |
|
text-outline-width |
Double |
|
text-outline-color |
Color |
|
tab-stops |
Distance[] |
Tab stop positions |
Listview
| Property |
Type |
Notes |
columns |
Integer |
Number of columns (grid) |
lines |
Integer |
Visible rows |
fixed-height |
Boolean |
Don't shrink |
fixed-columns |
Boolean |
|
dynamic |
Boolean |
Resize on filter |
scrollbar |
Boolean |
|
cycle |
Boolean |
Wrap navigation |
layout |
Keyword |
horizontal, vertical |
flow |
Keyword |
Packing direction |
reverse |
Boolean |
|
require-input |
Boolean |
Hide until typing |
Icon / element-icon
| Property |
Type |
Notes |
filename |
String |
Path or theme icon name |
size |
Distance |
e.g. 32px |
squared |
Boolean |
Force square |
tint |
Color |
Color overlay |
Scrollbar
| Property |
Type |
handle-width |
Distance |
handle-color |
Color |
handle-rounded-corners |
Boolean |
States and Selectors
No CSS selectors. Target widgets by name, append state modifiers. Two axes multiply:
| | .normal | .urgent | .active | |--|-----------|-----------|-----------| | normal | element normal.normal | element normal.urgent | element normal.active | | selected | element selected.normal | element selected.urgent | element selected.active | | alternate | element alternate.normal | element alternate.urgent | element alternate.active |
alternate = hardcoded odd/even row striping. This is the only :nth-child equivalent.
Multiple selectors: entry, element-text { } (comma-separated).
Variables and Inheritance
/* Global scope only — defined in * {} */
* {
bg: #1e1e2eff;
fg: #cdd6f4ff;
accent: #89b4faff;
gap: 8px;
radius: 8px;
}
window {
background-color: @bg;
}
/* var() with fallback */
width: var(my-width, 50%);
/* env() for environment variables */
width: env(ROFI_WIDTH, 50%);
/* inherit from parent */
inputbar {
background-color: inherit;
}
No scoped variables. Global * {} only. inherit pulls from parent widget.
calc()
Supported operators: +, -, *, /, modulo, min, max, floor, ceil, round.
window { width: calc(50% min 800px); }
listview { spacing: calc(1em / 2); }
@media Queries
Monitor-resolution-based (not window-based):
@media ( min-width: 2560 ) {
listview { columns: 3; }
}
@media ( monitor-id: 0 ) {
window { width: 30%; }
}
@media ( max-height: 768 ) {
listview { lines: 5; }
}
Constraints: min-width, max-width, min-height, max-height, min-aspect-ratio, max-aspect-ratio, monitor-id, enabled.
Gradients and Images
/* Linear gradient — the only gradient type */
background-image: linear-gradient(to right, #ff000080, #0000ff80);
background-image: linear-gradient(45deg, #ff0000, #00ff00, #0000ff);
/* Image file — PNG primarily, SVG if compiled with librsvg */
background-image: url("/path/to/image.png", both);
/* Scale modes: none, both, width, height */
No radial-gradient, conic-gradient, repeating-linear-gradient. No background-size, background-position, background-repeat.
Imports and Composition
@import "filename" /* Merge into current theme */
@theme "filename" /* Wipe slate, load fresh */
?import "filename" /* Optional — no error if missing */
Convention: .rasinc extension for partials. Typical modular pattern:
@theme "base-layout"
@import "~/.config/rofi/colors/palette.rasi"
/* then override specific properties */
Layout Patterns
Centering with expand (flexbox hack)
inputbar {
children: [ dummy-left, entry, dummy-right ];
}
dummy-left { expand: true; }
dummy-right { expand: true; }
Horizontal launcher (Spotlight-style)
listview { layout: horizontal; lines: 1; columns: 8; }
element { orientation: vertical; }
element-icon { horizontal-align: 0.5; }
element-text { horizontal-align: 0.5; }
Grid launcher (3x3)
listview { columns: 3; lines: 3; fixed-columns: true; }
element { orientation: vertical; spacing: 16px; }
element-icon { horizontal-align: 0.5; vertical-align: 0.5; }
element-text { horizontal-align: 0.5; }
Pill-shaped input
entry {
border-radius: 999px;
padding: 8px 16px;
border: 1px solid;
}
Transparency with depth
window {
transparency: "real";
background-color: transparent;
}
mainbox {
background-image: linear-gradient(to bottom, #1e1e2ef2, #1e1e2ecc);
}
Decorative header + divider
mainbox {
children: [ textbox-header, inputbar, textbox-sep, listview ];
}
textbox-header {
content: " Launch";
background-color: @accent;
text-color: @bg;
padding: 12px;
}
textbox-sep {
content: "";
background-color: @accent;
padding: 1px;
}
Monitor-responsive columns
@media ( min-width: 2560 ) {
listview { columns: 3; }
}
@media ( max-width: 1920 ) {
listview { columns: 1; }
}
Hard Stops — What RASI Cannot Do
| Feature |
Status |
Workaround |
| Animations / transitions |
None |
Static only |
:hover, :focus |
None |
Only the 9-state system |
::before, ::after |
None |
Use textbox-* widgets |
| Box-shadow |
None |
Nested borders / compositor shadow |
| Text-shadow |
None |
text-outline is partial substitute |
| Transform (rotate/scale) |
None |
|
| Z-index / stacking |
None |
Widget order = paint order |
| Overflow control |
None |
Only listview scrolls |
| Position absolute/relative |
None |
Flow-based box packing only |
| Flexbox / CSS Grid |
None |
orientation + expand + spacing only |
| Radial/conic gradient |
None |
linear-gradient only |
| Scoped variables |
None |
Global * {} only |
| Selector specificity |
None |
Last-definition-wins |
min-width/max-width on widgets |
None |
@media on monitor only |
| Per-character font fallback |
None |
Pango handles fallback |
background-size/position/repeat |
None |
Only scale mode in url() |
Swappable Theme Architecture
Separation of Concerns
Split rofi config into three layers:
~/.config/rofi/
config.rasi # Behavior only (modi, keybindings, matching, terminal)
themes/
cybrland.rasi # Complete theme: layout + colors
catppuccin-mocha.rasi # Another complete theme
minimal-dark.rasi # Another
shared/
colors-catppuccin.rasinc # Color-only partial (reusable)
colors-cybrland.rasinc # Color-only partial (reusable)
layout-grid.rasinc # Layout-only partial (reusable)
layout-list.rasinc # Layout-only partial (reusable)
config.rasi — Behavior Only
Keep config.rasi free of any visual styling. End it with a single @theme line:
configuration {
modi: "drun,run,filebrowser";
show-icons: true;
font: "GeistMono Nerd Font 12";
terminal: "ghostty";
/* ... all behavior settings ... */
}
@theme "cybrland"
To swap themes, change only the @theme line. Everything else stays.
Theme File Resolution
When @theme "name" is used (no path, no extension), rofi searches:
- Relative to the file containing the
@theme statement
~/.config/rofi/themes/
~/.config/rofi/
~/.local/share/rofi/themes/
${INSTALL_PREFIX}/share/rofi/themes/
It appends .rasi then .rasinc when resolving. So @theme "cybrland" finds ~/.config/rofi/themes/cybrland.rasi.
Full paths and ~/ expansion also work: @theme "~/dotfiles/rofi/mytheme.rasi".
Standalone Theme Structure
A complete swappable theme must be fully self-contained — it defines everything visual:
/* themes/catppuccin-mocha.rasi */
/* === Colors === */
* {
bg: #1e1e2ef2;
surface: #313244ff;
fg: #cdd6f4ff;
accent: #89b4faff;
subtle: #6c7086ff;
urgent: #f38ba8ff;
gap: 8px;
radius: 12px;
}
/* === Window === */
window {
transparency: "real";
background-color: @bg;
border: 2px solid;
border-color: @accent;
border-radius: @radius;
width: 600px;
location: center;
}
/* === Layout === */
mainbox {
background-color: transparent;
padding: @gap;
children: [ inputbar, listview ];
}
/* === Input === */
inputbar {
/* ... */
}
/* === List === */
listview {
/* ... */
}
/* === Elements + States === */
element normal.normal { /* ... */ }
element selected.normal { /* ... */ }
element alternate.normal { /* ... */ }
/* ... all 9 states ... */
Composable Themes with Partials
For maximum reuse, split colors and layout into .rasinc partials and compose them:
/* themes/catppuccin-grid.rasi — composed theme */
@import "shared/colors-catppuccin.rasinc"
@import "shared/layout-grid.rasinc"
/* Theme-specific overrides */
window { width: 800px; }
/* themes/shared/colors-catppuccin.rasinc — color partial */
* {
bg: #1e1e2ef2;
surface: #313244ff;
fg: #cdd6f4ff;
accent: #89b4faff;
/* ... */
}
/* themes/shared/layout-grid.rasinc — layout partial */
window {
transparency: "real";
background-color: @bg;
border: 2px solid;
border-color: @accent;
border-radius: @radius;
location: center;
}
mainbox {
children: [ inputbar, listview ];
/* ... */
}
listview {
columns: 3;
lines: 3;
/* ... */
}
/* ... full layout ... */
This lets you mix any color scheme with any layout: colors-catppuccin + layout-grid, colors-cybrland + layout-list, etc.
Key rule: Partials use .rasinc extension — this is the convention for files meant to be imported, not used standalone. Rofi resolves both .rasi and .rasinc but the naming signals intent.
CLI Override
Override the theme for a single invocation without touching config:
# Use a specific theme
rofi -show drun -theme catppuccin-mocha
# Use a full path
rofi -show drun -theme ~/.config/rofi/themes/minimal-dark.rasi
The -theme flag behaves like @theme — it wipes and replaces. Useful for testing new themes or binding different themes to different keybindings in sway:
# sway config — different launcher styles per keybind
bindsym $mod+space exec rofi -show drun -theme cybrland
bindsym $mod+r exec rofi -show run -theme minimal-dark
Theme Debugging
# Dump the fully resolved theme (all imports flattened)
rofi -dump-theme
# Dump with a specific theme applied
rofi -theme mytheme -dump-theme
# Preview without modifying config
rofi -show drun -theme mytheme
-dump-theme outputs the final computed RASI after all @theme/@import resolution — useful for debugging which values actually apply.
Common Mistakes
Forgetting inherit or transparent: Widgets default to a theme-default background. Set background-color: transparent; or inherit explicitly on containers you want see-through.
border-radius on children clipped by parent: If a child has rounded corners but the parent doesn't, the child's corners get hidden. Match or exceed parent radius.
expand: true fights: If multiple siblings expand, they split space equally. Use expand: false on fixed-size widgets and let only one expand.
@theme vs @import: @theme wipes everything first. If your overrides disappear, you probably used @theme when you meant @import.
Color format: Rofi accepts #RRGGBB, #RRGGBBAA, rgba(), argb:AARRGGBB. Be consistent — mixing formats causes confusion.
transparency: "real" requires compositor support: On Sway, ensure your compositor renders layer-shell surfaces with alpha. Without it, "real" transparency shows black instead of desktop.