Hydrogen Webview Tab Icons
UnreviewedHow Hydrogen **webview** tab icons actually work in the Adom editor — specifically panels with `panelType: adom/a1b2c3d4-0031-4000-a000-000000000031`.
Hydrogen Webview Tab Icons
How Hydrogen webview workspace tabs actually get their icons in
the Adom editor. Read the "Real mechanism" section first — the
placeholder vs. favicon distinction is the thing future Claude
sessions usually get wrong, including an earlier version of this
skill.
Scope
This skill is only about tabs whose panelType isadom/a1b2c3d4-0031-4000-a000-000000000031 — the generic Hydrogen
webview that loads an HTML page from a URL. The favicon-override
mechanism described below is specific to webview panels because
only they load an HTML page that can ship its own<link rel="icon">.
Other panel types — Schematic Editor, 3D Editor, KiCad panel,
Desktop Demo, Movie Maker, Adom Viewer native panel, etc. —
have their icons assigned by Hydrogen's internal panel registry
keyed off panelType, not by anything the tab adder passes in.
If your tab isn't a webview and its icon is wrong, this skill
will not help you — open a Hydrogen issue instead.
Real mechanism (the thing everyone misses)
Hydrogen webview tabs have two sources for their displayed icon,
and the loaded page wins:
panelState.displayIcon— a placeholder you pass via--display-icon/ HTTPdisplayIcon. Shown only until the page
loads.- The loaded page's
<link rel="icon">— Hydrogen's Web View
panel reads the favicon afterloadand writes it intopanelState.displayIcon, overriding whatever the placeholder was.
Practical consequence: once any page with a favicon finishes
loading, your --display-icon "mdi:chip" is gone. If you want the
icon to be stable and predictable, you have to serve the favicon
you want from the page you load.
Empirical proof: create a tab with --display-icon "mdi:rocket-launch",
navigate it to https://example.com, then inspect the tab'spanelState.displayIcon via adom-cli hydrogen workspace tabs — it
will be a data:image/svg+xml;base64,... value matching example.com's
favicon, not mdi:rocket-launch.
Brand rule: icons must be monochrome #e6edf3
Per gallia/skills/brand/SKILL.md, all Adom icons must be monochrome
white (#e6edf3) — no colored icons, no multi-color, no brand-palette
colors in icons. This applies to tab icons too.
- First choice: use an
mdi:<name>placeholder for the--display-icon
flag. MDI icons are already monochrome and inherit the theme color
automatically, so you get brand compliance for free. Search
https://pictogrammers.com/library/mdi/ for a matching icon. - If you need a custom icon (no MDI match, or you want to ship your
own favicon for the page-load override): hand-roll an SVG withfill="#e6edf3"(orfill="currentColor"so it inherits), a24x24viewBox, and clean filled paths or 2px strokes. No
gradients, no shadows, no color. Details that break at 16 px lose.
Concrete example of a brand-compliant custom icon (the adom-tsci
chip) — 318 bytes, single path, monochrome #e6edf3:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#e6edf3">
<path d="M6 2a2 2 0 0 0-2 2v2H2v2h2v3H2v2h2v3H2v2h2v2a2 2 0 0 0 2 2h2v2h2v-2h3v2h2v-2h3v2h2v-2a2 2 0 0 0 2-2v-2h2v-2h-2v-3h2v-2h-2V8h2V6h-2V4a2 2 0 0 0-2-2h-2V0h-2v2h-3V0h-2v2H9V0H7v2H6zm0 2h12v16H6V4zm2 3v10h8V7H8zm2 2h4v6h-4V9z"/>
</svg>
Rendered in a dark-background Hydrogen tab bar, that's a white chip
with pins sticking out — visually identical to mdi:chip but served
from your own app so it can't be overridden.
The right way: serve your own favicon from your app
If you're writing a Rust/Node/Python/Go app that opens a Hydrogen
webview tab, put a <link rel="icon"> in the HTML you serve.
Hydrogen will pick it up on load and that's what the user will see.
Use a brand-compliant #e6edf3 monochrome SVG (see section above).
Rust + tiny_http example (the adom-tsci pattern)
<!-- src/assets/shell.html, embedded in the binary via include_str! -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My App</title>
<!-- Hydrogen reads this and uses it as the tab icon. -->
<link rel="icon" type="image/svg+xml" href="favicon.svg">
...
// Serve favicon.svg from the tiny_http route handler.
if url == "/favicon.svg" || url == "/favicon.ico" {
let body = include_str!("../assets/icon.svg");
return Ok(request.respond(
Response::from_string(body)
.with_header(
Header::from_bytes(&b"Content-Type"[..], &b"image/svg+xml"[..]).unwrap()
),
)?);
}
<!-- src/assets/icon.svg — MONOCHROME per the brand rule. fill must be
"#e6edf3" (or "currentColor"), NEVER a colored palette value. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#e6edf3">
<rect x="5" y="5" width="14" height="14" rx="1"/>
<rect x="3" y="7" width="2" height="2"/>
<rect x="3" y="11" width="2" height="2"/>
<rect x="3" y="15" width="2" height="2"/>
<rect x="19" y="7" width="2" height="2"/>
<rect x="19" y="11" width="2" height="2"/>
<rect x="19" y="15" width="2" height="2"/>
<rect x="7" y="3" width="2" height="2"/>
<rect x="11" y="3" width="2" height="2"/>
<rect x="15" y="3" width="2" height="2"/>
<rect x="7" y="19" width="2" height="2"/>
<rect x="11" y="19" width="2" height="2"/>
<rect x="15" y="19" width="2" height="2"/>
</svg>
(Perfectly symmetric chip: body + 12 pins, 3 per side, even 4-unit
spacing, rx=1 rounded corner, all in brand #e6edf3. This is the
exact adom-tsci icon — taken from the repo's src/assets/icon.svg.)
When the page loads from https://<workspace>.adom.cloud/proxy/<port>/,
the browser fetches /proxy/<port>/favicon.svg, your app returns the
SVG, Hydrogen syncs it onto the tab, and the icon stays put.
Rules for the favicon file:
- Square
viewBox(e.g.0 0 64 64or0 0 24 24). - Self-contained SVG — no external fonts, no
xlink:hrefto
other files, no embedded images. - Bold, filled shapes over thin strokes — the tab bar is
~16–20 px so details get lost. - Transparent background is fine, the tab bar shows through.
- Brand color in the SVG itself if you want a colored icon. MDI
icons can't be tinted.
Placeholder-only case: mdi:<name> / data:... via add-tab
If the page you're loading doesn't set its own favicon, or if
you're creating a tab that navigates to about:blank or similar, the--display-icon placeholder is all you get — and it will stick.
Examples where this is actually useful:
- A tab that loads an external site with no favicon. Rare in
practice; most real sites have a favicon. - A stopgap during the ~100ms the page takes to load — the user
sees the placeholder, then it swaps to the page's favicon. - When you don't own the page — you can't inject a
<link>, so
the placeholder is your only control point, knowing it's about to
get overridden.
CLI
adom-cli hydrogen workspace add-tab \
--panel-id <leaf-id> \
--panel-type "adom/a1b2c3d4-0031-4000-a000-000000000031" \
--display-name "My App" \
--display-icon "mdi:chip" \
--initial-state '{"url":"https://example.com/"}'
HTTP API
curl -s -X POST \
-H "X-Api-Key: $API_KEY" $TARGET_HEADER \
-H "Content-Type: application/json" \
-d '{
"panelId": "<leaf-id>",
"panelType": "adom/a1b2c3d4-0031-4000-a000-000000000031",
"displayName": "My App",
"displayIcon": "mdi:chip",
"initialState": { "url": "https://example.com/" }
}' \
"https://hydrogen.adom.inc/api/workspaces/editor/$OWNER/$REPO/current/tabs"
Two placeholder formats
| Format | When to use | Example |
|---|---|---|
mdi:<icon-name> |
Monochrome icon from Material Design Icons. Inherits the editor theme's foreground color. Can't be tinted. | mdi:chip, mdi:bug, mdi:eye |
data:image/svg+xml;base64,<b64> |
Custom SVG, full control. Must still be monochrome #e6edf3 per the brand rule — don't color your own placeholder. |
data:image/svg+xml;base64,PHN2... |
Again: both are placeholders. The page's favicon overrides them
both as soon as it loads.
Common MDI icons used in existing Adom tabs
| Icon | Typical use |
|---|---|
mdi:chip |
chips, ICs, boards, PCB/tscircuit tools |
mdi:eye |
viewers (AV) |
mdi:bug |
bug-filing tools |
mdi:chart-line |
visualizers |
mdi:monitor-cellphone |
demo/desktop tools |
mdi:github |
repo browsers |
mdi:book-open-variant |
docs viewers |
mdi:cart |
commerce tools |
mdi:robot |
automation / AI |
mdi:folder |
file explorers |
mdi:code-tags |
code editors |
mdi:palette |
design tools |
mdi:camera |
screenshot tools |
Full catalog: https://icon-sets.iconify.design/mdi/.
Building a custom SVG data URL by hand
When you need a one-off custom icon and don't want to ship a file,
you can build a data: URL inline:
SVG='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#00b8b0" d="M13.13 22.19L11.5 18.36..."/></svg>'
B64=$(echo -n "$SVG" | base64 -w0)
DATA_URL="data:image/svg+xml;base64,$B64"
adom-cli hydrogen workspace add-tab \
--panel-id <leaf-id> \
--panel-type "adom/a1b2c3d4-0031-4000-a000-000000000031" \
--display-name "My App" \
--display-icon "$DATA_URL" \
--initial-state '{"url":"about:blank"}'
base64 -w0 (no line wrapping) is important — newlines inside the
value break the JSON it gets embedded in.
This is still a placeholder. If the tab ever navigates to a page
with its own favicon, the placeholder is lost.
Troubleshooting
"I pass mdi:chip but the tab shows a different icon"
Your loaded page has a favicon that overrode the placeholder.
Options:
- Best: serve your own favicon from your app (see Real mechanism
above). Pick an SVG that matches the brand/icon you want. Hydrogen
will sync it onto the tab. - Acceptable: if you don't control the page, you can re-set the
displayIconafter load with anotheradd-tab/PATCH call — but
the next navigation will override it again.
"My placeholder flickers for a moment then reverts"
That's the expected behavior — the placeholder shows until the page
finishes loading, then Hydrogen swaps in the page's favicon.
"My tab has no icon at all"
The loaded page has no <link rel="icon"> and no--display-icon was passed. Hydrogen falls back to the default Web
View icon (a globe). Add a favicon to your served HTML.
"I pass a data:image/svg+xml;base64,... and it's broken"
Usually a base64 encoding issue:
- Use
base64 -w0(no wrap) so the output is a single line. - Make sure the raw SVG has no unescaped newlines or tabs that break
the JSON wrapper. - The SVG must be self-contained: no
xlink:hrefto other files, no@importin<style>, no embedded<image>pointing at URLs.
"I set the tab to mdi:chip but workspace tabs shows a data URL"
Normal. Hydrogen rewrites panelState.displayIcon after page load
with whatever the page's favicon was (usually a data URL because
some sites inline their favicons). This is the override in action.
How the adom-tsci app handles this
src/cli/start.rspasses--display-icon "mdi:chip"toadom-cli hydrogen workspace add-tab— this shows the MDI chip
icon for ~100ms while the page loads.src/assets/shell.htmlhas<link rel="icon" type="image/svg+xml" href="favicon.svg">.src/server/mod.rsserves the favicon at/favicon.svgwith the
content ofsrc/assets/icon.svg— a teal-colored (#00b8b0)
custom SVG chip.- When the shell loads, Hydrogen fetches
favicon.svg, gets the
teal chip SVG, and writes that intopanelState.displayIcon. - The tab now permanently shows the teal chip — because every
future reload re-serves the same favicon.
The MDI mdi:chip placeholder and the served favicon.svg are
visually similar by design — the transition is invisible. If they
were different, you'd see a brief flash.
See also
~/.claude/skills/adom/guides/adom-workspace-control.md— the full
workspace API reference that documentsdisplayIconas apanelStatefield (but buries the favicon-override behavior).~/.claude/skills/adom/webview/SKILL.md— Web View panel actions
(navigate, refresh, setHeaderHidden, setProxyEnabled).~/.claude/skills/adom/guides/app-creator.md— app conventions
including thedocs/icon.svgbrand-icon requirement.- iconify MDI catalog — the
searchable list of everymdi:<name>.