Skip to content

fix: code blocks invisible in dark mode#763

Closed
WolfieLeader wants to merge 4 commits intoTanStack:mainfrom
WolfieLeader:fix/empty-code-blocks
Closed

fix: code blocks invisible in dark mode#763
WolfieLeader wants to merge 4 commits intoTanStack:mainfrom
WolfieLeader:fix/empty-code-blocks

Conversation

@WolfieLeader
Copy link

@WolfieLeader WolfieLeader commented Mar 17, 2026

Summary

  • Code blocks across all TanStack.com docs rendered empty in dark mode
  • The initial <pre> render used className="shiki github-light dark:aurora-x" — the CSS rule html.dark .shiki.github-light { display: none } hid it, and dark:aurora-x is not a valid Tailwind utility so the aurora-x class was never applied
  • If Shiki's async highlighting failed (no error handling), the hidden initial render stayed permanently

Changes

  • CodeBlock.tsx: Replace theme-specific classes with Tailwind dark: variants so the initial render is visible in both modes
  • CodeBlock.tsx: Add try/catch + cleanup flag to the Shiki highlighting effect
  • CopyPageDropdown.tsx: Remove unused PACKAGE_MANAGERS import (lint cleanup)

Test plan

  • Verified code blocks render with syntax highlighting in light mode
  • Verified code blocks render with syntax highlighting in dark mode
  • TypeScript check passes
  • Lint passes (0 errors)
  • Verify on production deployment

Fixes #761, fixes #760, fixes #759

Summary by CodeRabbit

  • Improvements

    • Enhanced code block styling with explicit padding and neutral light/dark colors
    • Better rendering for embedded diagrams (now produced as SVG and wrapped for display)
  • Bug Fixes

    • Improved error handling and logging during code highlighting
    • Prevented state updates after components unmount via cancellation guards
  • Refactor

    • Removed unused imports and simplified control flow

The initial render used className "shiki github-light dark:aurora-x".
In dark mode, `html.dark .shiki.github-light { display: none }` hid
the element. Since dark:aurora-x is not a valid Tailwind utility, the
aurora-x class was never applied — leaving code blocks permanently
hidden when Shiki's async highlighting failed to replace them.

- Use Tailwind dark: variants for proper light/dark initial render
- Add try/catch to Shiki effect to prevent silent failures
- Add cleanup flag to prevent state updates after unmount

Fixes TanStack#761, fixes TanStack#760, fixes TanStack#759
@netlify
Copy link

netlify bot commented Mar 17, 2026

Deploy Preview for tanstack ready!

Name Link
🔨 Latest commit 55a2054
🔍 Latest deploy log https://app.netlify.com/projects/tanstack/deploys/69b949bfe6681a0008af2a1e
😎 Deploy Preview https://deploy-preview-763--tanstack.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 53 (🟢 up 18 from production)
Accessibility: 90 (no change from production)
Best Practices: 75 (🔴 down 17 from production)
SEO: 98 (🟢 up 1 from production)
PWA: 70 (no change from production)
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@changeset-bot
Copy link

changeset-bot bot commented Mar 17, 2026

⚠️ No Changeset found

Latest commit: d8e2889

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Mar 17, 2026

📝 Walkthrough

Walkthrough

Refactors code block rendering: adds async cancellation and error handling, restructures syntax-highlighting and mermaid rendering, and removes an unused import in CopyPageDropdown. Rendering control flow and DOM updates are guarded against component unmounts.

Changes

Cohort / File(s) Summary
Import Cleanup
src/components/CopyPageDropdown.tsx
Removed unused PACKAGE_MANAGERS import; now only getPackageManager is imported. No behavior change.
Code Block Rendering Refactor
src/components/markdown/CodeBlock.tsx
Rewrote async highlighting flow: added cancellation flag and cleanup to avoid state updates after unmount, wrapped async work in try/catch with warnings, consolidated theme-generated HTML before setState, adjusted pre element styling, and changed mermaid handling to render SVG inside a container div with computed attributes.

Sequence Diagram(s)

sequenceDiagram
  participant Browser as Browser/React
  participant CodeBlock as CodeBlock component
  participant Highlighter as SyntaxHighlighter
  participant Mermaid as MermaidRenderer
  participant DOM as DOM / state

  Browser->>CodeBlock: mount with code + lang + theme
  CodeBlock->>Highlighter: request highlight (async)
  alt lang == mermaid
    CodeBlock->>Mermaid: render mermaid to svg (async)
    Mermaid-->>CodeBlock: svg/html
  else
    Highlighter-->>CodeBlock: highlighted HTML per theme
  end
  CodeBlock->>CodeBlock: check cancelled flag
  alt not cancelled
    CodeBlock->>DOM: setState/update innerHTML
  else
    CodeBlock--xDOM: abort update
  end
  Browser->>CodeBlock: unmount -> set cancelled flag
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 hop-hop, I parsed each line and caught the snag,
Async highlights now guard against the drag,
Mermaid blooms in a tidy SVG nest,
Unused imports cleared — the code can rest,
Hooray, docs sparkle; the rabbit's off to nap! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: code blocks invisible in dark mode' clearly and concisely describes the main change, which is fixing the visibility issue of code blocks when displayed in dark mode.
Linked Issues check ✅ Passed The PR successfully addresses the primary objectives: fixes code blocks rendering in dark mode via styled pre elements and Shiki error handling, resolves dark mode visibility issues, and includes error handling improvements.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing code block rendering and dark mode issues. The import cleanup in CopyPageDropdown.tsx is a minor lint-related change that does not introduce out-of-scope functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/components/markdown/CodeBlock.tsx (1)

