RomM v2 — Tokens, Theming & Visual Language
SkillMediaGuides your agent to style the RomM v2 frontend using design tokens instead of hardcoded colors.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the RomM v2 — Tokens, Theming & Visual Language skill
About this capability
Theming, design tokens, colors, and visual language in the RomM v2 frontend. Use when styling v2 components, picking colors, adding/using CSS variables, working with light/dark themes, or whenever you'd reach for a hex/rgba literal. Covers the token pipeline (src/v2/tokens/index.ts → build:tokens →
What this skill tells your AI
The instructions your AI receives, as published by rommapp/romm in .claude/skills/frontend-v2-theming/SKILL.md and read by ahel’s review.
Tokens are the only source of truth for theming. Zero hex/rgba() literals in v2 components. If a value is missing, add a token. Every component must work in both v2-dark and v2-light.
Token pipeline
src/v2/tokens/index.ts is the source. It feeds two consumers:
src/v2/styles/tokens.css— generated byscripts/build-tokens.ts(npm run build:tokens, hooked intopredev/prebuild). Do not hand-edit. This is how the vast majority of tokens are consumed:var(--r-color-...)in CSS.- Direct JS/TS imports of named exports (
colorCanvas,colorCoverArt,layout, …) for the few cases needing a token value in JavaScript — baking colors into an SVG string (utils/covers), canvas/QR backgrounds (Player/Ruffle.vue,ShowQRCodeDialog), and the virtualiser's pixel math (Gallery/listColumnsreadinglayout).
v2 has no Vuetify theme of its own, and no Vuetify at all. tokens.css emits a palette block per theme under .r-v2.r-v2-dark / .r-v2.r-v2-light; RomM.vue toggles those classes on <html>. v2 surfaces never read Vuetify's runtime theme (src/plugins/vuetify.ts serves v1 only).
Adding a new token
- Add it to
src/v2/tokens/index.tswith a semantic, role-based name (--r-color-danger, not--r-color-red). - Provide both dark and light values.
- Consume via
var(--r-...)(CSS) or the named export (JS). - Run
npm run build:tokensto regeneratetokens.css. - If the JS→CSS variable name needs an exception (e.g.
--r-nav-h), add an entry toNAME_OVERRIDESin the generator.
Where the scope classes live — and why <html>
.r-v2, .r-v2-dark, .r-v2-light go on <html> (RomM.vue toggles them whenever uiVersion or the active theme changes). The overlay primitives (RDialog, RMenu, RTooltip) <Teleport to="body">, landing outside the app root. Only <html> covers both the regular tree and the teleports; without it, overlays lose their tokens.
Diagnostics — when var(--r-color-...) resolves to nothing on an overlay
- Check
RomM.vue's watch ondocumentElement.classList(load-bearing). - Check that the teleport target is attached to the document.
<html>is the document root, so any in-document target inherits the scope; a detached node resolves novar(--r-*)at all. - Never "fix" it by swapping the token for a hex literal — that hides the bug and breaks the dual theme.
Visual language
- Single visual vocabulary. Every surface (dialog, menu, popover, card, toolbar) reads as a sibling — same blur, curvature, depth. No standalone "dialog look" vs "menu look".
- Canonical references when designing: ask the user before consulting
https://mockup.thebirdcage.tv/. They decide whether the mockup or existing primitives take priority. - State semantics are shared across primitives (don't reinvent per component):
- hover (neutral, or brand-tinted on selected rows)
- selected/checked (
--r-color-brand-primary) - active/favorite (
--r-color-fav) - focus (modality-gated; visible only on
key/pad— seefrontend-v2-input) - busy/pending · disabled
- Implementation gotchas:
- Every dialog goes through
RDialog; every menu throughRMenu; every tooltip throughRTooltip. Each owns its teleport and positioning, so don't hand-roll a parallel surface. The scrim and the reference-counted body scroll lock (lib/overlays/bodyScrollLock.ts) belong toRDialogandRDraweronly; menus and tooltips have neither.
- Every dialog goes through
Color-literal policy: zero exceptions
Outside src/v2/tokens/index.ts (the source-of-truth TS module) and the generated src/v2/styles/tokens.css, no hex or rgba() literals exist anywhere in v2. Everything previously "excepted" is now a token or a color-mix:
- Cover-overlay glass →
--r-color-overlay-*(fixed dark glass; never theme-flips). - Cover artwork placeholder & shimmer →
--r-color-cover-placeholder,--r-color-cover-placeholder-bright. - Panel / tooltip / shimmer-sweep →
--r-color-panel,--r-color-panel-border,--r-color-tooltip-bg,--r-color-shimmer-sweep. - Backdrop scrims (
global.css) →color-mix(in srgb, var(--r-color-bg) X%, transparent). - Status tints →
color-mix(in srgb, var(--r-color-status-base-{success,warning,danger,info}) X%, transparent). - Brand-tinted backgrounds (selected rows, focus rings) →
color-mix(in srgb, var(--r-color-brand-primary) X%, transparent). - Black/white shadows →
color-mix(in srgb, black X%, transparent)(CSS named color, not a hex literal). - Metadata-provider chips →
--r-color-provider-*. Player canvas →--r-color-canvas-bg,--r-color-canvas-bg-deep. - Emphasis pill (always-white-on-dark "Play" CTA over cover art) →
--r-color-overlay-emphasis-bg/-fg/-bg-hover.
If a literal would otherwise be needed, the answer is: add a token (steps above), then consume via var(--r-color-...) or the named export.
Style conventions
- Scoped
<style>by default; unscoped only for teleport overrides. - BEM-ish class names:
.feature__element--modifier. Prefixes:.r-v2-...for app-shell surfaces outside components;.r-...for globally shared utilities/tokens. - No utility-class framework (no Tailwind, no Vuetify): layout is plain CSS in the component's scoped block, with tokens (
var(--r-space-*),var(--r-radius-*)) for every value that has one.
Signals
- GitHub stars
- 13k
- Forks
- 735
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
frontend-v2-theming- Source
- github.com/rommapp/romm