← каталог
Theme Toggle Morph
Переключатель темы: солнце превращается в луну ОДНОЙ фигурой — круг выгрызается вторым кругом через SVG-маску, лучи расходятся и гаснут. Не подмена иконок, а морф.
cssclickmorphintermediate~2 КБreduced-motion ✓
🎛 Настройки — крути и копируй
Пропсы
Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.
| Проп | Тип | По умолчанию | Что это |
|---|---|---|---|
| defaultDark? | boolean | false | Тёмная тема включена изначально. |
| onChange? | (dark: boolean) => void; | — |
Установка
npx shadcn@latest add https://motiva.pages.dev/r/theme-toggle-morph.jsonЧто нужно в проекте до установки
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";
export interface ThemeToggleMorphProps {
/** Тёмная тема включена изначально. */
defaultDark?: boolean;
onChange?: (dark: boolean) => void;
}
/**
* Переключатель темы: солнце превращается в луну одной фигурой — круг «выгрызается»
* вторым кругом через SVG-маску, и полумесяц получается сам. Лучи расходятся врозь
* и гаснут. Никаких двух иконок с кроссфейдом — это ОДИН объект, поэтому морф читается
* как трансформация, а не как подмена.
*/
export function ThemeToggleMorph({ defaultDark = false, onChange }: ThemeToggleMorphProps) {
const [dark, setDark] = useState(defaultDark);
const toggle = () => {
const next = !dark;
setDark(next);
onChange?.(next);
};
return (
<button className={`ttm${dark ? " is-dark" : ""}`} onClick={toggle} aria-pressed={dark} aria-label="Сменить тему">
<style>{`
.ttm { --ttm-fg: oklch(0.75 0.16 75); --ttm-bg: oklch(0.94 0.02 85);
position: relative; width: 62px; height: 34px; padding: 0; cursor: pointer;
border-radius: 999px; border: 1px solid oklch(0.8 0.03 85);
background: var(--ttm-bg); transition: background 0.45s, border-color 0.45s;
display: grid; place-items: center; }
.ttm.is-dark { --ttm-fg: oklch(0.9 0.02 265); --ttm-bg: oklch(0.24 0.03 275);
border-color: oklch(0.36 0.03 275); }
.ttm:focus-visible { outline: 2px solid var(--ds-accent, oklch(0.72 0.15 300)); outline-offset: 3px; }
.ttm-knob { position: absolute; top: 3px; left: 3px; width: 26px; height: 26px;
border-radius: 50%; background: oklch(1 0 0 / 0.06);
transition: translate 0.45s cubic-bezier(0.34, 1.4, 0.64, 1); }
.ttm.is-dark .ttm-knob { translate: 28px 0; }
.ttm-svg { position: absolute; top: 5px; left: 5px; width: 22px; height: 22px;
overflow: visible; color: var(--ttm-fg);
transition: translate 0.45s cubic-bezier(0.34, 1.4, 0.64, 1), rotate 0.6s ease; }
.ttm.is-dark .ttm-svg { translate: 28px 0; rotate: -35deg; }
/* маска: чёрный круг «выедает» из белого диска полумесяц */
.ttm-cut { transition: cx 0.5s var(--ds-ease-in-out, cubic-bezier(0.65,0,0.35,1)), cy 0.5s var(--ds-ease-in-out, cubic-bezier(0.65,0,0.35,1)); }
.ttm-rays { transition: opacity 0.3s, scale 0.45s var(--ds-ease-spring, cubic-bezier(0.34,1.4,0.64,1));
transform-origin: 12px 12px; }
.ttm.is-dark .ttm-rays { opacity: 0; scale: 0.4; }
.ttm-disc { transition: r 0.45s var(--ds-ease-spring, cubic-bezier(0.34,1.4,0.64,1)); }
@media (prefers-reduced-motion: reduce) {
.ttm, .ttm-knob, .ttm-svg, .ttm-cut, .ttm-rays, .ttm-disc { transition: none; }
}
`}</style>
<span className="ttm-knob" />
<svg className="ttm-svg" viewBox="0 0 24 24" aria-hidden>
<mask id="ttm-mask">
<rect width="24" height="24" fill="#fff" />
{/* этот круг съезжает на диск и вырезает полумесяц */}
<circle className="ttm-cut" cx={dark ? 16 : 30} cy={dark ? 6 : -6} r="9" fill="#000" />
</mask>
<circle
className="ttm-disc"
cx="12"
cy="12"
r={dark ? 8.5 : 5.5}
fill="currentColor"
mask="url(#ttm-mask)"
/>
<g className="ttm-rays" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
<path d="M12 1.6v2.4M12 20v2.4M4.2 4.2l1.7 1.7M18.1 18.1l1.7 1.7M1.6 12h2.4M20 12h2.4M4.2 19.8l1.7-1.7M18.1 5.9l1.7-1.7" />
</g>
</svg>
</button>
);
}
Похожие эффекты
лицензия: MITавтор: Motivaисточник: оригинал (Motiva)v1.0.0