ui: Dark mode not working in App directory

Hey @shadcn, really enjoying working with the UI project, thanks.

I’m having trouble getting the dark mode to work in Next 13.4.7 in the App dir.

I’ve followed the guide on this page: https://ui.shadcn.com/docs/dark-mode

// app/layout.tsx `import Header from “@/components/header”; import ‘…/styles/globals.css’; import { ThemeProvider } from ‘@/components/theme-provider’;

export default function RootLayout({ children }) { return ( <html lang="en"> <body> <Header/> <ThemeProvider attribute="class" defaultTheme="system" enableSystem> {children} </ThemeProvider> </body> </html> ) }`

// app/components/theme-provider.tsx `“use client”;

import * as React from “react” import { ThemeProvider as NextThemesProvider } from “next-themes” import { type ThemeProviderProps } from “next-themes/dist/types”

export function ThemeProvider({ children, …props }: ThemeProviderProps) { return <NextThemesProvider {…props}>{children}</NextThemesProvider> }`

// app/theme-switcher `“use client”;

import * as React from “react” import { Moon, Sun } from “lucide-react” import { useTheme } from “next-themes”

import { Button } from “@/components/ui/button” import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from “@/components/ui/dropdown-menu”

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

return (
    <DropdownMenu>
        The current theme is: {theme}
        <DropdownMenuTrigger asChild>
            <Button variant="outline" size="icon">
                <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
                <Moon 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>
)

}`

Uploading image.png…

Most likely something I’m doing wrong, any chance you can spot it? Thanks!

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 1
  • Comments: 19

Most upvoted comments

Getting the following console error:

app-index.js:32 Warning: Extra attributes from the server: class,style
    at html
    at RedirectErrorBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/redirect-boundary.js:73:9)
    at RedirectBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/redirect-boundary.js:81:11)
    at NotFoundErrorBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/not-found-boundary.js:51:9)
    at NotFoundBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/not-found-boundary.js:59:11)
    at ReactDevOverlay (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/react-dev-overlay/internal/ReactDevOverlay.js:66:9)
    at HotReload (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:276:11)
    at Router (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/app-router.js:92:11)
    at ErrorBoundaryHandler (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/error-boundary.js:77:9)
    at ErrorBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/error-boundary.js:101:11)
    at AppRouter (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/app-router.js:392:13)
    at ServerRoot (webpack-internal:///(app-client)/./node_modules/next/dist/client/app-index.js:163:11)
    at RSCComponent
    at Root (webpack-internal:///(app-client)/./node_modules/next/dist/client/app-index.js:180:11)

@LyMc adding this to the layout page also fixes it.

export const viewport: Viewport = {
  themeColor: [
    { media: "(prefers-color-scheme: light)", color: "white" },
    { media: "(prefers-color-scheme: dark)", color: "black" },
  ],
}

@morganfeeney you can avoid adding suppressHydrationWarning by doing this: https://github.com/pacocoursey/next-themes?tab=readme-ov-file#avoid-hydration-mismatch

Be sure to add the mounted logic to both provider and switcher.

for me it was darkMode: "class" that was missing from my tailwind.config.ts

import type { Config } from "tailwindcss";

const config: Config = {
  darkMode: "class", <-- add this
  content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/ui/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
    },
  },
  plugins: [require("tailwindcss-animate")],
};
export default config;

Getting the following console error:

app-index.js:32 Warning: Extra attributes from the server: class,style
    at html
    at RedirectErrorBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/redirect-boundary.js:73:9)
    at RedirectBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/redirect-boundary.js:81:11)
    at NotFoundErrorBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/not-found-boundary.js:51:9)
    at NotFoundBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/not-found-boundary.js:59:11)
    at ReactDevOverlay (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/react-dev-overlay/internal/ReactDevOverlay.js:66:9)
    at HotReload (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:276:11)
    at Router (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/app-router.js:92:11)
    at ErrorBoundaryHandler (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/error-boundary.js:77:9)
    at ErrorBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/error-boundary.js:101:11)
    at AppRouter (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/app-router.js:392:13)
    at ServerRoot (webpack-internal:///(app-client)/./node_modules/next/dist/client/app-index.js:163:11)
    at RSCComponent
    at Root (webpack-internal:///(app-client)/./node_modules/next/dist/client/app-index.js:180:11)

Hey. How did you resolve this issue ?

I’m facing the same one.

I needed to include the Header component which contained the ModeToggle component inside the ThemeProvider component.

//layout.tsx
import { ThemeProvider } from '@/components/theme-provider';
import Header from "@/components/header";
import '../styles/globals.css';
export default function RootLayout({ children }) {
    return (
            <html suppressHydrationWarning lang="en">
            <head />
            <body>
            <ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
                <Header/>
                {children}
            </ThemeProvider>
            </body>
            </html>
    )
}

Instead of:

//layout.tsx
import { ThemeProvider } from '@/components/theme-provider';
import Header from "@/components/header";
import '../styles/globals.css';
export default function RootLayout({ children }) {
    return (
            <html suppressHydrationWarning lang="en">
            <head />
            <body>
            <Header/>
            <ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
                {children}
            </ThemeProvider>
            </body>
            </html>
    )
}

Makes perfect sense now I see what it’s doing!

Just worked out that the <ModeToggle /> component was being called in the <header /> which was called in the layout.tsx.

When I add the <ModeToggle /> into a page component, it seems to work. No idea why, but making progress 😄