Shadcn/ui codebase analysis: Perfect Next.js dark mode in 2 lines of code with next-themes.

Shadcn/ui codebase analysis: Perfect Next.js dark mode in 2 lines of code with next-themes.

So I wanted to figure out how ui.shadcn.com implemented dark mode on their website. I looked at its source code. These things usually lie in providers.

That is when I came across next-themes. You can easily add dark mode to your next.js app using next-themes. In this article, you will learn the below concepts:

  1. How to configure next-themes in Next.js?

  2. How is next-themes configured in shadcn-ui/ui?

  3. useTheme to toggle dark/light modes.

  4. How is useTheme written in shadcn-ui/ui?

Want to learn how to build shadcn-ui/ui from scratch? Check out build-from-scratch and give it a star if you like it. Sovle challenges to build shadcn-ui/ui from scratch. If you are stuck or need help? there is a solution.

How to configure next-themes in Next.js?

With app/

You’ll need to update your app/layout.jsx to use next-themes. The simplest layout looks like this:

// app/layout.jsx
export default function Layout({ children }) {
  return (
    <html>
      <head />
      <body>{children}</body>
    </html>
  )
}

Adding dark mode support takes 2 lines of code:

// app/layout.jsx
import { ThemeProvider } from 'next-themes'
export default function Layout({ children }) {
  return (
    <html suppressHydrationWarning>
      <head />
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

Note that ThemeProvider is a client component, not a server component.

Note! If you do not add suppressHydrationWarning to your you will get warnings because next-themes updates that element. This property only applies one level deep, so it won't block hydration warnings on other elements.

How is next-themes configured in shadcn-ui/ui?

The answer is in Layout.tsx, it has a ThemeProvider imported from @/components/providers.tsx. Shadcn-ui/ui follows the same configuration provided in the official documentation, except for few properties. They are shown in the image below:

  • attribute = 'data-theme': HTML attribute modified based on the active theme, accepts class and data-* (meaning any data attribute, data-mode, data-color, etc.) (example)

  • defaultTheme = 'system': Default theme name (for v0.0.12 and lower the default was light). If enableSystem is false, the default theme is light

  • enableSystem = true: Whether to switch between dark and light based on prefers-color-scheme

  • disableTransitionOnChange = false: Optionally disable all CSS transitions when switching themes (example)

useTheme to toggle dark/light modes.

Your UI will need to know the current theme and be able to change it. The useTheme hook provides theme information:

import { useTheme } from 'next-themes'

const ThemeChanger = () => {
  const { theme, setTheme } = useTheme()

  return (
    <div>
      The current theme is: {theme}
      <button onClick={() => setTheme('light')}>Light Mode</button>
      <button onClick={() => setTheme('dark')}>Dark Mode</button>
    </div>
  )
}

Warning! The above code is hydration unsafe and will throw a hydration mismatch warning when rendering with SSG or SSR. This is because we cannot know the theme on the server, so it will always be undefined until mounted on the client.

How is useTheme written in shadcn-ui/ui?

mode-toggle.tsx exports a component named ModeToggle which is used in site-header.tsx

ModeToggle in the header on ui.shadcn.com is shown below:

The following code is from mode-toggle.tsx

"use client"

import \* as React from "react"
import { MoonIcon, SunIcon } from "@radix-ui/react-icons"
import { useTheme } from "next-themes"

import { Button } from "@/registry/new-york/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/registry/new-york/ui/dropdown-menu"

export function ModeToggle() {
  const { setTheme } = useTheme()

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="ghost" className="w-9 px-0">
          <SunIcon className="h-\[1.2rem\] w-\[1.2rem\] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
          <MoonIcon className="absolute h-\[1.2rem\] w-\[1.2rem\] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
          <span className="sr-only">Toggle theme</span>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onClick={() => setTheme("light")}>
          Light
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("dark")}>
          Dark
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("system")}>
          System
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

In order to switch modes, setTheme function is exposed by the useTheme hook.

Conclusion:

This approach seems quite straight forward, except for few additional properties passed into ThemeProvider in shadcn/ui. You need to first configure the ThemeProvider and then use the useTheme hook to switch dark/light modes.

About me:

Website: https://ramunarasinga.com/

Linkedin: https://www.linkedin.com/in/ramu-narasinga-189361128/

Github: https://github.com/Ramu-Narasinga

Email: ramu.narasinga@gmail.com

References:

  1. https://github.com/shadcn-ui/ui/blob/main/apps/www/app/layout.tsx

  2. https://github.com/shadcn-ui/ui/blob/main/apps/www/components/providers.tsx#L10

  3. https://github.com/pacocoursey/next-themes/tree/main

  4. https://github.com/shadcn-ui/ui/blob/13d9693808badd4b92811abac5e18dc1cddf2384/apps/www/components/mode-toggle.tsx#L6

  5. https://github.com/shadcn-ui/ui/blob/13d9693808badd4b92811abac5e18dc1cddf2384/apps/www/components/site-header.tsx#L9

  6. https://github.com/shadcn-ui/ui/blob/13d9693808badd4b92811abac5e18dc1cddf2384/apps/www/app/(app)/layout.tsx