169-221: Good async cancellation and error handling pattern.

The cancellation flag correctly prevents state updates after unmount. The try/catch ensures the component gracefully degrades to the initial placeholder if highlighting fails.

One minor note: isEmbedded is used in the effect (line 207) but not included in the dependency array. If isEmbedded could change during the component's lifecycle, the styling would become stale. In practice this prop is likely stable, but consider adding it for correctness:

-  }, [code, lang])
+  }, [code, lang, isEmbedded])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/markdown/CodeBlock.tsx` around lines 169 - 221, The effect
uses the isEmbedded prop inside its async work (affecting className logic) but
the dependency array only lists [code, lang], so add isEmbedded to the
dependency array of the useEffect that contains the async IIFE (the block that
sets cancelled, calls getHighlighter/getMermaid and calls setCodeElement) to
ensure the effect re-runs when isEmbedded changes; update the dependency list
from [code, lang] to [code, lang, isEmbedded] (or include isEmbedded alongside
existing deps) so styling remains correct and React rules of hooks are
satisfied.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/markdown/CodeBlock.tsx`:
- Around line 187-196: The code builds a div using preAttributes.class from
extractPreAttributes(output) which can be null, causing class='null ...'; update
the mermaid handling in CodeBlock.tsx to null-safe the class value returned by
extractPreAttributes: read preAttributes via extractPreAttributes(output),
coalesce preAttributes.class to an empty string (or only include the class
attribute when non-empty) before interpolating into the returned HTML string
(references: extractPreAttributes, preAttributes, genSvgMap, getMermaid,
trimmedCode).

---

Nitpick comments:
In `@src/components/markdown/CodeBlock.tsx`:
- Around line 169-221: The effect uses the isEmbedded prop inside its async work
(affecting className logic) but the dependency array only lists [code, lang], so
add isEmbedded to the dependency array of the useEffect that contains the async
IIFE (the block that sets cancelled, calls getHighlighter/getMermaid and calls
setCodeElement) to ensure the effect re-runs when isEmbedded changes; update the
dependency list from [code, lang] to [code, lang, isEmbedded] (or include
isEmbedded alongside existing deps) so styling remains correct and React rules
of hooks are satisfied.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 389c9f92-3560-462a-8ea4-199f99c2fe3a

📥 Commits

Reviewing files that changed from the base of the PR and between 1e79903 and 072f8dc.

📒 Files selected for processing (2)
  • src/components/CopyPageDropdown.tsx
  • src/components/markdown/CodeBlock.tsx

