惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
CERT Recently Published Vulnerability Notes
GbyAI
GbyAI
Google DeepMind News
Google DeepMind News
C
CXSECURITY Database RSS Feed - CXSecurity.com
AWS News Blog
AWS News Blog
V
Vulnerabilities – Threatpost
T
Tenable Blog
P
Proofpoint News Feed
C
Check Point Blog
T
The Exploit Database - CXSecurity.com
F
Full Disclosure
P
Privacy & Cybersecurity Law Blog
美团技术团队
U
Unit 42
C
Cyber Attacks, Cyber Crime and Cyber Security
阮一峰的网络日志
阮一峰的网络日志
Simon Willison's Weblog
Simon Willison's Weblog
量子位
AI
AI
Spread Privacy
Spread Privacy
Help Net Security
Help Net Security
Know Your Adversary
Know Your Adversary
IT之家
IT之家
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
P
Proofpoint News Feed
Recent Commits to openclaw:main
Recent Commits to openclaw:main
L
LangChain Blog
I
Intezer
T
The Blog of Author Tim Ferriss
爱范儿
爱范儿
月光博客
月光博客
Recorded Future
Recorded Future
O
OpenAI News
WordPress大学
WordPress大学
Microsoft Security Blog
Microsoft Security Blog
J
Java Code Geeks
Y
Y Combinator Blog
Engineering at Meta
Engineering at Meta
S
Security @ Cisco Blogs
Recent Announcements
Recent Announcements
P
Privacy International News Feed
NISL@THU
NISL@THU
MongoDB | Blog
MongoDB | Blog
W
WeLiveSecurity
B
Blog RSS Feed
Blog — PlanetScale
Blog — PlanetScale
博客园 - Franky
Cyberwarzone
Cyberwarzone
H
Hacker News: Front Page

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Building a Dark Mode System in Next.js App Router — Without Layout Flash
Aon infotech · 2026-06-21 · via DEV Community

Dark mode sounds simple until you implement it. Then you discover the flash.

On first load, before JavaScript runs, your page renders with the default theme. Then the theme switcher kicks in. For a fraction of a second — sometimes longer on slow connections — users see the wrong theme before it corrects itself.

This is the flash of unstyled content (FOUC) applied to theming, and it's one of the more annoying UX problems to solve correctly in Next.js App Router.

Here's the approach that works, which I used building the theming system for a free AI image generation tool.


Why the Flash Happens

Next.js App Router renders on the server by default. The server doesn't know the user's theme preference — that's stored in localStorage or a cookie on the client. So the server renders with the default theme, sends that HTML to the browser, and then JavaScript runs and corrects the theme.

The gap between HTML arriving and JavaScript running = the flash.


The Solution — Inline Script Before React Hydration

The key insight: to prevent the flash, you need to apply the theme before React hydrates. This means a synchronous inline script in the <head> that reads the preference and applies it immediately.

// app/layout.js
export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <script
          dangerouslySetInnerHTML={{
            __html: `
              (function() {
                try {
                  var theme = localStorage.getItem('theme');
                  var prefersDark = window.matchMedia(
                    '(prefers-color-scheme: dark)'
                  ).matches;

                  if (theme === 'dark' || (!theme && prefersDark)) {
                    document.documentElement.classList.add('dark');
                  }
                } catch (e) {}
              })();
            `,
          }}
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

Why suppressHydrationWarning? React will complain that the server-rendered HTML doesn't match the client (because we've added a dark class that the server didn't know about). This prop tells React to ignore that mismatch on the html element specifically.

Why try/catch? localStorage can throw in certain browser environments (private mode, security policies). Wrapping prevents a JavaScript error from breaking the page.


The Theme Context

// contexts/ThemeContext.js
'use client';
import { createContext, useContext, useEffect, useState } from 'react';

const ThemeContext = createContext({
  theme: 'system',
  setTheme: () => {},
  resolvedTheme: 'light',
});

