# Rofi Rasi Theming

> Use when creating, editing, or debugging rofi themes (.rasi files), designing rofi launcher UIs, or when the user asks to customize rofi appearance — covers the full RASI styling system including widget tree, box model, properties, gradients, media queries, layout tricks, and hard limitations

- Skill: `thisfrontenddev/rofi-rasi-theming` (Agent Skill)
- Install (CLI): `npx skillmds@latest add thisfrontenddev/rofi-rasi-theming`
- Raw SKILL.md: https://api.skillmd.com/api/skills/thisfrontenddev/rofi-rasi-theming/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: thisfrontenddev (https://skillmd.com/u/thisfrontenddev)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/thisfrontenddev/rofi-rasi-theming

---


# 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` |

```css
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`:

```css
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

```css
/* 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`.

```css
window { width: calc(50% min 800px); }
listview { spacing: calc(1em / 2); }
```

## @media Queries

Monitor-resolution-based (not window-based):

```css
@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

```css
/* 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

```css
@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:

```css
@theme "base-layout"
@import "~/.config/rofi/colors/palette.rasi"
/* then override specific properties */
```

## Layout Patterns

### Centering with expand (flexbox hack)

```css
inputbar {
    children: [ dummy-left, entry, dummy-right ];
}
dummy-left { expand: true; }
dummy-right { expand: true; }
```

### Horizontal launcher (Spotlight-style)

```css
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)

```css
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

```css
entry {
    border-radius: 999px;
    padding: 8px 16px;
    border: 1px solid;
}
```

### Transparency with depth

```css
window {
    transparency: "real";
    background-color: transparent;
}
mainbox {
    background-image: linear-gradient(to bottom, #1e1e2ef2, #1e1e2ecc);
}
```

### Decorative header + divider

```css
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

```css
@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:

```css
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:

1. Relative to the file containing the `@theme` statement
2. `~/.config/rofi/themes/`
3. `~/.config/rofi/`
4. `~/.local/share/rofi/themes/`
5. `${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:

```css
/* 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:

```css
/* themes/catppuccin-grid.rasi — composed theme */
@import "shared/colors-catppuccin.rasinc"
@import "shared/layout-grid.rasinc"

/* Theme-specific overrides */
window { width: 800px; }
```

```css
/* themes/shared/colors-catppuccin.rasinc — color partial */
* {
    bg:      #1e1e2ef2;
    surface: #313244ff;
    fg:      #cdd6f4ff;
    accent:  #89b4faff;
    /* ... */
}
```

```css
/* 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:

```bash
# 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:

```bash
# 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

```bash
# 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.