- Add null safety for preAttributes.class in mermaid rendering
- Add isEmbedded to effect dependency array
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/markdown/CodeBlock.tsx`:
- Around line 215-217: The catch block in CodeBlock.tsx only logs errors ("Shiki
highlighting failed") and leaves the component blank; update the error path to
set a real fallback UI so users see a sensible placeholder: in the catch of the
highlighting routine (the function that performs Shiki highlighting, e.g.,
highlightCode or the async block around shiki), set the component state used to
render output (e.g., setHighlightedHtml / setRenderState) to a fallback markup
for normal code blocks (plain escaped code wrapped in a
<pre><code>...)</code></pre> and for mermaid blocks call the existing mermaid
render path fallback (renderMermaid) or set the mermaid state to an empty <svg
/> placeholder; ensure the fallback path is used by the render branch that reads
the highlighted state so users see the placeholder instead of nothing.
- Around line 191-193: The current mermaid.render call uses a constant id 'foo',
which causes SVG id collisions; update the CodeBlock.tsx rendering logic to
generate a unique id per render (for example using a short UUID, a timestamp +
counter, or the trimmedCode hash) and pass that id into mermaid.render instead
of 'foo', ensure the same generated id is used if caching via genSvgMap for
trimmedCode so lookups remain consistent, and obtain the mermaid instance via
getMermaid() as before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1ece5f9c-9186-4471-83b9-ed4a626d6cea

📥 Commits

Reviewing files that changed from the base of the PR and between 072f8dc and d8e2889.

📒 Files selected for processing (1)
  • src/components/markdown/CodeBlock.tsx

Comment on lines +191 to +193
const mermaid = await getMermaid()
const { svg } = await mermaid.render('foo', trimmedCode)
genSvgMap.set(trimmedCode, svg)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For Mermaid v11.11.0, what does the first idargument ofmermaid.render(id, text) control, and is it required/recommended to be unique per rendered diagram instance?

💡 Result:

In Mermaid v11.11.0, the first argument id in await mermaid.render(id, text) is used as the DOM id of the generated root <svg> element. Mermaid’s internal renderer literally does .append('svg').attr('id', id). [1]

It’s effectively required (the parameter is not optional) and it’s strongly recommended to be unique per rendered diagram instance in the same document, because Mermaid also derives other temporary element IDs/selectors from it (e.g. 'd' + id, 'i' + id) and will remove any existing elements with the same id before rendering—so reusing an id can overwrite/remove a previously-rendered diagram or otherwise cause collisions. [2]

Sources
[1] https://raw.githubusercontent.com/mermaid-js/mermaid/9c85521/packages/mermaid/src/mermaidAPI.ts
[2] https://raw.githubusercontent.com/mermaid-js/mermaid/9c85521/packages/mermaid/src/mermaidAPI.ts


🏁 Script executed:

cat -n src/components/markdown/CodeBlock.tsx | sed -n '185,200p'

Repository: TanStack/tanstack.com

Length of output: 776


🏁 Script executed:

# Check broader context - how many mermaid blocks might be rendered on a single page
# and if this function is called multiple times
grep -n "lang === 'mermaid'" src/components/markdown/CodeBlock.tsx

Repository: TanStack/tanstack.com

Length of output: 167


Use a unique Mermaid render ID instead of a constant.

The hardcoded 'foo' ID in mermaid.render('foo', trimmedCode) at line 192 will cause duplicate SVG IDs and broken marker/style references when multiple Mermaid blocks are rendered on the same page. According to Mermaid's API, the id parameter becomes the DOM id of the root <svg> element, and reusing the same ID can cause collisions, overwrites, and derived ID conflicts within Mermaid's internal selectors.

Generate a unique ID for each render call:

Suggested fix
 let mermaidInstance: Mermaid | null = null
+let mermaidRenderCounter = 0
 const genSvgMap = new Map<string, string>()

 ...
-                const { svg } = await mermaid.render('foo', trimmedCode)
+                const renderId = `mermaid-${++mermaidRenderCounter}`
+                const { svg } = await mermaid.render(renderId, trimmedCode)
                 genSvgMap.set(trimmedCode, svg)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/markdown/CodeBlock.tsx` around lines 191 - 193, The current
mermaid.render call uses a constant id 'foo', which causes SVG id collisions;
update the CodeBlock.tsx rendering logic to generate a unique id per render (for
example using a short UUID, a timestamp + counter, or the trimmedCode hash) and
pass that id into mermaid.render instead of 'foo', ensure the same generated id
is used if caching via genSvgMap for trimmedCode so lookups remain consistent,
and obtain the mermaid instance via getMermaid() as before.

Comment on lines +215 to +217
} catch (err) {
console.warn('Shiki highlighting failed:', err)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add a real fallback UI in the error path.

When highlighting/rendering fails, the catch path only logs. For Mermaid blocks, the initial state can stay visually empty (placeholder <svg />), so users still get a blank block on failure.

Suggested fix
       } catch (err) {
         console.warn('Shiki highlighting failed:', err)
+        if (!cancelled) {
+          setCodeElement(
+            <pre
+              ref={ref}
+              className="h-full p-4 bg-white dark:bg-gray-950 text-gray-800 dark:text-gray-200"
+            >
+              <code>{code || ''}</code>
+            </pre>,
+          )
+        }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/markdown/CodeBlock.tsx` around lines 215 - 217, The catch
block in CodeBlock.tsx only logs errors ("Shiki highlighting failed") and leaves
the component blank; update the error path to set a real fallback UI so users
see a sensible placeholder: in the catch of the highlighting routine (the
function that performs Shiki highlighting, e.g., highlightCode or the async
block around shiki), set the component state used to render output (e.g.,
setHighlightedHtml / setRenderState) to a fallback markup for normal code blocks
(plain escaped code wrapped in a <pre><code>...)</code></pre> and for mermaid
blocks call the existing mermaid render path fallback (renderMermaid) or set the
mermaid state to an empty <svg /> placeholder; ensure the fallback path is used
by the render branch that reads the highlighted state so users see the
placeholder instead of nothing.

@LadyBluenotes
Copy link
Member

I appreciate the effort you put into this, however, the issue was due to a prev commit. If we end up needing to revisit this implementation due to any errors, we can revisit opening it!

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.

Critical: Documentation Site Completely Broken tanstack start authentication docs broken Website runtime error

2 participants