Around Theme Toggle
Animated dark mode toggle component for React, Svelte, and Vue.
1Run the shadcn CLI
npx shadcn@latest add https://toggles.dev/r/around2Import and use
"use client";
import { useState } from "react";
import { Around } from "@/components/ui/around";
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. */}
<Around
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 { Around } from "@theme-toggles/react";
import "@theme-toggles/react/styles/around.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. */}
<Around
toggled={toggled}
onClick={() => setToggled((value) => !value)}
/>
</>
);
}1Copy the component
import { type ButtonHTMLAttributes, type CSSProperties, useId } from "react";
export interface AroundProps 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 Around({
duration = 500,
toggled,
className,
type = "button",
title = "Toggle theme",
"aria-label": ariaLabel = "Toggle theme",
"aria-pressed": ariaPressed,
...props
}: AroundProps) {
const toggleId = useId();
const clipMainId = `toggles.dev-around-main-${toggleId}`;
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"
fill={"currentColor"}
style={
{ "--toggles-around--duration": `${duration}ms` } as CSSProperties
}
>
<defs>
<clipPath
id={clipMainId}
className="[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.6)_ease] dark:[transform:rotate(-90deg)] motion-safe:dark:[transition:transform_var(--toggles-around--duration)_ease]"
>
<path
d={"M0 0h42v30a1 1 0 00-16 13H0Z"}
className="motion-safe:transition-[d,translate] motion-safe:[transition-duration:calc(var(--toggles-around--duration)_*_0.6)] motion-safe:[transition-timing-function:ease] dark:[d:path('M-12_-14h42v30a1_1_0_00-16_13H0Z')] dark:not-supports-[d:path('M0_0')]:-translate-x-[12px] dark:not-supports-[d:path('M0_0')]:-translate-y-[14px] motion-safe:dark:[transition-duration:var(--toggles-around--duration)]"
/>
</clipPath>
</defs>
<g clipPath={`url(#${clipMainId})`}>
<circle
cx={16}
cy={16}
r={8.4}
className="[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.6)_ease] dark:[transform:scale(1.4)] motion-safe:dark:[transition:transform_var(--toggles-around--duration)_ease]"
/>
<g>
<circle
cx={16}
cy={3.3}
r={2.3}
className="[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.2)_ease_calc(var(--toggles-around--duration)_*_0.253)] dark:[transform:scale(0)] motion-safe:dark:[transition:transform_calc(var(--toggles-around--duration)_*_0.4)_ease]"
/>
<circle
cx={27}
cy={9.7}
r={2.3}
className="[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.2)_ease_calc(var(--toggles-around--duration)_*_0.348)] dark:[transform:scale(0)] motion-safe:dark:[transition:transform_calc(var(--toggles-around--duration)_*_0.4)_ease]"
/>
<circle
cx={27}
cy={22.3}
r={2.3}
className="[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.2)_ease_calc(var(--toggles-around--duration)_*_0.443)] dark:[transform:scale(0)] motion-safe:dark:[transition:transform_calc(var(--toggles-around--duration)_*_0.4)_ease]"
/>
<circle
cx={16}
cy={28.7}
r={2.3}
className="[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.2)_ease_calc(var(--toggles-around--duration)_*_0.538)] dark:[transform:scale(0)] motion-safe:dark:[transition:transform_calc(var(--toggles-around--duration)_*_0.4)_ease]"
/>
<circle
cx={5}
cy={22.3}
r={2.3}
className="[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.2)_ease_calc(var(--toggles-around--duration)_*_0.633)] dark:[transform:scale(0)] motion-safe:dark:[transition:transform_calc(var(--toggles-around--duration)_*_0.4)_ease]"
/>
<circle
cx={5}
cy={9.7}
r={2.3}
className="[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.2)_ease_calc(var(--toggles-around--duration)_*_0.728)] dark:[transform:scale(0)] motion-safe:dark:[transition:transform_calc(var(--toggles-around--duration)_*_0.4)_ease]"
/>
</g>
</g>
</svg>
</button>
);
}2Import and use
"use client";
import { useState } from "react";
// Update this path to where you copied the component.
import { Around } from "./Around";
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. */}
<Around
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 { Around } from "@theme-toggles/svelte";
import "@theme-toggles/svelte/styles/around.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. -->
<Around {toggled} on:click={() => (toggled = !toggled)} />1Copy the component
<script context="module" lang="ts">
let nextId = 0;
</script>
<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 };
const toggleId = `around-${++nextId}`;
const clipMainId = `toggles.dev-around-main-${toggleId}`;
</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"
fill={"currentColor"}
style={`--toggles-around--duration: ${duration}ms`}
>
<defs>
<clipPath
id={clipMainId}
class={"[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.6)_ease] dark:[transform:rotate(-90deg)] motion-safe:dark:[transition:transform_var(--toggles-around--duration)_ease]"}
>
<path
d={"M0 0h42v30a1 1 0 00-16 13H0Z"}
class={"motion-safe:transition-[d,translate] motion-safe:[transition-duration:calc(var(--toggles-around--duration)_*_0.6)] motion-safe:[transition-timing-function:ease] dark:[d:path('M-12_-14h42v30a1_1_0_00-16_13H0Z')] dark:not-supports-[d:path('M0_0')]:-translate-x-[12px] dark:not-supports-[d:path('M0_0')]:-translate-y-[14px] motion-safe:dark:[transition-duration:var(--toggles-around--duration)]"}
/>
</clipPath>
</defs>
<g clip-path={`url(#${clipMainId})`}>
<circle
cx={16}
cy={16}
r={8.4}
class={"[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.6)_ease] dark:[transform:scale(1.4)] motion-safe:dark:[transition:transform_var(--toggles-around--duration)_ease]"}
/>
<g>
<circle
cx={16}
cy={3.3}
r={2.3}
class={"[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.2)_ease_calc(var(--toggles-around--duration)_*_0.253)] dark:[transform:scale(0)] motion-safe:dark:[transition:transform_calc(var(--toggles-around--duration)_*_0.4)_ease]"}
/>
<circle
cx={27}
cy={9.7}
r={2.3}
class={"[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.2)_ease_calc(var(--toggles-around--duration)_*_0.348)] dark:[transform:scale(0)] motion-safe:dark:[transition:transform_calc(var(--toggles-around--duration)_*_0.4)_ease]"}
/>
<circle
cx={27}
cy={22.3}
r={2.3}
class={"[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.2)_ease_calc(var(--toggles-around--duration)_*_0.443)] dark:[transform:scale(0)] motion-safe:dark:[transition:transform_calc(var(--toggles-around--duration)_*_0.4)_ease]"}
/>
<circle
cx={16}
cy={28.7}
r={2.3}
class={"[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.2)_ease_calc(var(--toggles-around--duration)_*_0.538)] dark:[transform:scale(0)] motion-safe:dark:[transition:transform_calc(var(--toggles-around--duration)_*_0.4)_ease]"}
/>
<circle
cx={5}
cy={22.3}
r={2.3}
class={"[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.2)_ease_calc(var(--toggles-around--duration)_*_0.633)] dark:[transform:scale(0)] motion-safe:dark:[transition:transform_calc(var(--toggles-around--duration)_*_0.4)_ease]"}
/>
<circle
cx={5}
cy={9.7}
r={2.3}
class={"[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.2)_ease_calc(var(--toggles-around--duration)_*_0.728)] dark:[transform:scale(0)] motion-safe:dark:[transition:transform_calc(var(--toggles-around--duration)_*_0.4)_ease]"}
/>
</g>
</g>
</svg>
</button>2Import and use
<script>
// Update this path to where you copied the component.
import Around from "./Around.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. -->
<Around {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 { Around } from "@theme-toggles/vue";
import "@theme-toggles/vue/styles/around.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. -->
<Around :toggled="toggled" @click="toggled = !toggled" />
</template>1Copy the component
<script lang="ts">
let nextId = 0;
</script>
<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();
const toggleId = `around-${++nextId}`;
const clipMainId = `toggles.dev-around-main-${toggleId}`;
</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"
:fill="'currentColor'"
:style="{ '--toggles-around--duration': `${props.duration}ms` }"
>
<defs>
<clipPath
:id="clipMainId"
:class="'[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.6)_ease] dark:[transform:rotate(-90deg)] motion-safe:dark:[transition:transform_var(--toggles-around--duration)_ease]'"
>
<path
:d="'M0 0h42v30a1 1 0 00-16 13H0Z'"
:class="'motion-safe:transition-[d,translate] motion-safe:[transition-duration:calc(var(--toggles-around--duration)_*_0.6)] motion-safe:[transition-timing-function:ease] dark:[d:path(\'M-12_-14h42v30a1_1_0_00-16_13H0Z\')] dark:not-supports-[d:path(\'M0_0\')]:-translate-x-[12px] dark:not-supports-[d:path(\'M0_0\')]:-translate-y-[14px] motion-safe:dark:[transition-duration:var(--toggles-around--duration)]'"
/>
</clipPath>
</defs>
<g :clip-path="`url(#${clipMainId})`">
<circle
:cx="16"
:cy="16"
:r="8.4"
:class="'[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.6)_ease] dark:[transform:scale(1.4)] motion-safe:dark:[transition:transform_var(--toggles-around--duration)_ease]'"
/>
<g>
<circle
:cx="16"
:cy="3.3"
:r="2.3"
:class="'[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.2)_ease_calc(var(--toggles-around--duration)_*_0.253)] dark:[transform:scale(0)] motion-safe:dark:[transition:transform_calc(var(--toggles-around--duration)_*_0.4)_ease]'"
/>
<circle
:cx="27"
:cy="9.7"
:r="2.3"
:class="'[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.2)_ease_calc(var(--toggles-around--duration)_*_0.348)] dark:[transform:scale(0)] motion-safe:dark:[transition:transform_calc(var(--toggles-around--duration)_*_0.4)_ease]'"
/>
<circle
:cx="27"
:cy="22.3"
:r="2.3"
:class="'[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.2)_ease_calc(var(--toggles-around--duration)_*_0.443)] dark:[transform:scale(0)] motion-safe:dark:[transition:transform_calc(var(--toggles-around--duration)_*_0.4)_ease]'"
/>
<circle
:cx="16"
:cy="28.7"
:r="2.3"
:class="'[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.2)_ease_calc(var(--toggles-around--duration)_*_0.538)] dark:[transform:scale(0)] motion-safe:dark:[transition:transform_calc(var(--toggles-around--duration)_*_0.4)_ease]'"
/>
<circle
:cx="5"
:cy="22.3"
:r="2.3"
:class="'[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.2)_ease_calc(var(--toggles-around--duration)_*_0.633)] dark:[transform:scale(0)] motion-safe:dark:[transition:transform_calc(var(--toggles-around--duration)_*_0.4)_ease]'"
/>
<circle
:cx="5"
:cy="9.7"
:r="2.3"
:class="'[transform-origin:center] motion-safe:[transition:transform_calc(var(--toggles-around--duration)_*_0.2)_ease_calc(var(--toggles-around--duration)_*_0.728)] dark:[transform:scale(0)] motion-safe:dark:[transition:transform_calc(var(--toggles-around--duration)_*_0.4)_ease]'"
/>
</g>
</g>
</svg>
</button>
</template>2Import and use
<script setup lang="ts">
import { ref } from "vue";
// Update this path to where you copied the component.
import Around from "./Around.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. -->
<Around :toggled="toggled" @click="toggled = !toggled" />
</template>