← Back to TILs

p tags and their horrible recoloring

March 30, 2025

Why That p Tag Refused to Recolor

I hit a weird rendering bug recently while implementing theme switching (light ↔ dark) on this site. Most of the UI flipped colors smoothly, but the text inside some <p> tags would… hesitate. Like it didn’t get the memo. Sometimes it would flash white. Sometimes it would stay stuck in the old theme for a few frames. Very glitchy. Very annoying.

What made it more confusing: this didn’t happen uniformly across all text elements. It happened mostly with <p> tags, I am unsure if this happens for other tags but I am very sure p tags are the culprits


GPU Hacks

Like any self-respecting dev faced with flickering visuals, I checked on some random blogs, tried to do chatgpt but nothing worked, I then reached for will-change and transform: translateZ(0) — the classic GPU-acceleration tricks, I assumed due to the size of content in the p tag GPU acceleration might just work

p {
  will-change: color;
  transform: translateZ(0);
}

Did that help?

Not really. The flickering persisted. In fact, it sometimes got worse — the browser seemed to be fighting itself on when to repaint the element.


The dreaded RCA

The moment of clarity came when I popped open DevTools and carefully stepped through the DOM during a theme toggle. Here’s what I saw:

  • The <body> class was toggling correctly (dark was being added or removed).
  • Tailwind’s dark mode styles were working for most elements.
  • But the <p> tag itself wasn’t being repainted reliably.

I used the DevTools rendering overlay and noticed that the paragraph didn’t always trigger a repaint on theme change. Even though it should have — its color was different between themes.

Here’s the best theory I could confirm through testing:

The browser had internally cached how to render that <p> based on its previous style tree. Due to a combination of display: block, lack of layout change, and font anti-aliasing layers, it deferred repainting the paragraph when the parent theme changed — especially when using prefers-color-scheme + class-based toggling (like Tailwind).

So even though the intent was to repaint, the layout engine said, “eh, it looks the same structurally, skip it.”


The Fix (aka: The Dumb Hack That Works)

I tried GPU hacks. I tried isolating the component with contain: paint. I even tried backface-visibility: hidden. All of them were unstable or brittle.

What finally worked?

Wrapping the paragraph in a div :D

<div>
  <p className="transition-colors text-black dark:text-white">...</p>
</div>

That’s it.

By giving the <p> tag a parent container, it basically nudged the browser to treat it as a fresh renderable surface. It always repainted on theme change after that — no flickering, no ghosting, no visual lag.

It’s likely that the browser promotes <div>s with changing children more aggressively than <p>s, which are often treated as layout-static. The wrapper forced a new composite layer.


Before (flickering):

<body class="dark">
  <p class="text-black dark:text-white">I flicker!</p>
</body>

Rendered structure:

[ body.dark ]
  └─ p  — text may not repaint

After (stable):

<body class="dark">
  <div>
    <p class="text-black dark:text-white">I behave.</p>
  </div>
</body>

Rendered structure:

[ body.dark ]
  └─ div
       └─ p  — repaint triggered

Takeaway

I have no clue what the main take away is apart from “Sometimes, a plain <div> is all it takes”

If you’re seeing weird flickering on dark mode toggles, don’t overthink it. Wrap it.

css
ESC