← Back to Blog
Dark ModeTailwindNext.jsThailand

Dark Mode Development for Thai Users

18 May 2026 · by Yunmin Shin

Why Dark Mode Is Worth Implementing

Dark mode isn't a cosmetic extra in Thailand's market — it's a feature your users are actively reaching for. Most Thai visitors to a small business site are on Android, and a large share of those devices (Samsung A-series and mid-range Xiaomi/OPPO models especially) use OLED screens, where a true black background measurably extends battery life compared to a white one. Combine that with how people actually browse: someone comparing filler prices on bangkokfillers.com at 11pm in bed, or scrolling a restaurant's menu on snsstopper.com while deciding on dinner after the lights are already off, and dark mode stops being a nice-to-have. A site that stays blindingly white regardless of context reads as dated, and worse, it's mildly painful to use exactly when a lot of browsing happens.

The technical implementation in Next.js with Tailwind CSS is straightforward once you follow the right pattern from the start. Get it wrong and you get the classic bug that undermines the whole feature: a flash of the wrong theme on every page load, which is often more jarring than having no dark mode at all.

How Does Tailwind's Dark Mode Work?

Tailwind v4 supports dark mode via the dark: variant. Any class prefixed with dark: applies only when dark mode is active:

<div class="bg-white text-gray-900 dark:bg-gray-950 dark:text-gray-100">
  Content here
</div>

Tailwind supports two dark mode strategies:

  1. media strategy — Responds to the operating system's prefers-color-scheme: dark media query. Automatic, requires no JavaScript, but gives the user no control.
  2. class strategy — Applies dark mode when the dark class is present on the <html> element. Allows manual toggling but requires JavaScript.

For a clinic or restaurant site, use the class strategy. Some users will still want to override their system setting — a patient reading before/after photos in a bright waiting room, for instance, may prefer light mode even if their phone is set to dark system-wide. Giving people a toggle rather than forcing the OS preference on them avoids that friction.

How Do You Implement a Theme Toggle?

Create a ThemeProvider client component that manages the theme state and exposes it to the rest of the app — including any embedded LINE booking widget or contact form that needs to match the surrounding page:

"use client";
import { createContext, useContext, useEffect, useState } from "react";

type Theme = "light" | "dark" | "system";

// Apply theme to <html> element
function applyTheme(theme: Theme) {
  const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
  const isDark = theme === "dark" || (theme === "system" && prefersDark);
  document.documentElement.classList.toggle("dark", isDark);
}

Store the user's preference in localStorage so it persists across visits — a returning patient checking their appointment confirmation shouldn't have to re-toggle every time:

useEffect(() => {
  const saved = localStorage.getItem("theme") as Theme ?? "system";
  setTheme(saved);
  applyTheme(saved);
}, []);

Place the toggle somewhere consistent and small — a corner of the header, not competing with your LINE contact button, which should remain the most visually prominent call-to-action on the page regardless of theme.

How Do You Prevent the Flash of Wrong Theme?

The "flash" problem occurs because React hydrates on the client after the server has already rendered the HTML. If the server renders light-mode HTML and the user's saved preference is dark, there's a brief flash of light before JavaScript runs and switches to dark. On a slow 4G connection in an outer Bangkok district, that flash can last long enough to be genuinely distracting — JavaScript execution and hydration take longer on mid-range hardware than on a developer's laptop.

The solution is a blocking inline script in the <head> that runs before the page paints:

<script dangerouslySetInnerHTML={{ __html: `
  (function() {
    var theme = localStorage.getItem('theme') || 'system';
    var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    if (theme === 'dark' || (theme === 'system' && prefersDark)) {
      document.documentElement.classList.add('dark');
    }
  })();
` }} />

Add this script to your root layout.tsx inside the <head>. It executes synchronously before the browser paints any content, which eliminates the flash entirely — not just makes it shorter.

What About SSR and Hydration Mismatches?

Server-rendered HTML cannot know the user's localStorage value. This can cause React hydration mismatches if you render different content based on theme server-side. The safest approach: render all content server-side without theme-dependent differences, and let Tailwind's CSS handle the visual switch. Only diverge based on theme in purely visual (CSS) ways, not in structural (JSX) ways — don't conditionally render different components for light versus dark.

What Should You Watch Out for on Clinic and Medical Sites?

Dark mode introduces one hazard specific to aesthetic and medical clinic sites: before/after photo galleries. Skin tone, redness, and bruising in a botox or filler before/after image need to render with color accuracy that matches the light-mode version — never apply a dark: filter, mix-blend-mode, or opacity adjustment directly to photo content, only to the surrounding chrome (backgrounds, cards, text). A photo that looks subtly different in dark mode undermines the credibility of the result you're showing, which matters more on a clinic site than almost any other content type. Treatment price tables and consent-form text should also keep sufficient contrast in dark mode — WCAG AA minimum, ideally AAA for body text, since patients are often reading this content carefully while making a real decision.

How Do You Test Dark Mode on Real Thai Devices?

Chrome DevTools' emulated dark mode is a starting point, not a substitute for testing on the hardware your users actually carry. Load the live site on a mid-range Samsung Galaxy A-series or Xiaomi Redmi with OLED display and check for common issues that don't show up in emulation: pure black (#000000) backgrounds that make anti-aliased Thai text look slightly fuzzy at the edges (a very dark gray like #0a0a0a usually reads better), icons or logos with transparent backgrounds that disappear or clash, and any third-party embed — a LINE chat widget, a Google Maps embed, a payment iframe — that ignores your dark: classes entirely and needs its own wrapper treatment.

Ready to Build Something Fast?

Get a free quote. We reply within 24 hours.

Ready to build something fast and scalable?

Get a free project quote. We reply within 24 hours.

Get a Free Quote →