primitives
Emoji Spin Toggle
A circular toggle switch whose thumb spins 180° on every state change and displays ✖️ (off) or ✔️ (on) emoji icons. The circular track shifts from a rose/danger tint (off) to a success tint (on) via a CSS color transition on the background-color. The thumb counter-rotates over 500 ms so the emoji always reads upright; hovering scales the thumb to 75% for tactile feedback. All transitions are CSS-only with no JS animation library. The toggle is built on a visually-hidden native `<input type="checkbox" role="switch">` for full keyboard support and screen-reader semantics — the visual track and thumb are purely decorative. Colors are derived exclusively from `--primary` and `--background` tokens via `color-mix(in oklch, …)`, so the component themes in both light and dark without any additional CSS.
Ported from Javierrocadev (MIT)
Install
1. Register the namespace (once per project):
// components.json — register the @bottega namespace once
{
"registries": {
"@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
}
}2. Add the component:
npx shadcn add @bottega/emoji-spin-toggleUsage
<EmojiSpinToggle checked onChange={undefined} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| checked* | boolean | — | Controlled checked state. |
| onChange* | (checked: boolean) => void | — | Called when the user toggles the switch. |
| label | string | — | Accessible label for the switch (used as aria-label when no visible label is present). |
| disabled | boolean | false | Whether the toggle is disabled. |
| id | string | — | Optional id — auto-generated from label if omitted. |
| className | string | Additional class names merged onto the root label element. |
Source
import type React from "react";
import styles from "./emoji-spin-toggle.module.css";
export type EmojiSpinToggleProps = {
/** Controlled checked state. */
checked: boolean;
/** Called when the user toggles the switch. */
onChange: (checked: boolean) => void;
/** Accessible label for the switch (used as aria-label when no visible label is present). */
label?: string;
/** Whether the toggle is disabled. */
disabled?: boolean;
/** Optional id — auto-generated from label if omitted. */
id?: string;
/** Additional class names merged onto the root label element. */
className?: string;
};
/**
* A circular toggle switch whose thumb spins 180° on toggle and displays
* ✖️ (off) or ✔️ (on) emoji icons. The track color shifts from a rose/danger
* tint (off) to a success/primary tint (on). Purely CSS-driven transitions;
* no JS animation libs needed. Uses a visually-hidden native checkbox for
* full a11y and keyboard support.
*/
export function EmojiSpinToggle({
checked,
onChange,
label,
disabled = false,
id,
className = "",
}: EmojiSpinToggleProps) {
const inputId = id ?? (label ? `emoji-spin-toggle-${label.toLowerCase().replace(/\s+/g, "-")}` : "emoji-spin-toggle");
return (
<label
htmlFor={inputId}
className={[styles.root, disabled && styles.disabled, className]
.filter(Boolean)
.join(" ")}
aria-label={label}
>
{/* Visually-hidden native checkbox — carries state + a11y */}
<input
id={inputId}
type="checkbox"
role="switch"
className={styles.input}
checked={checked}
disabled={disabled}
onChange={(e) => onChange(e.target.checked)}
aria-checked={checked}
aria-label={label}
/>
{/* Custom-drawn track + thumb */}
<span aria-hidden="true" className={styles.track}>
<span className={styles.thumb}>
<span className={styles.icon}>{checked ? "✔️" : "✖️"}</span>
</span>
</span>
</label>
);
}
Dependencies
- @bottega/tokens