diff --git a/__tests__/dark-mode-switcher-empty.test.tsx b/__tests__/dark-mode-switcher-empty.test.tsx
new file mode 100644
index 0000000..05dd5ea
--- /dev/null
+++ b/__tests__/dark-mode-switcher-empty.test.tsx
@@ -0,0 +1,125 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import DarkModeSwitcher, { DarkModeSwitcherEmptyState } from "@/app/components/DarkModeSwitcher";
+
+describe("DarkModeSwitcher - empty list display views #313", () => {
+ describe("empty state when isDarkMode is null/undefined", () => {
+ it("renders empty placeholder when isDarkMode is null", () => {
+ render();
+ expect(screen.getByTestId("dark-mode-switcher-empty-state")).toBeInTheDocument();
+ });
+
+ it("renders empty placeholder when isDarkMode is undefined", () => {
+ render();
+ expect(screen.getByTestId("dark-mode-switcher-empty-state")).toBeInTheDocument();
+ });
+
+ it("does NOT render switch when empty", () => {
+ render();
+ expect(screen.queryByRole("switch")).not.toBeInTheDocument();
+ });
+
+ it("does not render empty state when isDarkMode is false (valid light mode)", () => {
+ render();
+ expect(screen.queryByTestId("dark-mode-switcher-empty-state")).not.toBeInTheDocument();
+ expect(screen.getByRole("switch")).toBeInTheDocument();
+ });
+
+ it("does not render empty state when isDarkMode is true", () => {
+ render();
+ expect(screen.queryByTestId("dark-mode-switcher-empty-state")).not.toBeInTheDocument();
+ });
+
+ it("does not render empty when loading (loading takes precedence)", () => {
+ render();
+ expect(screen.queryByTestId("dark-mode-switcher-empty-state")).not.toBeInTheDocument();
+ expect(screen.getByRole("status")).toBeInTheDocument();
+ });
+ });
+
+ describe("empty state UI elements - descriptive placeholder", () => {
+ it("has region role with aria-label No theme preferences", () => {
+ render();
+ expect(screen.getByRole("region", { name: "No theme preferences" })).toBeInTheDocument();
+ });
+
+ it("shows title No theme preferences available", () => {
+ render();
+ expect(screen.getByText("No theme preferences available")).toBeInTheDocument();
+ });
+
+ it("shows descriptive copy about theme data is empty", () => {
+ render();
+ expect(screen.getByText(/Theme data is empty/)).toBeInTheDocument();
+ });
+
+ it("shows illustrative copy about default light theme", () => {
+ render();
+ expect(screen.getByText(/default light theme/)).toBeInTheDocument();
+ });
+
+ it("shows Waiting for theme data badge", () => {
+ render();
+ expect(screen.getByText("Waiting for theme data")).toBeInTheDocument();
+ });
+
+ it("has decorative icon hidden from AT (aria-hidden)", () => {
+ const { container } = render();
+ const hidden = container.querySelector("[aria-hidden=\"true\"]");
+ expect(hidden).toBeInTheDocument();
+ });
+
+ it("uses design tokens: border-border-strong bg-surface-card rounded-xl", () => {
+ render();
+ const el = screen.getByTestId("dark-mode-switcher-empty-state");
+ expect(el.className).toContain("border-border-strong");
+ expect(el.className).toContain("bg-surface-card");
+ expect(el.className).toContain("rounded-xl");
+ });
+
+ it("uses text-text-primary and text-text-muted for contrast", () => {
+ render();
+ expect(screen.getByText("No theme preferences available").className).toContain("text-text-primary");
+ expect(screen.getByText(/Theme data is empty/).className).toContain("text-text-muted");
+ });
+
+ it("has centered layout (items-center justify-center text-center)", () => {
+ render();
+ const el = screen.getByTestId("dark-mode-switcher-empty-state");
+ expect(el.className).toContain("items-center");
+ expect(el.className).toContain("justify-center");
+ expect(el.className).toContain("text-center");
+ });
+ });
+
+ describe("DarkModeSwitcherEmptyState component directly", () => {
+ it("renders standalone empty state", () => {
+ render();
+ expect(screen.getByTestId("dark-mode-switcher-empty-state")).toBeInTheDocument();
+ });
+
+ it("accepts custom className", () => {
+ render();
+ expect(screen.getByTestId("dark-mode-switcher-empty-state").className).toContain("mt-4");
+ });
+
+ it("has correct test id and roles", () => {
+ render();
+ const el = screen.getByTestId("dark-mode-switcher-empty-state");
+ expect(el).toHaveAttribute("role", "region");
+ expect(el).toHaveAttribute("aria-label", "No theme preferences");
+ });
+ });
+
+ describe("not empty - normal rendering", () => {
+ it("renders switch for light mode with correct aria", () => {
+ render();
+ expect(screen.getByRole("switch")).toHaveAttribute("aria-checked", "false");
+ });
+
+ it("renders switch for dark mode with correct aria", () => {
+ render();
+ expect(screen.getByRole("switch")).toHaveAttribute("aria-checked", "true");
+ });
+ });
+});
diff --git a/app/components/DarkModeSwitcher.tsx b/app/components/DarkModeSwitcher.tsx
new file mode 100644
index 0000000..65dfc26
--- /dev/null
+++ b/app/components/DarkModeSwitcher.tsx
@@ -0,0 +1,169 @@
+"use client";
+
+import ButtonSpinner from "./ButtonSpinner";
+
+export interface DarkModeSwitcherProps {
+ /** Current theme state: true = dark, false = light, null/undefined = empty/no data */
+ isDarkMode?: boolean | null;
+ /** Toggle handler */
+ onToggle?: () => void;
+ /** Whether the switch is disabled */
+ disabled?: boolean;
+ /** Loading state - shows spinner */
+ loading?: boolean;
+ /** Optional id for the control */
+ id?: string;
+ /** Additional className */
+ className?: string;
+ /** Accessible label override */
+ ariaLabel?: string;
+}
+
+/**
+ * Empty state view for DarkModeSwitcher.
+ * Displayed when theme data is unavailable (isDarkMode is null/undefined).
+ * Uses design tokens and is fully accessible.
+ */
+export function DarkModeSwitcherEmptyState({
+ className = "",
+}: {
+ className?: string;
+}) {
+ return (
+
+
+ ☾
+
+
+ No theme preferences available
+
+
+ Theme data is empty. Once theme preferences are configured, the
+ dark/light toggle will appear here. You can still browse in the default
+ light theme.
+