bottega

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):

json
// components.json — register the @bottega namespace once
{
  "registries": {
    "@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
  }
}

2. Add the component:

bash
npx shadcn add @bottega/emoji-spin-toggle

Usage

Usagetsx
<EmojiSpinToggle checked onChange={undefined} />

Props

PropTypeDefaultDescription
checked*booleanControlled checked state.
onChange*(checked: boolean) => voidCalled when the user toggles the switch.
labelstringAccessible label for the switch (used as aria-label when no visible label is present).
disabledbooleanfalseWhether the toggle is disabled.
idstringOptional id — auto-generated from label if omitted.
classNamestringAdditional class names merged onto the root label element.

Source

emoji-spin-toggle.tsxtsx
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