DEV Community

Cover image for Shadcn-ui codebase analysis: Perfect Next.js dark mode in 2 lines of code with next-themes.
Ramu Narasinga
Ramu Narasinga

Posted on • Edited on

3

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?

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>
  )
}


Enter fullscreen mode Exit fullscreen mode

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>
  )
}


Enter fullscreen mode Exit fullscreen mode

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>
  )
}


Enter fullscreen mode Exit fullscreen mode

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>
  )
}


Enter fullscreen mode Exit fullscreen mode

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.

Get free courses inspired by the best practices used in open source.

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

Learn the best practices used in open source.

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

Sentry blog image

How I fixed 20 seconds of lag for every user in just 20 minutes.

Our AI agent was running 10-20 seconds slower than it should, impacting both our own developers and our early adopters. See how I used Sentry Profiling to fix it in record time.

Read more

Top comments (0)

Postgres on Neon - Get the Free Plan

No credit card required. The database you love, on a serverless platform designed to help you build faster.

Get Postgres on Neon

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay