<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Selja Sampe Rante</title>
    <description>The latest articles on DEV Community by Selja Sampe Rante (@seljarante).</description>
    <link>https://dev.to/seljarante</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3622048%2F78521b74-b2cf-4241-86ae-5f8fc0fd8427.jpg</url>
      <title>DEV Community: Selja Sampe Rante</title>
      <link>https://dev.to/seljarante</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/seljarante"/>
    <language>en</language>
    <item>
      <title>Implementasi Theme Switcher di NextJS Tanpa Ribet, Flicker Free + Persist via localStorage Pakai Context API.</title>
      <dc:creator>Selja Sampe Rante</dc:creator>
      <pubDate>Fri, 21 Nov 2025 11:15:33 +0000</pubDate>
      <link>https://dev.to/seljarante/implementasi-theme-switcher-di-nextjs-tanpa-ribet-flicker-free-persist-via-localstorage-pakai-24ai</link>
      <guid>https://dev.to/seljarante/implementasi-theme-switcher-di-nextjs-tanpa-ribet-flicker-free-persist-via-localstorage-pakai-24ai</guid>
      <description>&lt;p&gt;Halo, kali ini gua mau share ini karena gua pengen coba implement Theme Switcher di nextjs tapi yg tetep bisa terintegrasi sama tailwind.&lt;/p&gt;

&lt;p&gt;Cara pertama gua coba ngikutin di tailwind docs pake variant &lt;code&gt;dark:*&lt;/code&gt;.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faqab1qcw2jwkar8sgfgl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faqab1qcw2jwkar8sgfgl.png" alt="tailwidn dark variant usage" width="441" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tapi makin lama, nulis variant dark: berulang-ulang itu bikin capek. Class jadi gemuk, dan kode jadi keliatan berantakan.&lt;/p&gt;

&lt;p&gt;Pas gua ngulik-ngulik, gua nemu cara yang jauh lebih simpel dan lebih “manusiawi”. Nggak perlu variant, nggak perlu trik aneh, dan bisa punya banyak tema sekaligus, nggak cuma light &amp;amp; dark tapi tetep full terintegrasi sama Tailwind.&lt;/p&gt;

&lt;p&gt;Cara gantinya cuma 1 baris coyy!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fywwm2ctxd4khewbww9k0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fywwm2ctxd4khewbww9k0.jpg" alt="code" width="800" height="135"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;That it&lt;/em&gt;, nggak usah ngetik &lt;code&gt;dark:&lt;/code&gt; lagi. Warna otomatis ngikutin class tema yang kita set.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foisf75bxuuy3nn3gnhj3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foisf75bxuuy3nn3gnhj3.gif" alt="Hasil gif Contoh sederhana" width="540" height="387"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;em&gt;Full Code Ada di Akhir.&lt;/em&gt;
&lt;/h3&gt;


&lt;h3&gt;
  
  
  Buat Project Nextjsnya
&lt;/h3&gt;

&lt;p&gt;Kali ini gua pake Next.js 15 + Tailwind v4 (kalau ada yang request, gua siap bikinin versi Tailwind v3). Semuanya tanpa install library tambahan.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmuj00ykvclkfq03nsq80.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmuj00ykvclkfq03nsq80.png" alt="code image" width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Karena projectnya simpel, gua cukup pake Context API sebagai state management.&lt;/p&gt;


&lt;h3&gt;
  
  
  Step 0. Siapin Warna Tema
&lt;/h3&gt;

&lt;p&gt;Sebagai contoh, gua bikin 3 tema: default/light, dark, dan green.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//global.css
:root {
  --background: #fbfbfe;
  --text: #040316;
  --primary: #2f27ce;
  --secondary: #dddbff;
  --accent: #443dff;
}

.dark-theme {
  --background: #010104;
  --text: #eae9fc;
  --primary: #3a31d8;
  --secondary: #020024;
  --accent: #0600c2;
}

.green-theme {
  --background: #ddfdeb;
  --text: #033018;
  --primary: #098b43;
  --secondary: #97dee3;
  --accent: #5fb9d5;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jan lupa kita set di tailwind confignya.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@theme inline {
  --font-sans: var(--font-geist-sans);
  --font-mono: var(--font-geist-mono);

  --color-background: var(--background);
  --color-foreground: var(--text);
  --color-primary: var(--primary);
  --color-secondary: var(--secondary);
  --color-accent: var(--accent);
}

