feat(web): add two-color appearance palette - #5258
Conversation
| --border: var(--color-zinc-200); | ||
| --input: var(--color-zinc-300); | ||
| --ring: oklch(0.488 0.217 264); | ||
| --border: color-mix(in srgb, var(--theme-neutral-seed) var(--theme-border-strength), transparent); |
There was a problem hiding this comment.
🟡 Medium src/index.css:889
--border and --input mix --theme-neutral-seed with transparent rather than with a concrete surface color. In light mode the neutral seed is a mid-gray, so compositing it over transparency produces a border that blends to nearly the same value as the surrounding background, making input and control boundaries disappear. The previous code derived borders from --color-zinc-200, which had reliable contrast. Consider mixing the neutral seed over a surface like var(--card) or var(--background) instead of transparent so borders stay visible.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/index.css around line 889:
`--border` and `--input` mix `--theme-neutral-seed` with `transparent` rather than with a concrete surface color. In light mode the neutral seed is a mid-gray, so compositing it over transparency produces a border that blends to nearly the same value as the surrounding background, making input and control boundaries disappear. The previous code derived borders from `--color-zinc-200`, which had reliable contrast. Consider mixing the neutral seed over a surface like `var(--card)` or `var(--background)` instead of `transparent` so borders stay visible.
| --ring: oklch(0.488 0.217 264); | ||
| --border: color-mix(in srgb, var(--theme-neutral-seed) var(--theme-border-strength), transparent); | ||
| --input: color-mix(in srgb, var(--theme-neutral-seed) var(--theme-border-strength), transparent); | ||
| --ring: var(--primary); |
There was a problem hiding this comment.
🟠 High src/index.css:891
Setting --ring: var(--primary) ties the keyboard focus ring color directly to --theme-accent-seed with no contrast derivation. When the accent seed is near-white in light mode or near-black in dark mode, focus rings blend into the background and become invisible on all controls. Unlike --primary-foreground, --ring has no fallback that ensures contrast against the surrounding surface. Consider deriving --ring with a contrast-safe mix or restoring a fixed visible focus color.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/index.css around line 891:
Setting `--ring: var(--primary)` ties the keyboard focus ring color directly to `--theme-accent-seed` with no contrast derivation. When the accent seed is near-white in light mode or near-black in dark mode, focus rings blend into the background and become invisible on all controls. Unlike `--primary-foreground`, `--ring` has no fallback that ensures contrast against the surrounding surface. Consider deriving `--ring` with a contrast-safe mix or restoring a fixed visible focus color.
| const fallbackColor = normalizeThemeColor(getComputedStyle(document.body).backgroundColor); | ||
| const backgroundColor = surfaceColor ?? fallbackColor; | ||
| if (!backgroundColor) return; | ||
|
|
||
| document.documentElement.style.backgroundColor = backgroundColor; | ||
| document.body.style.backgroundColor = backgroundColor; | ||
| ensureThemeColorMetaTag().setAttribute("content", backgroundColor); |
There was a problem hiding this comment.
🟡 Medium hooks/browserChromeTheme.ts:42
syncBrowserChromeTheme permanently writes the resolved color to document.body.style.backgroundColor, and resolveBrowserChromeSurface returns document.body as a fallback. On any route without a sidebar-inset or sidebar-inner element, every subsequent invocation samples that stale inline color from document.body instead of the current stylesheet color, then writes it back — so light/dark or palette changes leave the page background and theme-color meta tag stuck at the previous value. Avoid persisting the sampled color onto the same element that is later sampled, or clear the inline override before recomputing.
const fallbackColor = normalizeThemeColor(getComputedStyle(document.body).backgroundColor);
const backgroundColor = surfaceColor ?? fallbackColor;
if (!backgroundColor) return;
document.documentElement.style.backgroundColor = backgroundColor;
+ document.body.style.removeProperty("background-color");
ensureThemeColorMetaTag().setAttribute("content", backgroundColor);🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/hooks/browserChromeTheme.ts around lines 42-48:
`syncBrowserChromeTheme` permanently writes the resolved color to `document.body.style.backgroundColor`, and `resolveBrowserChromeSurface` returns `document.body` as a fallback. On any route without a `sidebar-inset` or `sidebar-inner` element, every subsequent invocation samples that stale inline color from `document.body` instead of the current stylesheet color, then writes it back — so light/dark or palette changes leave the page background and `theme-color` meta tag stuck at the previous value. Avoid persisting the sampled color onto the same element that is later sampled, or clear the inline override before recomputing.
problem
t3 code has light, dark, and system appearance modes, but no small supported way to personalize their palette. previous theming prs tended to grow into full theme engines, custom css, import/export, or unrelated settings infrastructure.
approach
this keeps the surface intentionally small:
color-mix()prior art reviewed
direct theming attempts
supporting appearance work
#94, #108, #464, #648, #800, #924, #2174, #2759, #2779, #3294, #3466, #4715, #5103
#2531 was the closest fit because it generated a palette from primary and neutral seeds. #1550 also captured julius's direction toward one extendable appearance system. #2550 and #5226 show the cost of runtime rewrites and broad theme libraries, so this pr avoids both.
no historical discord message from julius matched the theming searches. the relevant guidance was in github review history. public
openai/codexsource exposes a tui theme picker, not the desktop app's accent implementation, so this follows the same narrow product constraint rather than copying unavailable desktop code.actual app evidence
before
after
light mode palette demo:
https://t3bot-production.up.railway.app/files/d--mB0YUr2rcq2mC7CrMFgIK/t3code-two-color-theme-light.mp4
dark mode palette and reload persistence demo:
https://t3bot-production.up.railway.app/files/wDv0zwW1wxI7welxzxBOGQOn/t3code-two-color-theme-dark-persistence.mp4
both recordings are the real t3 code app in chromium.
validation
one concern
arbitrary seed colors are clamped into contrast-safe semantic roles, so very light or dark selections intentionally look different from the raw seed on primary controls.
Built with OpenAI Codex on T3 Code.
Note
Add two-color accent and neutral theme palette to appearance settings
useThemeColorshook that reads/writes accent and neutral hex color seeds to localStorage, applies them as--theme-accent-seedand--theme-neutral-seedCSS custom properties, and syncs across tabs via storage events.color-mix, in both light and dark themes.<head>bootstrap script and on module import in main.tsx to avoid flash of unstyled color./settings/appearance.📊 Macroscope summarized d3c4644. 4 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.