LearnNewsExamplesServices
Frontmatter
id18141
titleAn omitted theme value cannot fall back: the inline tab header keeps its neo surface under a classic theme
stateClosed
labels
bugdesignairegressioncore
assigneesneo-opus-grace
createdAtSep 2, 2026, 11:29 PM
updatedAtSep 3, 2026, 12:49 AM
githubUrlhttps://github.com/neomjs/neo/issues/18141
authorneo-opus-grace
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 3, 2026, 12:49 AM

An omitted theme value cannot fall back: the inline tab header keeps its neo surface under a classic theme

Closed v13.2.0/archive-v13-2-0-chunk-18 bugdesignairegressioncore
neo-opus-grace
neo-opus-grace commented on Sep 2, 2026, 11:29 PM

Context

Reported by @tobiu on the running app: the ui: 'inline' tab header used by DockLayouts keeps its dark surface after a runtime switch from neo-theme-neo-dark to neo-theme-light, and a consumer wrote an in-app override to compensate.

Introduced today. PR #18097 (#18095, merged 2026-09-02T14:39:53Z) gave the inline header a flat surface plus a hairline by adding --tab-header-inline-background-color and --tab-header-inline-box-shadow to the two neo themes only, deliberately leaving the classic themes to the structural fallback. That decision is stated in the structural sheet it added:

resources/scss/src/tab/Container.scss — "Every fallback is transparent / none, so a theme that values nothing (the legacy themes) paints exactly what it painted before."

The premise is false, and the counter-statement is already in the tree. Four lines away, theme-light/tab/Container.scss says of the one token it does declare:

"Classic chrome remains intentionally flat; this explicit value must not decay into reliance on the structural fallback."

Both comments are about the same three sibling tokens. The second is correct.

The Problem

A CSS custom property that a theme omits is not reset — it is inherited. var(--x, fallback) uses the fallback only when --x is unset on the element and every ancestor. Themes declare at :root .neo-theme-*, so the value sits on the theme-carrying element and inherits down. A second theme scope that never declares --x does not clear it; the subtree keeps the outer theme's value and the structural fallback never engages.

Nested theme scopes are a first-class feature, not an edge case — component.Base#getTheme() walks the component chain precisely to support them, and its JSDoc says "a child inside a themed scope carries no theme class of its own by design." Theme classes also land on more than one element in practice: @neo-opus-vega measured on 2026-08-31 that at boot the viewport and body carry one, .neo-grid-container carries one from the start, and a runtime toggle gives .workstation-workspace one too — "any bridge pinned to one element is one re-render from being shadowed again." For an inherited custom property the nearest declaring ancestor wins outright; specificity never enters that comparison.

Current state of the three siblings — verified independently by @neo-opus-ada:

token theme-light theme-dark theme-neo-light theme-neo-dark
--tab-header-inline-background-color ❌ ❌ ✅ ✅
--tab-header-inline-background-image ✅ none ✅ none ✅ ✅
--tab-header-inline-box-shadow ❌ ❌ ✅ ✅

One of three, in the classic pair only — under the comment stating the rule the other two break.

The Architectural Reality

This is not a typo, it is a contract nobody stated: the structural fallback is a default, never a reset. A theme opts out of a value only by declaring the opt-out; omission opts in to whatever an ancestor decided. Every consumer that hits this reverse-engineers the cascade instead — resources/scss/src/apps/workstation/Viewport.scss opens with forty lines of specificity archaeology (".workstation-viewport alone is (0,1,0) and loses to the theme's (0,2,0) :root .neo-theme-*") and selects [class*="neo-theme-"] because that "is precisely the set :root .neo-theme-* can match." That override is the symptom, not a preference.

Same genus as #18102 (sheet weight) and #18106 (the getTheme walk), both closed: the engine publishes an answer and one layer never received it.

The Fix

Declare the two missing tokens explicitly in theme-light and theme-dark, beside the sibling that already carries the rule in its comment, with the flat values those themes intend:

--tab-header-inline-background-color: transparent;
--tab-header-inline-background-image: none;   // already present
--tab-header-inline-box-shadow      : none;

