Theme Toggle

A minimal light/dark mode toggle with animated sun and moon icons. Client component powered by next-themes.

API Reference

PropTypeDefault
variant"solid" | "outlined" | "ghost" | "minimal""minimal"
size"sm" | "md" | "lg""sm"
color"root" | "primary" | "secondary" | "accent""root"

Preview

Usage

import { ThemeToggle } from "@/bonk/components";

// Default (minimal, sm, root)
<ThemeToggle />

// Customize styling
<ThemeToggle variant="ghost" size="md" color="primary" />

Usage Notes

  • This is a client component. It can be dropped into server components like Footer without making the parent a client component.
  • Wraps IconButton — variant, size, and color are forwarded directly.
  • The sun/moon icons animate on toggle via a spin-in keyframe animation.
  • Requires a ThemeProvider (from next-themes) to be present in the component tree.

Source

src/components/ThemeToggle.tsx
"use client";

import { useState, type ComponentProps } from "react";
import { IconButton } from "../ui";
import { DarkModeIcon, LightModeIcon } from "../ui/icons";
import { useTheme } from "next-themes";

interface ThemeToggleProps {
  variant?: ComponentProps<typeof IconButton>["variant"];
  size?: ComponentProps<typeof IconButton>["size"];
  color?: ComponentProps<typeof IconButton>["color"];
}

export function ThemeToggle({
  variant = "minimal",
  size = "sm",
  color = "root",
}: ThemeToggleProps) {
  const [animKey, setAnimKey] = useState(0);
  const { resolvedTheme, setTheme } = useTheme();

  return (
    <IconButton
      variant={variant}
      size={size}
      color={color}
      onClick={() => {
        setTheme(resolvedTheme === "dark" ? "light" : "dark");
        setAnimKey((k) => k + 1);
      }}
      aria-label="Toggle theme"
    >
      <span
        key={animKey}
        className={`inline-flex${animKey > 0 ? " animate-spin-in" : ""}`}
      >
        <span className="block dark:hidden">
          <LightModeIcon />
        </span>
        <span className="hidden dark:block">
          <DarkModeIcon />
        </span>
      </span>
    </IconButton>
  );
}