The I2P+ theming system supports four built-in themes (dark, light, classic, midnight) and allows full customization via override.css files. Themes span the router console and all webapps (susimail, susidns, i2psnark, login, i2ptunnel). Each webapp has its own theme directory under the same theme name so a single theme selection applies across all apps.
docs/themes/ <-- deployed at runtime under $I2P/
├── fonts/ <-- shared web fonts (OpenSans, Sora, FiraCode)
├── console/ <-- Router Console themes
│ ├── shared.css <-- CSS rules shared by all console themes
│ ├── confignav.css <-- config navigation
│ ├── helpnav.css <-- help navigation
│ ├── tablesort.css <-- sortable tables
│ ├── tunnels.css <-- tunnel page styling
│ ├── viewprofile.css<-- peer profile page
│ ├── mobile.css <-- mobile responsive defaults
│ ├── graphConfig.css
│ ├── images/ <-- shared SVG icons (as CSS custom properties)
│ │ ├── images.css <-- all icon data-URIs
│ │ ├── itooplus.css
│ │ └── i2ptunnel.css
│ ├── dark/ <-- default theme
│ │ ├── global.css <-- CSS custom properties (colors, gradients, shadows)
│ │ ├── console.css<-- main stylesheet, @imports global.css, shared.css
│ │ ├── chromescroll.css
│ │ ├── console_big.css, console_ar.css, mobile.css, wizard.css
│ │ ├── i2ptunnel.css
│ │ ├── images/ <-- theme-specific images (logo, favicon, thumbnail)
│ │ └── override.css.ocean.blue <-- example override files (rename to override.css to use)
│ ├── light/
│ │ ├── global.css, console.css, ...
│ │ └── override.css.*
│ ├── classic/
│ │ └── ...
│ └── midnight/
│ └── ...
├── susimail/ <-- webmail themes
│ ├── shared.css
│ ├── images/
│ ├── dark/, light/, classic/, midnight/
│ └── susimail.css, mobile.css, images/
├── susidns/ <-- address book themes
│ ├── shared.css, lazyload.css
│ ├── images/
│ └── dark/, light/, classic/, midnight/
│ └── susidns.css, images/
├── snark/ <-- BitTorrent client themes
│ ├── shared.css
│ ├── dark/, light/, classic/, midnight/, ubergine/, vanilla/, zilvero/
│ └── snark.css, nocollapse.css, snark_big.css, images/
├── login/ <-- login page themes
│ ├── shared.css
│ └── dark/, light/, classic/, midnight/
│ └── login.css
├── geomap/
│ └── geomap.css
└── imagegen/
└── imagegen.css
Source location:
installer/resources/console/themes/in the source tree.
Deployed to:$I2P/docs/themes/at runtime (copied by theprepthemeupdatesbuild target).
Not bundled in WAR files — themes live on the filesystem so they survive upgrades and can be edited in place.
The active theme is controlled by the router config property:
routerconsole.theme=dark
Default is dark. Valid values are any directory name under docs/themes/console/.
CSSHelper.java (apps/routerconsole/java/src/net/i2p/router/web/CSSHelper.java) resolves the theme path:
public static final String PROP_THEME_NAME = "routerconsole.theme";
public static final String DEFAULT_THEME = "dark";
public static final String BASE_THEME_PATH = "/themes/console/";
public String getTheme(String userAgent) {
String url = BASE_THEME_PATH;
if (userAgent != null && userAgent.contains("MSIE") && !userAgent.contains("Trident/6")) {
url += "classic/"; // force classic for old IE
} else {
String theme = _context.getProperty(PROP_THEME_NAME, DEFAULT_THEME);
url += theme + "/";
}
return url;
}
When routerconsole.universal.theme=true, all webapps (susimail, susidns, i2psnark) use the same routerconsole.theme value. When false, each webapp can have its own saved theme preference (stored in the respective app’s config file).
Third-party themes can be registered via routerconsole.theme.<name>=/path/to/theme/dir. The viewtheme.jsp servlet resolves these paths (see Theme Serving).
Each webapp reads the console theme independently:
| Webapp | Class / Method | Theme path |
|---|---|---|
| Console | CSSHelper.getTheme() |
/themes/console/<theme>/ |
| I2PTunnel | IndexBean.getTheme() |
/themes/console/<theme>/ (shares console dir) |
| SusiDNS | BaseBean.getTheme() |
/themes/susidns/<theme>/ |
| Susimail | WebMail.java |
/themes/susimail/<theme>/ |
| I2PSnark | SnarkManager.getTheme() |
/themes/snark/<theme>/ |
| Login | reads docs/themes/login/theme.txt |
/themes/login/<theme>/ |
From head.jsi (apps/routerconsole/jsp/head.jsi), CSS is loaded in this exact order:
1. <theme>/global.css -- CSS custom property definitions (variables)
2. font stylesheet -- /themes/fonts/OpenSans.css or Sora.css
3. <theme>/console.css -- main theme stylesheet (may @import shared.css)
└─ @import url(global.css) -- (already loaded, cached)
└─ @import url(../shared.css) -- cross-theme reusable rules
└─ @import url(../images/itooplus.css) -- I2P+-specific icon overrides
4. /themes/console/images/images.css -- shared SVG icon data-URIs
5. <theme>/images/images.css -- theme-specific icon overrides
6. Language-specific CSS -- console_big.css (zh), console_ar.css (ar/fa), etc.
7. <theme>/override.css -- user customization (loaded LAST → highest priority)
override.cssis only included if the file exists on disk atdocs/themes/console/<theme>/override.css.
override.css Systemoverride.css is the primary customization mechanism. It is a user-created CSS file placed in a theme directory. Because it loads after all other CSS, any rule in override.css wins over the theme defaults.
override.css in $I2P/docs/themes/console/<theme>/override.csshead.jsi tests for existence via new File(themeBase + "override.css").exists()override.css is not shipped with the router and is never overwrittenviewtheme.jsp sends Cache-Control: no-cache, private, max-age=2628000 for override.css URLs, bypassing the immutable cache policy of regular theme assetsEach theme directory may include example override files with descriptive names rather than override.css itself. Users rename one to activate it:
| Directory | Example files |
|---|---|
console/dark/ |
override.css.ocean.blue, override.css.purple, override.css.red |
console/light/ |
override.css.charcoal, override.css.flat, override.css.lowlight, override.css.solar.green, override.css.solarsurprise |
console/midnight/ |
override.css.purple |
snark/dark/ |
override.css.ocean.blue |
snark/ubergine/ |
override.css.bandwidth, override.css.no_status_text |
susimail/light/ |
override.css.bottom.notifications, override.css.charcoal |
susidns/light/ |
override.css.charcoal |
/* Override the dark theme with a blue/cold hue rotation */
html {
filter: hue-rotate(120deg);
}
body {
background: repeating-linear-gradient(to right, rgba(0,0,0,0.8) 1px,
rgba(0,0,0,0.8) 2px, rgba(0,0,32,0.6) 3px) #000;
}
img, #sb_localtunnels img, .sb *::before, #routerlogs li, .tunnelBuildStatus {
filter: hue-rotate(-120deg) !important;
}
Each webapp also checks for its own override.css:
WebMail.java, ~line 2270): <link rel=stylesheet href=.../override.css>BaseBean.isOverrideCssActive()): checks docs/themes/susidns/<theme>/override.cssI2PSnarkServlet.java): includes it conditionally at line ~4745Each theme defines its visual identity in global.css via :root CSS custom properties. The dark theme defines ~169 variables including:
| Variable | Purpose |
|---|---|
--bodybg |
Page background (gradient) |
--a |
Link color |
--active |
Active link/highlight color |
--hover |
Hover color (typically #f60 orange) |
--ink |
Primary text color |
--border |
Standard border |
--btn |
Button background gradient |
--input_txt |
Input field background |
--helpbox |
Help box background |
--badge |
Badge/pill background |
--download_bar |
Download progress bar pattern |
--highlight |
Highlight shadow inset |
--camo |
Camouflage SVG filter overlay |
--graphoverlay |
Graph overlay pattern |
Webapps @import the console’s global.css to inherit these variables:
/* susimail/dark/susimail.css */
@import url(/themes/console/dark/global.css);
@import url(../images/images.css);
@import url(images/images.css);
All icons are delivered as inline SVG data URIs stored in CSS custom properties. The shared file console/images/images.css defines ~100+ icons:
:root {
--abook:url("data:image/svg+xml,%3Csvg viewBox='0 0 64 64'...");
--add:url("data:image/svg+xml,%3Csvg ...");
--ban:url("data:image/svg+xml,%3Csvg ...");
--clock:url("data:image/svg+xml,%3Csvg ...");
--configure:url("data:image/svg+xml,%3Csvg ...");
...
}
Usage in CSS:
.someElement {
background: var(--add) no-repeat center;
}
Theme-specific icon overrides live in <theme>/images/images.css. The I2P+-specific icons are in console/images/itooplus.css.
All theme files are served at runtime by viewtheme.jsp (apps/routerconsole/jsp/viewtheme.jsp), which:
css, png, svg, woff2, etc.)routerconsole.theme.<name> properties$I2P/docs/ (not from the WAR)override.css: no-cache, private, max-age=2628000private, max-age=2628000, immutableno-cache, private, max-age=2628000Theme resources at /themes/console/<theme>/images/thumbnail.png, favicon.svg, i2plogo.png are public (no auth required via AuthFilter.java).
GraphRenderer.java reads routerconsole.theme to select graph color schemes:
String theme = _context.getProperty("routerconsole.theme", "dark");
if (theme.equals("midnight")) {
/* purple tones */
} else if (theme.equals("dark")) {
/* orange tones */
} else {
/* light/classic tones */
}
The theme selection UI is rendered by ConfigUIHelper.getSettings() (also ConfigUIHelper.java). It:
docs/themes/console/routerconsole.theme.* for plugin themes/themes/console/<theme>/images/thumbnail.pngOn form submission, ConfigUIHandler.java saves routerconsole.theme=<name> and writes docs/themes/login/theme.txt for the login page.
Create a directory under the console theme root:
mkdir -p installer/resources/console/themes/console/mytheme/images
global.cssDefine your CSS custom properties. At minimum:
:root {
--a: #494;
--active: #f90;
--hover: #f60;
--ink: #ee9;
--ink_bright: #aa3;
--bodybg: #000;
--border: 1px solid #242;
--border_hard: 1px solid #252;
--border_soft: 1px solid #2529;
--btn: linear-gradient(180deg, #001000, #000);
--badge: linear-gradient(180deg, #020, #010);
}
You can reference the built-in themes for a complete list of variables:
console/dark/global.css (169 variables)console/light/global.css (143 variables)console/midnight/global.css (137 variables)console.css@import url(global.css);
@import url(../shared.css);
@import url(../images/itooplus.css);
/* Your theme styles */
body {
background: var(--bodybg);
color: var(--ink);
}
a { color: var(--a); }
a:hover { color: var(--hover); }
/* ... */
Place at minimum:
images/thumbnail.png — 48x48 picker thumbnailimages/i2plogo.png — console logoimages/favicon.svg — faviconimages/images.css — theme-specific icon overrides (empty is fine)For each webapp you want themed, create a parallel directory:
installer/resources/console/themes/susimail/mytheme/susimail.css
installer/resources/console/themes/susidns/mytheme/susidns.css
installer/resources/console/themes/snark/mytheme/snark.css
installer/resources/console/themes/login/mytheme/login.css
Each webapp CSS should @import the console’s global.css:
/* susimail/mytheme/susimail.css */
@import url(/themes/console/mytheme/global.css);
@import url(../images/images.css);
@import url(images/images.css);
body {
background: var(--bodybg);
color: var(--ink);
}
/* ... */
ant prepthemeupdates # copies themes to build temp dir
ant pkg # full build including themes
Or for quick development, copy directly to your running router:
cp -r installer/resources/console/themes/console/mytheme $I2P/docs/themes/console/
Then set the theme in router console → Config → UI, or edit $I2P/router.config:
routerconsole.theme=mytheme
override.css example (optional)Ship example override files alongside your theme:
installer/resources/console/themes/console/mytheme/override.css.warm
installer/resources/console/themes/console/mytheme/override.css.contrast
Users rename one to override.css to activate it.
override.css Only (No Full Theme)For quick customizations without creating a full theme:
Create $I2P/docs/themes/console/dark/override.css:
/* Make links purple instead of green */
a { color: #a6f !important; }
a:hover { color: #f60 !important; }
/* Custom background */
body {
background: #0a0a12 !important;
}
Since webapps also check for override.css, create it in each:
$I2P/docs/themes/susimail/dark/override.css
$I2P/docs/themes/susidns/dark/override.css
$I2P/docs/themes/snark/dark/override.css
Or link them:
ln -s ../console/dark/override.css $I2P/docs/themes/susimail/dark/override.css
A single-line override can completely shift the color scheme:
/* Turn green theme to ocean blue */
html { filter: hue-rotate(120deg); }
/* Un-rotate images so they stay natural colors */
img { filter: hue-rotate(-120deg) !important; }
| Concept | File/Location |
|---|---|
| Theme selection | routerconsole.theme config property |
| Theme resolution | CSSHelper.java → getTheme() |
| CSS variables | <theme>/global.css |
| Main stylesheet | <theme>/console.css |
| Shared CSS rules | console/shared.css |
| Shared icons | console/images/images.css (~100 SVG data URIs) |
| User overrides | <theme>/override.css (user-created, survives upgrades) |
| Theme serving | viewtheme.jsp (serves from $I2P/docs/themes/) |
| Theme picker UI | ConfigUIHelper.getSettings() |
| Build target | ant prepthemeupdates copies themes to docs/themes/ |
| Source root | installer/resources/console/themes/ |
| Runtime root | $I2P/docs/themes/ |
| Plugin themes | routerconsole.theme.<name>=/path/ |
| Graph colors | GraphRenderer.java reads routerconsole.theme |