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 is adom/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:

  1. panelState.displayIcon — a placeholder you pass via --display-icon / HTTP displayIcon. Shown only until the page loads.
  2. The loaded page's <link rel="icon"> — Hydrogen's webview panel reads the favicon after load and writes it into panelState.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's panelState.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 with fill="#e6edf3" (or fill="currentColor" so it inherits), a 24x24 viewBox, 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 64 or 0 0 24 24).
  • Self-contained SVG — no external fonts, no xlink:href to 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 displayIcon after load with another add-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:href to other files, no @import in <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

  1. src/cli/start.rs passes --display-icon "mdi:chip" to adom-cli hydrogen workspace add-tab — this shows the MDI chip icon for ~100ms while the page loads.
  2. src/assets/shell.html has <link rel="icon" type="image/svg+xml" href="favicon.svg">.
  3. src/server/mod.rs serves the favicon at /favicon.svg with the content of src/assets/icon.svg — a teal-colored (#00b8b0) custom SVG chip.
  4. When the shell loads, Hydrogen fetches favicon.svg, gets the teal chip SVG, and writes that into panelState.displayIcon.
  5. 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 documents displayIcon as a panelState field (but buries the favicon-override behavior).
  • ~/.claude/skills/adom/webview/SKILL.md — webview panel actions (navigate, refresh, setHeaderHidden, setProxyEnabled).
  • ~/.claude/skills/adom/guides/app-creator.md — app conventions including the docs/icon.svg brand-icon requirement.
  • iconify MDI catalog — the searchable list of every mdi:<name>.