Skip to content

feat(web): add two-color appearance palette - #5258

Draft
t3-code[bot] wants to merge 4 commits into
mainfrom
agent/two-color-appearance
Draft

feat(web): add two-color appearance palette#5258
t3-code[bot] wants to merge 4 commits into
mainfrom
agent/two-color-appearance

Conversation

@t3-code

@t3-code t3-code Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

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:

  • one accent seed and one neutral seed
  • original system, light, and dark miniature workspace previews inspired by codex’s presentation, without copying its ui
  • an interface contrast control that adjusts palette strength without adding a third color seed
  • semantic light and dark palettes generated with css color-mix()
  • accent-driven primary, info, focus, hover, and selected states
  • neutral-driven backgrounds, cards, borders, inputs, and sidebar surfaces
  • contrast-safe foreground roles even for extreme picker values
  • local persistence applied in the document bootstrap to avoid a flash of defaults
  • browser chrome synced to the live palette
  • native color pickers, curated swatches, per-row reset, restore-all support, and settings search
  • focused normalization, persistence, and root-variable tests

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/codex source 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

default appearance

after

two-color appearance

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

  • latest github ci: check, tests, mobile analysis, and release smoke all passing
  • formatter
  • targeted lint
  • web typecheck
  • web unit suite: 203 files, 1771 tests
  • production web build
  • manual chromium checks in light and dark mode, including reload persistence
  • two independent blocker reviews, including extreme seed contrast and control semantics

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

  • Adds useThemeColors hook that reads/writes accent and neutral hex color seeds to localStorage, applies them as --theme-accent-seed and --theme-neutral-seed CSS custom properties, and syncs across tabs via storage events.
  • Refactors index.css so all palette tokens (backgrounds, cards, borders, sidebars, etc.) derive from the two seed variables using color-mix, in both light and dark themes.
  • Adds color swatch pickers to the Appearance settings panel with reset support, and wires theme color state into the existing settings restore flow.
  • Seeds are applied in the <head> bootstrap script and on module import in main.tsx to avoid flash of unstyled color.
  • "Theme colors" is added to the settings search index pointing to /settings/appearance.
📊 Macroscope summarized d3c4644. 4 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 3, 2026
@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 3, 2026
Comment thread apps/web/src/index.css
Comment thread apps/web/src/index.css
--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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment thread apps/web/src/index.css
--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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 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.

Comment on lines +42 to +48
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants