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
.rasitheme - 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
@themestatement ~/.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.