This restores exactly the paint PR #18097 intended for the classic themes ("paints exactly what it painted before") and additionally resets the value at a nested theme boundary, which a fallback cannot do. Separation in the classic themes stays with --tab-container-content-border, which they both declare.

And correct the structural comment in resources/scss/src/tab/Container.scss, because it is actively instructing the next author to omit values: a fallback covers a theme with no ancestor scope, not a theme nested inside one.

Deliberately not in scope here — the general rule and its backlog, which want their own tickets:

  1. No CSS selectors inside theme value files (@tobiu, 2026-09-02). Theme files declare variables; anything that needs a selector belongs in resources/scss/src/, with new variables as required. Measured: 26 theme files carry nested selector blocks, 62 blocks total — including this same tab/Container.scss in all four themes, whose variant blocks set --tab-button-* behind .neo-tab-container-inline / -standalone selectors.
  2. Components with no classic-theme styling at all (markdown component, portal app). A real gap, a different one: eventually every theme should support every widget.

Acceptance Criteria

  • AC-1 — with neo-theme-light or neo-theme-dark active, the ui: 'inline' tab header paints no background colour and no hairline, whether or not a neo-theme-neo-* scope is an ancestor.
  • AC-2 — the neo themes are unchanged: flat surface plus hairline, byte-identical emitted CSS for theme-neo-light and theme-neo-dark.
  • AC-3 — a witness that fails against the current tree: a classic-theme inline header nested inside a neo-theme scope resolves the background colour to transparent, not to the neo surface. An arm that only reads a non-nested classic theme cannot see this defect and does not satisfy this AC.
  • AC-4 — the structural comment in src/tab/Container.scss no longer claims a fallback covers a theme that values nothing, and states the reset rule instead.

Out of Scope

  • The two follow-ups listed above (selector-bearing theme files; unstyled components).
  • Any change to what the neo themes paint — #18095 settled that today.
  • The remaining 65 engine-component properties that are declared by some themes and not others. Most are legitimate (a component that only supports the neo themes), and separating real breaks from intended omissions is the follow-up ticket's job, not this one's.

Avoided Traps

  • "Just delete the fallbacks so omission is visible." The fallbacks are correct for the non-nested case and removing them turns a wrong colour into an unstyled header everywhere.
  • Giving the classic themes a surface colour of their own. Tempting, because a flat dock header has less separation — but both classic themes state flat as the intent, and inventing a design value here would smuggle a design decision into a regression fix.
  • Asserting the fix with a non-nested arm. A plain classic-theme header may already read transparent if no ancestor declares the token; only a nested arm discriminates the defect.

Related

  • #18095 / PR #18097 — added the two tokens to the neo themes only; the change this regresses from.
  • #18102, #18106 — same genus, both closed.
  • #18107 — the :where() weight sweep on theme value sheets, adjacent surface.

Live latest-open sweep: checked the latest 20 open issues at 2026-09-02T21:26Z — no equivalent (#18107 is sheet weight, not value omission). A2A claim sweep over the last 12 messages, all read-states: no competing claim. Memory Core rationale sweep on the symptom's nouns surfaced #18095/#18097 as the origin, which is why this is filed as a regression rather than a fresh defect. Own-assignment sweep: none on themes.

Origin Session ID: fb58e855-74b2-4367-9f83-5877f151f024

Retrieval Hint: custom property omitted theme does not reset inherits nested theme scope structural fallback tab header inline background transparent classic legacy themes

tobiu referenced in commit 20dab0c - "fix(tab): every theme states the inline header's paint, because a fallback is not a reset (#18141) (#18144) on Sep 3, 2026, 12:49 AM
tobiu closed this issue on Sep 3, 2026, 12:49 AM
tobiu referenced in commit 671cd74 - "refactor(tab): the inline and standalone selectors leave the theme value files (#18145) (#18167) on Sep 3, 2026, 10:41 AM
tobiu referenced in commit 862edb3 - "refactor(grid): the grid selectors leave the theme value files (#18145) (#18168) on Sep 3, 2026, 10:46 AM