Skip to content

Refine docs dark theme contrast#42

Merged
charlesrhoward merged 3 commits into
mainfrom
codex/docs-soft-contrast-theme
May 18, 2026
Merged

Refine docs dark theme contrast#42
charlesrhoward merged 3 commits into
mainfrom
codex/docs-soft-contrast-theme

Conversation

@charlesrhoward
Copy link
Copy Markdown
Contributor

@charlesrhoward charlesrhoward commented May 18, 2026

Summary

  • replace the docs chrome with the softened Mogplex dark token set
  • tune sidebar, active nav, TOC, prose links, tables, code blocks, and cards to reduce harsh contrast
  • remove elevated shadows from docs chrome and keep the logo/active indicators restrained

Verification

  • pnpm lint
  • pnpm types:check
  • pnpm build
  • Playwright screenshots: desktop and mobile Connections and MCP page
  • Confirmed server HTML has exactly one active sidebar item on /configure-and-extend/connections-and-mcp

View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

@vercel
Copy link
Copy Markdown

vercel Bot commented May 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mogplex-docs Ready Ready Preview, Comment May 18, 2026 8:31pm

Request Review

Copy link
Copy Markdown

@mogplex mogplex Bot left a comment

Choose a reason for hiding this comment

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

Mogplex PR Review

Status: Attention needed

Solid, well-structured theming overhaul. The token system is clean and the dark-only intent is consistent. Three targeted issues: a WCAG contrast failure on tertiary text, a hardcoded color value that escapes the token system, and a potentially broken pseudo-element positioning dependency. Nothing is a showstopper, but the contrast failure affects all TOC items and could be a real accessibility regression.

4 findings were added inline.

View check run

Comment thread src/app/global.css
--border-default: rgba(255, 255, 255, 0.08);
--border-strong: rgba(255, 255, 255, 0.12);

--text-primary: rgba(255, 255, 255, 0.92);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning: --text-tertiary fails WCAG AA contrast (TOC, prose bullets)

--text-tertiary: rgba(255, 255, 255, 0.38) on --bg-canvas: #0a0a0c yields approximately 3.1:1 contrast — below the WCAG AA minimum of 4.5:1 for normal-sized text.

Affected surfaces:

  • #nd-toc, #nd-toc #toc-title, #nd-toc a — all non-active TOC items render at this opacity
  • --tw-prose-counters and --tw-prose-bullets in the prose block

For a docs site, TOC navigation and list counters are core reading aids; failing AA here is a real accessibility regression.

