Lightbulb Theme Toggle
Animated dark mode toggle component for React, Svelte, and Vue.
1Run the shadcn CLI
npx shadcn@latest add https://toggles.dev/r/lightbulb2Import and use
"use client";
import { useState } from "react";
import { Lightbulb } from "@/components/ui/lightbulb";
export default function App() {
const [toggled, setToggled] = useState(false);
return (
<>
{/* Alternatively, omit `toggled` when your app applies `dark` or `light`
to a root element—the toggle follows that class automatically. */}
<Lightbulb
toggled={toggled}
onClick={() => setToggled((value) => !value)}
/>
</>
);
}1Install the package
▾
npm install @theme-toggles/reactpnpm add @theme-toggles/reactyarn add @theme-toggles/reactbun add @theme-toggles/react2Import and use
"use client";
import { useState } from "react";
import { Lightbulb } from "@theme-toggles/react";
import "@theme-toggles/react/styles/lightbulb.css";
export default function App() {
const [toggled, setToggled] = useState(false);
return (
<>
{/* Alternatively, omit `toggled` when your app applies `dark` or `light`
to a root element—the toggle follows that class automatically. */}
<Lightbulb
toggled={toggled}
onClick={() => setToggled((value) => !value)}
/>
</>
);
}1Copy the component
import { type ButtonHTMLAttributes, type CSSProperties } from "react";
export interface LightbulbProps extends Omit<
ButtonHTMLAttributes<HTMLButtonElement>,
"children"
> {
duration?: number;
/** Whether the toggle should render in its dark-theme state. */
toggled?: boolean;
[key: `data-${string}`]: string | number | boolean | null | undefined;
}
export function Lightbulb({
duration = 500,
toggled,
className,
type = "button",
title = "Toggle theme",
"aria-label": ariaLabel = "Toggle theme",
"aria-pressed": ariaPressed,
...props
}: LightbulbProps) {
return (
<button
{...props}
type={type}
title={title}
aria-label={ariaLabel}
aria-pressed={toggled ?? ariaPressed}
className={[
className,
toggled === true ? "dark" : toggled === false ? "light" : undefined,
]
.filter(Boolean)
.join(" ")}
>
<svg
width="1em"
height="1em"
viewBox="0 0 32 32"
aria-hidden="true"
strokeWidth={"2"}
stroke={"currentColor"}
fill={"currentColor"}
strokeLinecap={"round"}
style={
{ "--toggles-lightbulb--duration": `${duration}ms` } as CSSProperties
}
>
<path
strokeWidth={1}
d={
"M9.4 9.9c1.8-1.8 4.1-2.7 6.6-2.7 5.1 0 9.3 4.2 9.3 9.3 0 2.3-.8 4.4-2.3 6.1-.7.8-2 2.8-2.5 4.4 0 .2-.2.4-.5.4-.2 0-.4-.2-.4-.5v-.1c.5-1.8 2-3.9 2.7-4.8 1.4-1.5 2.1-3.5 2.1-5.6 0-4.7-3.7-8.5-8.4-8.5-2.3 0-4.4.9-5.9 2.5-1.6 1.6-2.5 3.7-2.5 6 0 2.1.7 4 2.1 5.6.8.9 2.2 2.9 2.7 4.9 0 .2-.1.5-.4.5h-.1c-.2 0-.4-.1-.4-.4-.5-1.7-1.8-3.7-2.5-4.5-1.5-1.7-2.3-3.9-2.3-6.1 0-2.3 1-4.7 2.7-6.5z"
}
/>
<path d={"M19.8 27.5h-7.6"} />
<path d={"M19.8 29.2h-7.6"} />
<path d={"M19.8 30.9h-7.6"} />
<path
pathLength={1}
fill={"none"}
d={
"M14.6 27.1c0-3.4 0-6.8-.1-10.2-.2-1-1.1-1.7-2-1.7-1.2-.1-2.3 1-2.2 2.3.1 1 .9 1.9 2.1 2h7.2c1.1-.1 2-1 2.1-2 .1-1.2-1-2.3-2.2-2.3-.9 0-1.7.7-2 1.7 0 3.4 0 6.8-.1 10.2"
}
className="[stroke-dasharray:1.1] motion-safe:transition-[stroke-dashoffset,opacity] motion-safe:duration-(--toggles-lightbulb--duration) [stroke-dashoffset:0] opacity-100 dark:[stroke-dashoffset:1] dark:opacity-0"
/>
<path
d={"M16 5V1.3"}
pathLength={1}
strokeWidth={1.5}
className="[stroke-dasharray:1.1] motion-safe:transition-[stroke-dashoffset,opacity] motion-safe:duration-(--toggles-lightbulb--duration) [stroke-dashoffset:0] opacity-100 dark:[stroke-dashoffset:1] dark:opacity-0"
/>
<path
d={"M27.5 15.8h3.9"}
pathLength={1}
strokeWidth={1.5}
className="[stroke-dasharray:1.1] motion-safe:transition-[stroke-dashoffset,opacity] motion-safe:duration-(--toggles-lightbulb--duration) [stroke-dashoffset:0] opacity-100 dark:[stroke-dashoffset:1] dark:opacity-0"
/>
<path
d={"M23.6 7.9 26.3 5.4"}
pathLength={1}
strokeWidth={1.5}
className="[stroke-dasharray:1.1] motion-safe:transition-[stroke-dashoffset,opacity] motion-safe:duration-(--toggles-lightbulb--duration) [stroke-dashoffset:0] opacity-100 dark:[stroke-dashoffset:1] dark:opacity-0"
/>
<path
d={"M8.4 7.9 5.7 5.4"}
pathLength={1}
strokeWidth={1.5}
className="[stroke-dasharray:1.1] motion-safe:transition-[stroke-dashoffset,opacity] motion-safe:duration-(--toggles-lightbulb--duration) [stroke-dashoffset:0] opacity-100 dark:[stroke-dashoffset:1] dark:opacity-0"
/>
<path
d={"M4.5 15.8H.6"}
pathLength={1}
strokeWidth={1.5}
className="[stroke-dasharray:1.1] motion-safe:transition-[stroke-dashoffset,opacity] motion-safe:duration-(--toggles-lightbulb--duration) [stroke-dashoffset:0] opacity-100 dark:[stroke-dashoffset:1] dark:opacity-0"
/>
</svg>
</button>
);
}2Import and use
"use client";
import { useState } from "react";
// Update this path to where you copied the component.
import { Lightbulb } from "./Lightbulb";
export default function App() {
const [toggled, setToggled] = useState(false);
return (
<>
{/* Alternatively, omit `toggled` when your app applies `dark` or `light`
to a root element—the toggle follows that class automatically. */}
<Lightbulb
toggled={toggled}
onClick={() => setToggled((value) => !value)}
/>
</>
);
}1Install the package
▾
npm install @theme-toggles/sveltepnpm add @theme-toggles/svelteyarn add @theme-toggles/sveltebun add @theme-toggles/svelte2Import and use
<script>
import { Lightbulb } from "@theme-toggles/svelte";
import "@theme-toggles/svelte/styles/lightbulb.css";
let toggled = false;
</script>
<!-- Alternatively, omit `toggled` when your app applies `dark` or `light`
to a root element—the toggle follows that class automatically. -->
<Lightbulb {toggled} on:click={() => (toggled = !toggled)} />1Copy the component
<script lang="ts">
import type { HTMLButtonAttributes } from "svelte/elements";
interface $$Props extends Omit<HTMLButtonAttributes, "children"> {
duration?: number;
toggled?: boolean;
ariaLabel?: string;
class?: string;
}
export let duration = 500;
export let toggled: boolean | undefined = undefined;
export let type: HTMLButtonAttributes["type"] = "button";
export let title = "Toggle theme";
export let ariaLabel = "Toggle theme";
let className = "";
export { className as class };
</script>
<button
{...$$restProps}
{type}
{title}
aria-label={ariaLabel}
aria-pressed={toggled ?? $$restProps["aria-pressed"]}
class={[
className,
toggled === true ? "dark" : toggled === false ? "light" : undefined,
]
.filter(Boolean)
.join(" ")}
on:click
>
<svg
width="1em"
height="1em"
viewBox="0 0 32 32"
aria-hidden="true"
stroke-width={"2"}
stroke={"currentColor"}
fill={"currentColor"}
stroke-linecap={"round"}
style={`--toggles-lightbulb--duration: ${duration}ms`}
>
<path
stroke-width={1}
d={"M9.4 9.9c1.8-1.8 4.1-2.7 6.6-2.7 5.1 0 9.3 4.2 9.3 9.3 0 2.3-.8 4.4-2.3 6.1-.7.8-2 2.8-2.5 4.4 0 .2-.2.4-.5.4-.2 0-.4-.2-.4-.5v-.1c.5-1.8 2-3.9 2.7-4.8 1.4-1.5 2.1-3.5 2.1-5.6 0-4.7-3.7-8.5-8.4-8.5-2.3 0-4.4.9-5.9 2.5-1.6 1.6-2.5 3.7-2.5 6 0 2.1.7 4 2.1 5.6.8.9 2.2 2.9 2.7 4.9 0 .2-.1.5-.4.5h-.1c-.2 0-.4-.1-.4-.4-.5-1.7-1.8-3.7-2.5-4.5-1.5-1.7-2.3-3.9-2.3-6.1 0-2.3 1-4.7 2.7-6.5z"}
/>
<path d={"M19.8 27.5h-7.6"} />
<path d={"M19.8 29.2h-7.6"} />
<path d={"M19.8 30.9h-7.6"} />
<path
pathLength={1}
fill={"none"}
d={"M14.6 27.1c0-3.4 0-6.8-.1-10.2-.2-1-1.1-1.7-2-1.7-1.2-.1-2.3 1-2.2 2.3.1 1 .9 1.9 2.1 2h7.2c1.1-.1 2-1 2.1-2 .1-1.2-1-2.3-2.2-2.3-.9 0-1.7.7-2 1.7 0 3.4 0 6.8-.1 10.2"}
class={"[stroke-dasharray:1.1] motion-safe:transition-[stroke-dashoffset,opacity] motion-safe:duration-(--toggles-lightbulb--duration) [stroke-dashoffset:0] opacity-100 dark:[stroke-dashoffset:1] dark:opacity-0"}
/>
<path
d={"M16 5V1.3"}
pathLength={1}
stroke-width={1.5}
class={"[stroke-dasharray:1.1] motion-safe:transition-[stroke-dashoffset,opacity] motion-safe:duration-(--toggles-lightbulb--duration) [stroke-dashoffset:0] opacity-100 dark:[stroke-dashoffset:1] dark:opacity-0"}
/>
<path
d={"M27.5 15.8h3.9"}
pathLength={1}
stroke-width={1.5}
class={"[stroke-dasharray:1.1] motion-safe:transition-[stroke-dashoffset,opacity] motion-safe:duration-(--toggles-lightbulb--duration) [stroke-dashoffset:0] opacity-100 dark:[stroke-dashoffset:1] dark:opacity-0"}
/>
<path
d={"M23.6 7.9 26.3 5.4"}
pathLength={1}
stroke-width={1.5}
class={"[stroke-dasharray:1.1] motion-safe:transition-[stroke-dashoffset,opacity] motion-safe:duration-(--toggles-lightbulb--duration) [stroke-dashoffset:0] opacity-100 dark:[stroke-dashoffset:1] dark:opacity-0"}
/>
<path
d={"M8.4 7.9 5.7 5.4"}
pathLength={1}
stroke-width={1.5}
class={"[stroke-dasharray:1.1] motion-safe:transition-[stroke-dashoffset,opacity] motion-safe:duration-(--toggles-lightbulb--duration) [stroke-dashoffset:0] opacity-100 dark:[stroke-dashoffset:1] dark:opacity-0"}
/>
<path
d={"M4.5 15.8H.6"}
pathLength={1}
stroke-width={1.5}
class={"[stroke-dasharray:1.1] motion-safe:transition-[stroke-dashoffset,opacity] motion-safe:duration-(--toggles-lightbulb--duration) [stroke-dashoffset:0] opacity-100 dark:[stroke-dashoffset:1] dark:opacity-0"}
/>
</svg>
</button>2Import and use
<script>
// Update this path to where you copied the component.
import Lightbulb from "./Lightbulb.svelte";
let toggled = false;
</script>
<!-- Alternatively, omit `toggled` when your app applies `dark` or `light`
to a root element—the toggle follows that class automatically. -->
<Lightbulb {toggled} on:click={() => (toggled = !toggled)} />1Install the package
▾
npm install @theme-toggles/vuepnpm add @theme-toggles/vueyarn add @theme-toggles/vuebun add @theme-toggles/vue2Import and use
<script setup lang="ts">
import { ref } from "vue";
import { Lightbulb } from "@theme-toggles/vue";
import "@theme-toggles/vue/styles/lightbulb.css";
const toggled = ref(false);
</script>
<template>
<!-- Alternatively, omit `toggled` when your app applies `dark` or `light`
to a root element—the toggle follows that class automatically. -->
<Lightbulb :toggled="toggled" @click="toggled = !toggled" />
</template>1Copy the component
<script setup lang="ts">
import { useAttrs } from "vue";
export interface Props {
duration?: number;
toggled?: boolean;
type?: "button" | "submit" | "reset";
title?: string;
ariaLabel?: string;
}
const props = withDefaults(defineProps<Props>(), {
duration: 500,
type: "button",
title: "Toggle theme",
ariaLabel: "Toggle theme",
});
const attrs = useAttrs();
</script>
<template>
<button
v-bind="attrs"
:type="props.type"
:title="props.title"
:aria-label="props.ariaLabel"
:aria-pressed="props.toggled ?? attrs['aria-pressed']"
:class="
props.toggled === true
? 'dark'
: props.toggled === false
? 'light'
: undefined
"
>
<svg
width="1em"
height="1em"
viewBox="0 0 32 32"
aria-hidden="true"
:stroke-width="'2'"
:stroke="'currentColor'"
:fill="'currentColor'"
:stroke-linecap="'round'"
:style="{ '--toggles-lightbulb--duration': `${props.duration}ms` }"
>
<path
:stroke-width="1"
:d="'M9.4 9.9c1.8-1.8 4.1-2.7 6.6-2.7 5.1 0 9.3 4.2 9.3 9.3 0 2.3-.8 4.4-2.3 6.1-.7.8-2 2.8-2.5 4.4 0 .2-.2.4-.5.4-.2 0-.4-.2-.4-.5v-.1c.5-1.8 2-3.9 2.7-4.8 1.4-1.5 2.1-3.5 2.1-5.6 0-4.7-3.7-8.5-8.4-8.5-2.3 0-4.4.9-5.9 2.5-1.6 1.6-2.5 3.7-2.5 6 0 2.1.7 4 2.1 5.6.8.9 2.2 2.9 2.7 4.9 0 .2-.1.5-.4.5h-.1c-.2 0-.4-.1-.4-.4-.5-1.7-1.8-3.7-2.5-4.5-1.5-1.7-2.3-3.9-2.3-6.1 0-2.3 1-4.7 2.7-6.5z'"
/>
<path :d="'M19.8 27.5h-7.6'" />
<path :d="'M19.8 29.2h-7.6'" />
<path :d="'M19.8 30.9h-7.6'" />
<path
:pathLength="1"
:fill="'none'"
:d="'M14.6 27.1c0-3.4 0-6.8-.1-10.2-.2-1-1.1-1.7-2-1.7-1.2-.1-2.3 1-2.2 2.3.1 1 .9 1.9 2.1 2h7.2c1.1-.1 2-1 2.1-2 .1-1.2-1-2.3-2.2-2.3-.9 0-1.7.7-2 1.7 0 3.4 0 6.8-.1 10.2'"
:class="'[stroke-dasharray:1.1] motion-safe:transition-[stroke-dashoffset,opacity] motion-safe:duration-(--toggles-lightbulb--duration) [stroke-dashoffset:0] opacity-100 dark:[stroke-dashoffset:1] dark:opacity-0'"
/>
<path
:d="'M16 5V1.3'"
:pathLength="1"
:stroke-width="1.5"
:class="'[stroke-dasharray:1.1] motion-safe:transition-[stroke-dashoffset,opacity] motion-safe:duration-(--toggles-lightbulb--duration) [stroke-dashoffset:0] opacity-100 dark:[stroke-dashoffset:1] dark:opacity-0'"
/>
<path
:d="'M27.5 15.8h3.9'"
:pathLength="1"
:stroke-width="1.5"
:class="'[stroke-dasharray:1.1] motion-safe:transition-[stroke-dashoffset,opacity] motion-safe:duration-(--toggles-lightbulb--duration) [stroke-dashoffset:0] opacity-100 dark:[stroke-dashoffset:1] dark:opacity-0'"
/>
<path
:d="'M23.6 7.9 26.3 5.4'"
:pathLength="1"
:stroke-width="1.5"
:class="'[stroke-dasharray:1.1] motion-safe:transition-[stroke-dashoffset,opacity] motion-safe:duration-(--toggles-lightbulb--duration) [stroke-dashoffset:0] opacity-100 dark:[stroke-dashoffset:1] dark:opacity-0'"
/>
<path
:d="'M8.4 7.9 5.7 5.4'"
:pathLength="1"
:stroke-width="1.5"
:class="'[stroke-dasharray:1.1] motion-safe:transition-[stroke-dashoffset,opacity] motion-safe:duration-(--toggles-lightbulb--duration) [stroke-dashoffset:0] opacity-100 dark:[stroke-dashoffset:1] dark:opacity-0'"
/>
<path
:d="'M4.5 15.8H.6'"
:pathLength="1"
:stroke-width="1.5"
:class="'[stroke-dasharray:1.1] motion-safe:transition-[stroke-dashoffset,opacity] motion-safe:duration-(--toggles-lightbulb--duration) [stroke-dashoffset:0] opacity-100 dark:[stroke-dashoffset:1] dark:opacity-0'"
/>
</svg>
</button>
</template>2Import and use
<script setup lang="ts">
import { ref } from "vue";
// Update this path to where you copied the component.
import Lightbulb from "./Lightbulb.vue";
const toggled = ref(false);
</script>
<template>
<!-- Alternatively, omit `toggled` when your app applies `dark` or `light`
to a root element—the toggle follows that class automatically. -->
<Lightbulb :toggled="toggled" @click="toggled = !toggled" />
</template>