← каталог
Toggle Switch
Пружинный переключатель: ручка прыгает spring-ом, трек плавно меняет цвет; доступен как role=switch.
motionclickspringbasic~18 КБreduced-motion ✓
Пропсы
Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.
| Проп | Тип | По умолчанию | Что это |
|---|---|---|---|
| label? | string | "Уведомления" | |
| defaultOn? | boolean | false | |
| onChange? | (on: boolean) => void; | — |
Установка
npx shadcn@latest add https://motiva.pages.dev/r/toggle-switch.jsonзависимости: motion@^12.42.2
Что нужно в проекте до установки
components.jsonв корне — с алиасами в форме"components": "@/components". Нет файла — CLI не поймёт, куда класть.- Алиас
@объявлен дважды:"paths": {"@/*": ["./src/*"]}в корневомtsconfig.json(в Vite он часто содержит толькоreferences— тогда CLI создаёт папку@) иresolve.aliasв конфиге сборщика. - Токены подключены один раз на приложение:
import "@/styles/motiva-tokens.css"— они приезжают вместе с записью. Без них секция потеряет цвета, ритм и тему. - Своя типографика не должна спорить: если у вас на обёртке
text-align: center, он протечёт внутрь блоков. - Проект без TypeScript? Всё равно нужен минимальный
tsconfig.jsonсpathsи"tsx": trueвcomponents.json: CLI грузит tsconfig безусловно, а с"tsx": falseидёт заjsconfig.jsonи падает так же. Приехавший рядом.tsxможно не трогать — вам нужен.vanilla.js.
Экспорт
Код
"use client";
import { useState } from "react";
import { motion } from "motion/react";
export interface ToggleSwitchProps {
label?: string;
defaultOn?: boolean;
onChange?: (on: boolean) => void;
}
/** Пружинный toggle: ручка прыгает spring-ом, трек меняет цвет. Уважает reduced-motion / data-shot. */
export function ToggleSwitch({ label = "Уведомления", defaultOn = false, onChange }: ToggleSwitchProps) {
const [on, setOn] = useState(defaultOn);
const frozen =
typeof document !== "undefined" &&
(document.documentElement.hasAttribute("data-shot") ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches);
const toggle = () => {
setOn((v) => {
onChange?.(!v);
return !v;
});
};
return (
<label
style={{
display: "flex",
alignItems: "center",
gap: 12,
cursor: "pointer",
color: "oklch(0.92 0.01 265)",
fontSize: "0.95rem",
userSelect: "none",
}}
>
<button
role="switch"
aria-checked={on}
onClick={toggle}
style={{
position: "relative",
width: 52,
height: 30,
borderRadius: 999,
border: "1px solid oklch(0.32 0.02 270)",
background: on ? "oklch(0.65 0.17 300)" : "oklch(0.25 0.02 270)",
transition: frozen ? "none" : "background 0.25s",
padding: 0,
cursor: "pointer",
}}
>
<motion.span
animate={{ x: on ? 22 : 0 }}
transition={frozen ? { duration: 0 } : { type: "spring", stiffness: 500, damping: 28 }}
style={{
position: "absolute",
top: 2,
left: 3,
width: 24,
height: 24,
borderRadius: "50%",
background: "oklch(0.97 0 0)",
boxShadow: "0 2px 6px rgb(0 0 0 / 0.35)",
display: "block",
}}
/>
</button>
{label}
</label>
);
}
import { animate } from "motion";
/**
* Toggle Switch — vanilla JS/Motion (без React). Оживляет кнопку-переключатель:
* ручка прыгает пружиной, aria-checked ведёт состояние (стили трека — на aria-checked).
*
* <button id="sw" role="switch" aria-checked="false" style="position:relative;width:52px;height:30px">
* <span data-knob style="position:absolute;top:2px;left:3px;width:24px;height:24px;border-radius:50%"></span>
* </button>
* <script type="module">
* import { initToggleSwitch } from './toggle-switch.vanilla.js';
* initToggleSwitch(document.querySelector('#sw'), { onChange: (on) => console.log(on) });
* </script>
*
* @param {HTMLElement} btn — кнопка с [data-knob] внутри
* @param {{ distance?: number, onChange?: (on: boolean) => void }} [options]
* @returns {() => void} destroy
*/
export function initToggleSwitch(btn, { distance, onChange } = {}) {
const knob = btn.querySelector("[data-knob]");
if (!knob) return () => {};
const frozen = () =>
document.documentElement.hasAttribute("data-shot") ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const travel = () =>
distance ?? btn.clientWidth - knob.offsetWidth - (knob.offsetLeft ?? 3) * 2;
const apply = (on, animated) => {
const x = on ? travel() : 0;
if (animated && !frozen()) {
animate(knob, { transform: `translateX(${x}px)` }, { type: "spring", stiffness: 500, damping: 28 });
} else {
knob.style.transform = `translateX(${x}px)`;
}
};
const toggle = () => {
const next = btn.getAttribute("aria-checked") !== "true";
btn.setAttribute("aria-checked", String(next));
apply(next, true);
onChange?.(next);
};
apply(btn.getAttribute("aria-checked") === "true", false);
btn.addEventListener("click", toggle);
return () => btn.removeEventListener("click", toggle);
}
Похожие эффекты
лицензия: MITавтор: Motivaисточник: оригинал (Motiva)v1.0.0