body {
  background: var(--background);
  color: var(--foreground);
  font-family: Arial, Helvetica, sans-serif;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Step 1. Setup Context-nya
&lt;/h3&gt;

&lt;p&gt;Kita setup contextnya di folder app &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Forjc1g8tjvnqucg4n4u6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Forjc1g8tjvnqucg4n4u6.png" alt="code image" width="508" height="268"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Kasih prefix underscore agar foldernya diabaikan sama routing nextjs(optional)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Folder konteks gua taruh di app/_context biar aman dari routing Next.js.&lt;/p&gt;

&lt;p&gt;Struktur kerjanya sederhana:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bikin ThemeContext&lt;/li&gt;
&lt;li&gt;Bikin ThemeProvider&lt;/li&gt;
&lt;li&gt;Bikin useTheme()&lt;/li&gt;
&lt;/ol&gt;




&lt;h4&gt;
  
  
  Setup ThemeContext + Types
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const THEMES = ["default", "dark-theme", "green-theme"] as const;

type ThemeType = (typeof THEMES)[number];

type ThemeContextType = {
  theme: ThemeType;
  setTheme: (theme: ThemeType) =&amp;gt; void;
};

const ThemeContext = createContext&amp;lt;ThemeContextType | undefined&amp;gt;(undefined);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Kenapa kita kasih as const di var themesnya? nanti saya buat penjelasan detail nya yaa. Intinya biar jadi readonly dan tiap item dianggap literal type, bukan string biasa, jadinya lebih enak dipakai.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h4&gt;
  
  
  Buat isi ThemeProvider
&lt;/h4&gt;

&lt;p&gt;Nah ini part yang paling penting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const ThemeProvider = ({ children }: { children: ReactNode }) =&amp;gt; {
  const [theme, setTheme] = useState&amp;lt;ThemeType&amp;gt;("default");

  //value tema berubah -&amp;gt; remove semua theme class di body -&amp;gt; kalo bukan theme default tambah data theme yang terbaru ke body class.
  useEffect(() =&amp;gt; {
    const body = document.body;
    body.classList.remove(...THEMES);

    if (theme !== "default") {
      body.classList.add(theme);
    }
  }, [theme]);

  return (
    &amp;lt;ThemeContext.Provider value={{ theme, setTheme }}&amp;gt;
      {children}
    &amp;lt;/ThemeContext.Provider&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Logiknya simpel: kita cuma ganti class pada body htmlnya. CSS var yang dipake tailwind yang sudah kita set bakal otomatis berubah.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h4&gt;
  
  
  Bikin Hook useTheme()
&lt;/h4&gt;

&lt;p&gt;Biar bisa langsung di pake di component.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const useTheme = () =&amp;gt; {
  const context = useContext(ThemeContext);

  if (!context) {
    throw new Error("useTheme must be used within a ThemeProvider");
  }

  return context;
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jan Lupa export yang diperlukan ygy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export { ThemeProvider, THEMES, useTheme };
export type { ThemeType };
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Pasang ThemeProvider di layout
&lt;/h4&gt;

&lt;p&gt;Kita masukin ThemeProvidernya RootLayout&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; return (
    &amp;lt;html lang="en"&amp;gt;
      &amp;lt;body
        className={`${geistSans.variable} ${geistMono.variable} antialiased `}
      &amp;gt;
        &amp;lt;ThemeProvider&amp;gt;{children}&amp;lt;/ThemeProvider&amp;gt;
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Pemakaian
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Pilih Tema Manual
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  export default function Home() {
  const { theme, setTheme } = useTheme();

  return (
    &amp;lt;div className="..."&amp;gt;
      ...

      &amp;lt;div className=" flex gap-4"&amp;gt;
        {THEMES.map((t) =&amp;gt; (
          &amp;lt;button
            key={t}
            className="..."
            onClick={() =&amp;gt; setTheme(t)}
          &amp;gt;
            {t === theme ? "✓ " : ""}
            {t}
          &amp;lt;/button&amp;gt;
        ))}
      &amp;lt;/div&amp;gt;

      ...
     &amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4qu04tcv2m6qxgsh3zia.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4qu04tcv2m6qxgsh3zia.gif" alt="code image" width="500" height="613"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h4&gt;
  
  
  Toggle Dark Mode
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; const handleToggle = () =&amp;gt; {
    setTheme(theme == "default" ? "dark-theme" : "default");
  };

  return (
    &amp;lt;div className="..."&amp;gt;
     ...

      &amp;lt;div className="..."&amp;gt;
        &amp;lt;label className="..."&amp;gt;
          &amp;lt;span className="..."&amp;gt;
            Light Theme
          &amp;lt;/span&amp;gt;
          &amp;lt;input
            type="checkbox"
            value={theme}
            className="sr-only peer"
            onChange={handleToggle}
            checked={theme === "dark-theme"}
          /&amp;gt;
          &amp;lt;div className="..."&amp;gt;&amp;lt;/div&amp;gt;
          &amp;lt;span className="..."&amp;gt;
            Dark Theme
          &amp;lt;/span&amp;gt;
        &amp;lt;/label&amp;gt;
      &amp;lt;/div&amp;gt;

      ...
    &amp;lt;/div&amp;gt;
  );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frbzo9eqo60900bthwj4q.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frbzo9eqo60900bthwj4q.gif" alt="code image" width="793" height="673"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h4&gt;
  
  
  Persist Via localStorage (Optional)
&lt;/h4&gt;

&lt;p&gt;Kita tinggal refactor ThemeContextnya agar make dan nyimpen theme statenya di localStorage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  // kita ganti default value statenya dari localStorage kalau ada
  const [theme, setTheme] = useState&amp;lt;ThemeType&amp;gt;(() =&amp;gt; {
    //karena saat pertama render itu di server, kita buat fallback defaultnya
    if (typeof window === "undefined") return "default";

    //kalau sudah run di client baru kita akses localStoragenya
    const savedTheme = localStorage.getItem("theme") as ThemeType | null;
    return savedTheme &amp;amp;&amp;amp; THEMES.includes(savedTheme) ? savedTheme : "default";
  });

  //untuk ngefix hydration error
  //karena perbedaan theme di server dan di client 
  const [mounted, setMounted] = useState(false);

  useEffect(() =&amp;gt; {
    setMounted(true);
  }, []);

  useEffect(() =&amp;gt; {
    localStorage.setItem("theme", theme);
  }, [theme]);

  ...

  //kita hanya merender uinya jika run di client
  if (!mounted) return null;

   return (
    &amp;lt;ThemeContext.Provider value={{ theme, setTheme }}&amp;gt;
      {children}
    &amp;lt;/ThemeContext.Provider&amp;gt;
  );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🔗 Source Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/kangspbu/latian-themes" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Terima kasih sudah baca sampai akhir, semoga bermanfaat.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>indonesia</category>
      <category>nextjs</category>
      <category>tailwindcss</category>
    </item>
  </channel>
</rss>