export function ThemeProvider({ children }) {
  const [theme, setThemeState] = useState('system');
  const [resolvedTheme, setResolvedTheme] = useState('light');

  useEffect(() => {
    // Read stored preference on mount
    const stored = localStorage.getItem('theme') ?? 'system';
    setThemeState(stored);
  }, []);

  useEffect(() => {
    const root = document.documentElement;

    if (theme === 'dark') {
      root.classList.add('dark');
      setResolvedTheme('dark');
      localStorage.setItem('theme', 'dark');
    } else if (theme === 'light') {
      root.classList.remove('dark');
      setResolvedTheme('light');
      localStorage.setItem('theme', 'light');
    } else {
      // System preference
      localStorage.removeItem('theme');
      const prefersDark = window.matchMedia(
        '(prefers-color-scheme: dark)'
      ).matches;

      if (prefersDark) {
        root.classList.add('dark');
        setResolvedTheme('dark');
      } else {
        root.classList.remove('dark');
        setResolvedTheme('light');
      }
    }
  }, [theme]);

  // Listen for system preference changes
  useEffect(() => {
    if (theme !== 'system') return;

    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    const handler = (e) => {
      setResolvedTheme(e.matches ? 'dark' : 'light');
      document.documentElement.classList.toggle('dark', e.matches);
    };

    mediaQuery.addEventListener('change', handler);
    return () => mediaQuery.removeEventListener('change', handler);
  }, [theme]);

  const setTheme = (newTheme) => setThemeState(newTheme);

  return (
    <ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export const useTheme = () => useContext(ThemeContext);


The Toggle Component

// components/ThemeToggle.jsx
'use client';
import { useTheme } from '@/contexts/ThemeContext';

export function ThemeToggle() {
  const { theme, setTheme, resolvedTheme } = useTheme();

  return (
    <div className="flex items-center gap-2">
      <button
        onClick={() => setTheme('light')}
        className={`p-2 rounded-lg transition-colors ${
          theme === 'light'
            ? 'bg-neutral-200 dark:bg-neutral-700'
            : 'hover:bg-neutral-100 dark:hover:bg-neutral-800'
        }`}
        aria-label="Light mode"
      >
        ☀️
      </button>

      <button
        onClick={() => setTheme('system')}
        className={`p-2 rounded-lg transition-colors ${
          theme === 'system'
            ? 'bg-neutral-200 dark:bg-neutral-700'
            : 'hover:bg-neutral-100 dark:hover:bg-neutral-800'
        }`}
        aria-label="System preference"
      >
        💻
      </button>

      <button
        onClick={() => setTheme('dark')}
        className={`p-2 rounded-lg transition-colors ${
          theme === 'dark'
            ? 'bg-neutral-200 dark:bg-neutral-700'
            : 'hover:bg-neutral-100 dark:hover:bg-neutral-800'
        }`}
        aria-label="Dark mode"
      >
        🌙
      </button>
    </div>
  );
}


CSS Variables for Theming

Using CSS variables with Tailwind's dark mode makes theme-aware colors straightforward:

/* globals.css */
:root {
  --background: 255 255 255;
  --foreground: 15 15 15;
  --card: 248 248 248;
  --border: 226 226 226;
  --muted: 115 115 115;
}

.dark {
  --background: 10 10 10;
  --foreground: 245 245 245;
  --card: 24 24 24;
  --border: 38 38 38;
  --muted: 163 163 163;
}

// tailwind.config.js
module.exports = {
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        background: 'rgb(var(--background) / <alpha-value>)',
        foreground: 'rgb(var(--foreground) / <alpha-value>)',
        card: 'rgb(var(--card) / <alpha-value>)',
        border: 'rgb(var(--border) / <alpha-value>)',
        muted: 'rgb(var(--muted) / <alpha-value>)',
      },
    },
  },
};

Now you can use bg-background, text-foreground, border-border anywhere in your components and they switch automatically.