Suggestion: Raise --text-tertiary to at least rgba(255, 255, 255, 0.50) (~4.8:1 on #0a0a0c), or use --text-secondary (0.55, ~5.2:1) for these surfaces instead.

Comment thread src/lib/layout.shared.tsx Outdated
nav: {
title: (
<span className="inline-flex items-center gap-2 text-fd-foreground">
<span className="inline-flex items-center gap-2 text-[rgba(255,255,255,0.96)]">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning: Hardcoded RGBA in layout.shared.tsx bypasses token system

text-[rgba(255,255,255,0.96)] on the nav logo span is a one-off value that isn't represented in the design token set defined in global.css. The closest token is --text-primary at 0.92; 0.96 is neither defined nor documented as intentionally different.

This means if the token palette shifts later, this element won't follow.

Suggestion: Either add a --text-logo: rgba(255, 255, 255, 0.96) token (if the logo truly needs a distinct value) and use it via a utility, or align to --text-primary (0.92) which is imperceptible to the human eye at this luminance range.

Comment thread src/app/global.css
#nd-sidebar a[data-active='true'],
#nd-sidebar-mobile a[data-active='true'] {
background: var(--accent-soft) !important;
color: var(--text-primary) !important;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Active sidebar indicator ::after relies on undeclared position:relative

The ::after pseudo-element for the active sidebar accent bar uses position: absolute with inset-block/inset-inline-start to place a 2px left pill:

#nd-sidebar a[data-active='true']::after {
  position: absolute;
  inset-block: 0.625rem;
  inset-inline-start: 0;
  ...
}

For this to render correctly, the <a> ancestor must have position: relative. This currently works only if fumadocs sets that on its sidebar link elements. If a fumadocs upgrade changes that, the indicator will escape its container and visually glitch.

Suggestion: Add position: relative to #nd-sidebar a[data-active='true'] to make this self-contained and upgrade-safe.

Comment thread src/app/global.css
#nd-page h4,
#nd-page h5,
#nd-page h6 {
color: var(--text-primary);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: #nd-page > p selector is likely dead CSS

#nd-page > p { color: var(--text-body); } targets only direct children <p> elements of #nd-page. In practice, prose content is always nested inside .prose containers, never as direct children of the page root. This rule has no effect in normal navigation and can be removed to keep the stylesheet clean.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f577cee93c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/app/global.css
Comment on lines +29 to 31
:root,
.light,
.dark {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep light theme tokens distinct from dark theme

The theme variables are now assigned identically for :root, .light, and .dark, so selecting Light (or using System with a light OS preference) no longer changes docs styling. Because the docs sidebar still renders ThemeSwitcher with Light/System options, users can choose a mode that has no visual effect, which is a functional regression in theme behavior rather than just a style tweak.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Dismissing — the theme switcher is disabled in layout.shared.tsx (themeSwitch.enabled: false), so users cannot pick Light or System. The site is dark-only by design; the .light token block is kept for completeness but is never applied at runtime.

Copy link
Copy Markdown

@mogplex mogplex Bot left a comment

Choose a reason for hiding this comment

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

Mogplex PR Review

Status: Attention needed

This is a CSS theming refactor that introduces a well-structured custom token layer on top of Fumadocs' built-in design system. The approach is sound and the PR description matches the diff. There are two correctness issues worth fixing: (1) the dark-mode token cascade has a structural flaw that causes :root dark tokens to be overwritten by the .light class assignment, and (2) WCAG contrast ratios for several body-text tokens fall below the AA threshold. One architecture warning about selector coupling to Fumadocs internals is also worth noting.

4 findings were added inline.

View check run

Comment thread src/app/global.css

:root,
.light,
.dark {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Critical: :root defaults are dark, but the fd-token block unconditionally applies to :root — light mode overrides are silently clobbered

In global.css, the custom tokens are defined as dark-defaults on :root (lines 6–28), then overridden for .light (lines 30–44). However, the Fumadocs mapping block (:root, .light, .dark { … }) re-resolves all --color-fd-* vars using var(--bg-surface), var(--text-body), etc., which are correct at evaluation time.

The real problem is ordering: because :root carries the dark defaults AND the Fumadocs mapping block also targets :root, a browser that has no .dark or .light class on <html> (e.g., during SSR or before next-themes mounts) will render dark tokens — but the .light overrides will not fire until that class is present. This is likely intentional for a dark-first site, but it means that if next-themes defaults to 'system' and the OS preference is light, there will be a flash where dark tokens are applied before the .light class is injected.

More concretely: the .light block only overrides the raw tokens (--bg-canvas, etc.), but the Fumadocs mapping selector :root, .light, .dark re-evaluates them correctly once .light is added. The cascade is technically safe post-hydration. However, any SSR-rendered HTML for light-mode visitors will have incorrect dark token values baked into the initial paint, because :root is always dark and the server cannot add a .light class without a cookie hint. Verify that next-themes is configured with defaultTheme: 'dark' or that a cookie-based theme hint is in place; otherwise light-mode visitors will see a FOIT/FART.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Dismissing — same reason as the sibling Codex thread. Theme switcher is disabled (themeSwitch.enabled: false in layout.shared.tsx), so visitors cannot select Light or System. next-themes is not in play here; the site ships dark-only and there is no light-mode SSR path to flash through.

Comment thread src/app/global.css
--border-strong: rgba(255, 255, 255, 0.12);

--text-primary: rgba(255, 255, 255, 0.92);
--text-body: rgba(255, 255, 255, 0.74);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning: Several text tokens fail WCAG AA contrast on their respective backgrounds

The new token values produce contrast ratios below 4.5:1 (AA for normal text) in some combinations:

  • --text-body: rgba(255,255,255,0.74) on --bg-surface: hsl(0 0% 2%) (~#050505) → effective colour ≈ #C0C0C0 → contrast ~6.3:1 ✅
  • --text-secondary: rgba(255,255,255,0.55) on --bg-sidebar: hsl(0 0% 8%) (~#141414) → effective ≈ #8F8F8F → contrast ~3.9:1 ❌ (used for all sidebar nav links, TOC title, table cell text)
  • --text-tertiary: rgba(255,255,255,0.5) on --bg-canvas: hsl(0 0% 2%) → effective ≈ #818181 → contrast ~3.6:1 ❌ (used for TOC links at rest)
  • --border-subtle: rgba(255,255,255,0.05) is fine for decoration but is also used as --tw-prose-hr and --tw-prose-th-borders — low-contrast table headers may be hard to distinguish.

Sidebar navigation links (color: var(--text-secondary)) and TOC links (color: var(--text-tertiary)) are interactive controls and must meet AA. Consider bumping --text-secondary to rgba(255,255,255,0.65) and --text-tertiary to rgba(255,255,255,0.6) to clear 4.5:1 on both canvas and sidebar backgrounds.

Comment thread src/app/global.css
#nd-page h4,
#nd-page h5,
#nd-page h6 {
color: var(--text-primary);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning: Fumadocs internal ID selectors (#nd-sidebar, #nd-page, etc.) create a brittle coupling to library internals

The entire theming strategy relies on overriding styles via #nd-sidebar, #nd-sidebar-mobile, #nd-toc, #nd-page, #nd-docs-layout, and class-name fragment [class*='border-fd-foreground']. These are internal implementation details of fumadocs-ui and are not part of its public API. A minor version bump of the library could rename, restructure, or remove these IDs, silently breaking the entire theme without a build error.

Fumadocs exposes a CSS variable override mechanism (the --color-fd-* tokens, which this PR correctly populates). Where possible, prefer driving the theme exclusively through those tokens. The !important overrides on #nd-sidebar background and border-color suggest the token approach wasn't fully sufficient — it's worth filing an issue upstream or pinning the fumadocs-ui version explicitly in package.json with a comment explaining why.

Comment thread src/app/global.css
--color-fd-border: var(--border-default);
--color-fd-ring: var(--border-strong);
--color-fd-overlay: rgba(0, 0, 0, 0.62);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Inline rgba(0,0,0,0.62) hardcoded for --color-fd-overlay instead of using a semantic token

Line ~73: --color-fd-overlay: rgba(0, 0, 0, 0.62) is hardcoded in the :root,.light,.dark block rather than being derived from a named token like the rest of the palette. If the overlay needs to differ between light and dark (e.g., rgba(0,0,0,0.4) in light mode), there's no hook to change it. Define --bg-overlay in both :root and .light and reference it here for consistency.

- Bump --text-secondary 0.55 → 0.62 to clear WCAG AA on sidebar
- Add --bg-overlay token so the Fumadocs overlay is themeable
- Move Copy Markdown + View Options into the TOC footer
- Expose /<slug>.md (and /index.md) via rewrite for agent curl access
- Emit <link rel="alternate" type="text/markdown"> for auto-discovery
@charlesrhoward charlesrhoward merged commit 4e6150e into main May 18, 2026
6 checks passed
@charlesrhoward charlesrhoward deleted the codex/docs-soft-contrast-theme branch May 18, 2026 20:32
Copy link
Copy Markdown

@mogplex mogplex Bot left a comment

Choose a reason for hiding this comment

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

Mogplex PR Review

Status: Attention needed

A focused dark-theme contrast refinement with a small but meaningful set of functional changes: new .md URL rewrites for agent-friendly markdown access, TOC footer relocation, and a text/markdown alternate link in metadata. The CSS overhaul is well-structured. There are two correctness issues worth addressing — one in the rewrite destination pattern and one in the CSS theme-token cascade — plus a few minor concerns.

6 findings were added inline.

View check run

Comment thread next.config.mjs
root: __dirname,
},
async rewrites() {
// Agent-friendly: `curl https://docs/<slug>.md` returns raw markdown.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Critical: Rewrite destination path separator — slash vs. dot mismatch

The catch-all rewrite rule uses /:path*.md as the source, which captures path segments separated by /. For a URL like /configure-and-extend/connections-and-mcp.md, Next.js will expand :path* to configure-and-extend/connections-and-mcp — but the destination is /llms.mdx/docs/:path*/content.md, which becomes /llms.mdx/docs/configure-and-extend/connections-and-mcp/content.md. That looks correct for multi-segment paths. However, for a single-segment URL like /getting-started.md, :path* will be getting-started, which maps to /llms.mdx/docs/getting-started/content.md. That also seems fine.

The actual risk is at the boundary: the .md extension is part of the literal source pattern, not captured into :path*. So /foo/bar.md:path* = foo/bar, destination = /llms.mdx/docs/foo/bar/content.md. Cross-check this against how getPageMarkdownUrl builds its segments array ([...page.slugs, 'content.md']) — slugs don't include the .md suffix, so the destination aligns. This appears correct on careful reading, but the comment on the rule says curl https://docs/<slug>.md returns raw markdown — confirm with an integration test that a nested page like /configure-and-extend/connections-and-mcp.md actually resolves and that the pre-rendered static file exists at the destination path. If the static route isn't pre-rendered at that exact path the rewrite will 404 silently.

Comment thread src/app/global.css

:root,
.light,
.dark {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Critical: :root defaults dark tokens — .light class won't override html/body background correctly in SSR

:root is defined with the dark token set (e.g. --bg-canvas: hsl(0 0% 2%)). The .light class redefines the same tokens, but html and body both receive background: var(--bg-canvas) unconditionally, with no selector specificity that accounts for .light vs. the root default.

The cascade for html, body { background: var(--bg-canvas) } resolves at parse time using whatever --bg-canvas is on :root. If the page renders server-side without a .light class on <html> (which is typical for SSR before hydration), the dark canvas token applies. Once JS hydrates and adds .light, the custom property on :root itself is not changed — only .light's scoped value is available when the selector matches. Because var(--bg-canvas) on html/body uses the inheriting value, this should resolve correctly IF .light is placed on <html>. But the ThemeSwitcher has its theme switch disabled (enabled: false in layout.shared.tsx), so .light class application path needs to be verified. If fumadocs-ui or next-themes never adds .light to <html>, the light theme tokens are dead code and only dark tokens ever apply.

Separately: the :root, .light, .dark block maps ALL three contexts to the same fd-* variables. This means .dark and .light map identically, which is only correct because the contextual tokens (--bg-surface, etc.) differ per class. But :root maps dark tokens → fd-* variables, so if a user has prefers-color-scheme: light but no .light class, they get a dark UI. Verify this is intentional (docs are dark-first) and add a comment to prevent future confusion.

Comment thread next.config.mjs
turbopack: {
root: __dirname,
},
async rewrites() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning: .md rewrite conflicts with Next.js static asset handling

Next.js serves files in the public/ directory as static assets before rewrites are evaluated. If any .md file is ever placed in public/ (e.g. a CHANGELOG.md or README.md for agent crawling), it would bypass the rewrite silently. This is low risk currently but worth a short comment on the rewrite to document the assumption that no .md files live in public/.

Comment thread src/lib/source.ts
segments,
url: `${docsContentRoute}/${segments.join('/')}`,
};
// Agent-friendly URL (via next.config rewrite): /<slug>.md or /index.md for root.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning: getPageMarkdownUrl now returns agent-friendly URL as url — callers using the old internal path may break

Previously url was the canonical static pre-render path (${docsContentRoute}/${segments.join('/')}). It's now the rewrite-facing public URL (/index.md or /<slug>.md). Any callers that relied on url as the direct static-file path (e.g. sitemap generation, generateMetadata alternates, OG image logic) now get the rewritten URL instead. Check all call sites of getPageMarkdownUrl(page).url — in this PR, MarkdownCopyButton, ViewOptionsPopover, and generateMetadata alternates all consume it. Confirm MarkdownCopyButton and ViewOptionsPopover are designed to accept the public-facing URL (which seems intentional) and that the alternates.types['text/markdown'] value in metadata is also meant to be the public URL (it is, for discoverability). Just ensure no other caller in the codebase expected the old static path.

Comment thread src/app/global.css
@@ -45,6 +89,197 @@ html > body[data-scroll-locked] {
--removed-body-scroll-bar-size: 0px !important;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning: Hard-coded #nd-* fumadocs internal IDs are a fragile integration contract

Roughly 200 lines of the new CSS target internal fumadocs DOM IDs (#nd-docs-layout, #nd-page, #nd-sidebar, #nd-toc, #nd-sidebar-mobile). These IDs are not part of fumadocs-ui's documented public API and could change in a minor version bump without a breaking-change notice. Consider pinning fumadocs-ui to an exact version in package.json if not already done, or add a comment referencing the fumadocs version these IDs were validated against so future upgrades know to re-verify.

Comment thread src/app/global.css

#nd-toc [class*='border-fd-foreground'] {
border-color: var(--border-subtle) !important;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Prose link color is --text-body (not --accent) — may be intentional but reduces discoverability

Links in prose are styled color: var(--text-body) with only an underline decoration to distinguish them. This is a deliberate low-contrast aesthetic choice (per the PR title), but WCAG 2.1 SC 1.4.1 requires that links not differentiated by color alone must have a non-color visual indicator on the default state — the underline satisfies this. Just flagging for awareness if accessibility audit is planned.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant