# Theme Toggle
*URL: https://www.grettech.com/components/theme-toggle*

An animated light/dark toggle for next-themes, with a D keyboard shortcut.

- Press D to toggle (ignored while typing or with Cmd/Ctrl)
- Animated sun and moon icons
- Keyboard accessible, with a tooltip
- Follows the resolved system theme

## Install with the shadcn CLI
```bash
npx shadcn@latest add https://www.grettech.com/api/registry/theme-toggle
```
With its demo: `npx shadcn@latest add https://www.grettech.com/api/registry/theme-toggle-demo`

**Dependencies:** npm: lucide-react, motion, next-themes; shadcn/ui: button, tooltip

## Manual installation
**Step 1: Install dependencies**
Install the packages this component uses and the shadcn/ui components it builds on. It also needs the `cn` helper in `lib/utils.ts`, which `npx shadcn@latest init` adds.

```bash
npm install lucide-react motion next-themes
npx shadcn@latest add button tooltip
```

**Step 2: Copy the source code**
Add these files to your project components directory.

File: `components/ui/theme-toggle.tsx`
```tsx
"use client"

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

import { Button } from "@/components/ui/button"
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip"
import { domAnimation, LazyMotion } from "motion/react"
import * as m from "motion/react-m"

const noopSubscribe = () => () => {}

/** false during SSR and hydration, true once running in the browser */
function useMounted() {
  return React.useSyncExternalStore(
    noopSubscribe,
    () => true,
    () => false
  )
}

function isTypingTarget(target: EventTarget | null) {
  return (
    target instanceof HTMLInputElement ||
    target instanceof HTMLTextAreaElement ||
    target instanceof HTMLSelectElement ||
    (target instanceof HTMLElement && target.isContentEditable)
  )
}

export interface ThemeToggleProps {
  /** Show the current theme's name ("Light" or "Dark") beside the icon */
  showThemeName?: boolean
}

export function ThemeToggle({ showThemeName = false }: ThemeToggleProps) {
  const { resolvedTheme, setTheme } = useTheme()
  const mounted = useMounted()
  const isDark = mounted && resolvedTheme === "dark"

  const toggleTheme = React.useCallback(() => {
    setTheme(resolvedTheme === "dark" ? "light" : "dark")
  }, [resolvedTheme, setTheme])

  React.useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key !== "d" && e.key !== "D") return
      // Leave Cmd/Ctrl+D (bookmark) etc. and held keys alone
      if (e.metaKey || e.ctrlKey || e.altKey || e.repeat) return
      if (isTypingTarget(e.target)) return
      // Several toggles can be on one page; only the first one handles it
      if (e.defaultPrevented) return
      e.preventDefault()
      toggleTheme()
    }

    window.addEventListener("keydown", handleKeyDown)
    return () => window.removeEventListener("keydown", handleKeyDown)
  }, [toggleTheme])

  const label = `Switch to ${isDark ? "light" : "dark"} theme`

  return (
    // Hover and tap need only the light domAnimation features
    <LazyMotion features={domAnimation}>
      <Tooltip>
        <TooltipTrigger asChild>
          <m.div
            whileHover={{ scale: 1.05, transition: { duration: 0.2 } }}
            whileTap={{ scale: 0.95, transition: { duration: 0.2 } }}
            transition={{
              type: "spring" as const,
              stiffness: 400,
              damping: 17,
            }}
            className="flex items-center gap-2"
          >
            <Button
              variant="outline"
              size="icon"
              onClick={toggleTheme}
              aria-label={label}
              aria-keyshortcuts="D"
            >
              <Sun className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
              <Moon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
            </Button>
            {showThemeName && (
              <button
                type="button"
                onClick={toggleTheme}
                tabIndex={-1}
                aria-hidden="true"
                className="cursor-pointer text-sm leading-none font-medium select-none"
              >
                {isDark ? "Light" : "Dark"} Mode
              </button>
            )}
          </m.div>
        </TooltipTrigger>
        <TooltipContent side="bottom" className="text-xs">
          <span className="flex items-center gap-2">
            Toggle theme
            <kbd className="pointer-events-none inline-flex h-5 items-center gap-1 rounded border border-muted-foreground/20 bg-muted/80 px-1.5 font-mono text-[10px] font-medium text-foreground opacity-100 select-none">
              <span className="text-xs">D</span>
            </kbd>
          </span>
        </TooltipContent>
      </Tooltip>
    </LazyMotion>
  )
}

```

**Step 3: Framework Adjustments (Optional)**
If you are manually copying this code into a Vite or Remix project (without using the CLI), make sure to remove the `"use client";` directive from the very top of the file.

**Step 4: Framework Adjustments (Optional)**
If you are manually copying this code into a Vite or Remix project (without using the CLI), make sure to remove the `"use client";` directive from the very top of the file.

## Props
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
| `showThemeName` | `boolean` | `false` | No | Shows a label next to the icon naming the theme it switches to ("Dark Mode" or "Light Mode"). |

## Usage
```tsx
"use client"

import { ThemeToggle } from "@/components/ui/theme-toggle"

export default function ThemeToggleDemo() {
  return (
    <div className="flex gap-4 p-4">
      <ThemeToggle />
    </div>
  )
}

```