Common Pitfalls

Forgetting suppressHydrationWarning: React will throw hydration warnings in development. Add it to the <html> element specifically — not globally.

Using useEffect to read localStorage on mount: This still causes a flash, just a shorter one. The inline script approach is necessary to eliminate the flash entirely.

Not handling the system preference case: If the user has "system" preference and changes their OS theme while on your site, nothing updates unless you listen for prefers-color-scheme changes.

Images that don't adapt: Dark and light mode images need separate handling. Either use CSS filters (filter: invert(1) on dark backgrounds) or conditionally render different image sources using resolvedTheme.


Testing

// Verify no flash on load
// 1. Set theme to dark in localStorage
// 2. Hard refresh — should load dark immediately
// 3. Clear localStorage — should follow system preference
// 4. Toggle OS dark mode — should update without refresh (system mode)

The inline script approach eliminates the flash across all browsers I've tested. The trade-off is a small inline script in every page's <head> — negligible performance impact for the user experience improvement it provides.


What I Built This On

This theming system is running in production on pixova.io — the dark/light toggle in the navigation uses exactly this approach. No flash, system preference respected, instant switching.

Questions about specific edge cases? Comments open.


Handling Server Components With Theme

One tricky area in App Router: server components don't have access to the client's theme preference. This affects things like server-rendered images or content that should vary by theme.

Option 1 — CSS-only solution (preferred):
Use CSS to show/hide variants based on the .dark class:

/* Show light version by default, dark version when .dark is active */
.logo-light { display: block; }
.logo-dark { display: none; }

.dark .logo-light { display: none; }
.dark .logo-dark { display: block; }

This requires no JavaScript and works instantly since the .dark class is already applied by the inline script.

Option 2 — Client component wrapper:
Wrap theme-sensitive server components in a client component that reads resolvedTheme:

'use client';
import { useTheme } from '@/contexts/ThemeContext';

export function ThemedImage({ lightSrc, darkSrc, alt }) {
  const { resolvedTheme } = useTheme();

  return (
    <img
      src={resolvedTheme === 'dark' ? darkSrc : lightSrc}
      alt={alt}
    />
  );
}

The downside: this introduces a client boundary just for the image. The CSS approach is cleaner for most cases.


Cookie-Based Alternative (For SSR Accuracy)

If you need the server to know the theme (for server-rendered charts, personalized content, or avoiding any flash at all), store the preference in a cookie rather than localStorage.

// middleware.js — read theme cookie, add to response
import { NextResponse } from 'next/server';

export function middleware(request) {
  const theme = request.cookies.get('theme')?.value ?? 'system';
  const response = NextResponse.next();

  // Pass theme as a header the layout can read
  response.headers.set('x-theme', theme);
  return response;
}

// app/layout.js — read theme from headers for SSR
import { headers } from 'next/headers';

export default function RootLayout({ children }) {
  const headersList = headers();
  const theme = headersList.get('x-theme') ?? 'system';
  const isDark = theme === 'dark';

  return (
    <html 
      lang="en" 
      className={isDark ? 'dark' : ''}
      suppressHydrationWarning
    >
      ...
    </html>
  );
}

This eliminates the flash entirely — even before JavaScript runs — because the server knows the theme. The trade-off: requires a middleware layer and adds a cookie to every request.

For most use cases, the inline script approach is sufficient and simpler. The cookie approach is worth implementing if you have server-rendered content that needs to match the theme exactly on first load.


Summary

The flash-free dark mode stack in Next.js App Router:

  1. Inline script in <head> reads preference and applies class before hydration
  2. suppressHydrationWarning on <html> to suppress React's mismatch warning
  3. Theme context manages state and localStorage persistence
  4. CSS variables handle color switching
  5. System preference listener updates in real-time for "system" mode The inline script is the non-obvious piece that most implementations miss. Without it, no amount of context optimization eliminates the flash.