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

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

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-codet3-codeBot 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-actionsgithub-actionsBot 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-actionsgithub-actionsBot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 3, 2026
Comment threadapps/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.

🟡 Mediumsrc/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);

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.

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

🟡 Mediumhooks/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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@maria-rcks
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

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

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

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-codet3-codeBot 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-actionsgithub-actionsBot 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-actionsgithub-actionsBot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 3, 2026
Comment threadapps/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.

🟡 Mediumsrc/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);

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.

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

🟡 Mediumhooks/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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@maria-rcks
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

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

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-codet3-codeBot 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-actionsgithub-actionsBot 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-actionsgithub-actionsBot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 3, 2026
Comment threadapps/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.

🟡 Mediumsrc/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);

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.

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

🟡 Mediumhooks/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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@maria-rcks
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

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

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-codet3-codeBot 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-actionsgithub-actionsBot 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-actionsgithub-actionsBot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 3, 2026
Comment threadapps/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.

🟡 Mediumsrc/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);

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.

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

🟡 Mediumhooks/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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@maria-rcks
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

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

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

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-codet3-codeBot 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-actionsgithub-actionsBot 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-actionsgithub-actionsBot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 3, 2026
Comment threadapps/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.

🟡 Mediumsrc/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);

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.

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

🟡 Mediumhooks/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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@maria-rcks
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

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

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-codet3-codeBot 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-actionsgithub-actionsBot 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-actionsgithub-actionsBot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 3, 2026
Comment threadapps/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.

🟡 Mediumsrc/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);

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.

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

🟡 Mediumhooks/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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@maria-rcks
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

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

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-codet3-codeBot 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-actionsgithub-actionsBot 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-actionsgithub-actionsBot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 3, 2026
Comment threadapps/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.

🟡 Mediumsrc/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);

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.

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

🟡 Mediumhooks/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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@maria-rcks
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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

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

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-codet3-codeBot 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-actionsgithub-actionsBot 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-actionsgithub-actionsBot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 3, 2026
Comment threadapps/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.

🟡 Mediumsrc/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);

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.

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

🟡 Mediumhooks/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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@maria-rcks