A global color-scheme: dark leaks into print/PDF output unless you override it
TIL that setting color-scheme: dark once on html/body for a dark-themed site makes the browser paint its default canvas background dark everywhere that property cascades — including print and PDF output — unless @media print resets it.
Today I learned that color-scheme: dark isn't scoped to "the site's dark theme" the way I assumed — it's a standing instruction to the browser's own UA styles, and it applies to print and PDF rendering just as much as the screen.
I generate /resume.pdf for this site by running a headless-Chromium print pass over the /resume page's existing print stylesheet. The page itself renders fine on screen and prints fine from the browser's own print dialog on a fresh profile — but the generated PDF had a dark grey border bleeding around the white resume content area. No element was explicitly styled grey; the color came from the page's own default background.
:root {
color-scheme: dark;
}This line was set once, globally, so the browser applies its dark canvas color to any box that doesn't set its own background, including the printed page background. @media print doesn't automatically undo it — print styles only override what they explicitly target.
@media print {
html,
body {
background: #fff;
color-scheme: light;
}
}Why it works #
color-scheme tells the browser which built-in palette to paint for anything you haven't styled yourself: the root canvas, scrollbars, form control chrome. It's not a component-level toggle, it's a UA-rendering hint that cascades like any other property — through every media query, print included, unless something more specific overrides it there.
Any site with a global color-scheme: dark needs a @media print { color-scheme: light; } twin the moment you care about print or PDF output — otherwise the "dark theme" leaks into a context that was never meant to have one.
Related content
Sprout: A Next.js + Supabase Template Where Feedback Writes Its Own Git Branch
A template that wires a public feedback form straight into GitHub — every submission becomes a git branch via Claude Code, with no human touching the pipeline in between.
A JSX expression-container attribute silently loses its value in next-mdx-remote
TIL that generating attr={"..."} into MDX source instead of attr="..." made a prop come back as an empty object on every render, with no error anywhere in the pipeline — found while writing the Obsidian-callout preprocessor for this site.
One Fuse.js index can search five different content types at once
TIL that a single flat Fuse.js index with weighted keys is enough to search blog posts, TIL entries, projects, case studies, and snippets together — no separate index or backend needed per collection.