diff --git a/periscope/src/frontend/src/components/theme/theme-provider.tsx b/periscope/src/frontend/src/components/theme/theme-provider.tsx
new file mode 100644
index 0000000..9ec648b
--- /dev/null
+++ b/periscope/src/frontend/src/components/theme/theme-provider.tsx
@@ -0,0 +1,18 @@
+"use client";
+
+import type { ReactNode } from "react";
+import { ThemeProvider as NextThemes } from "next-themes";
+
+/** Class-based color scheme. Default is dark; OS preference is ignored. */
+export function ThemeProvider({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/periscope/src/frontend/src/components/theme/theme-toggle.tsx b/periscope/src/frontend/src/components/theme/theme-toggle.tsx
new file mode 100644
index 0000000..8982266
--- /dev/null
+++ b/periscope/src/frontend/src/components/theme/theme-toggle.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import { Moon, Sun } from "lucide-react";
+import { useTheme } from "next-themes";
+import { cn } from "@/lib/utils";
+
+export function ThemeToggle({ className }: { className?: string }) {
+ const { resolvedTheme, setTheme } = useTheme();
+
+ const flip = () => {
+ setTheme(resolvedTheme === "dark" ? "light" : "dark");
+ };
+
+ return (
+
+ );
+}
diff --git a/tests/test_periscope_frontend_theme_rewrite.py b/tests/test_periscope_frontend_theme_rewrite.py
new file mode 100644
index 0000000..224ecb7
--- /dev/null
+++ b/tests/test_periscope_frontend_theme_rewrite.py
@@ -0,0 +1,37 @@
+"""next-themes wrapper and toggle live under periscope/src."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+SRC = ROOT / "periscope" / "src" / "frontend" / "src"
+
+
+def test_theme_modules_are_src():
+ for rel in (
+ "components/theme/theme-provider.tsx",
+ "components/theme/theme-toggle.tsx",
+ ):
+ text = (SRC / rel).read_text(encoding="utf-8")
+ assert "Native Periscope overlay" not in text[:400]
+
+
+def test_theme_provider_contract():
+ text = (SRC / "components/theme/theme-provider.tsx").read_text(encoding="utf-8")
+ assert "export function ThemeProvider" in text
+ assert 'attribute="class"' in text
+ assert 'defaultTheme="dark"' in text
+ assert "enableSystem={false}" in text
+ assert "disableTransitionOnChange" in text
+ clerk = SRC / "components/theme/clerk-theme-provider.tsx"
+ assert not clerk.exists()
+
+
+def test_theme_toggle_contract():
+ text = (SRC / "components/theme/theme-toggle.tsx").read_text(encoding="utf-8")
+ assert "export function ThemeToggle" in text
+ assert 'aria-label="Toggle theme"' in text
+ assert "resolvedTheme" in text
+ assert 'className="hidden h-4 w-4 dark:block"' in text
+ assert 'className="h-4 w-4 dark:hidden"' in text