Web InnoventixFreeCode

Switch Labels Toggle

Original · free

switches with on/off labels inside

byWeb InnoventixReact + Tailwind
tglswitchlabelstoggles
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/tgl-switch-labels.json
tgl-switch-labels.tsx
"use client";

import { useId, useState, type ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";

type SwitchSize = "sm" | "md" | "lg";
type SwitchTone = "indigo" | "emerald" | "violet" | "rose" | "sky" | "amber";

const SIZE: Record<SwitchSize, { track: string; thumb: string; text: string; on: string; off: string }> = {
  sm: { track: "h-7 w-16 px-1", thumb: "h-5 w-5", text: "text-[10px]", on: "pl-2", off: "pr-2" },
  md: { track: "h-9 w-[4.75rem] px-1", thumb: "h-7 w-7", text: "text-[11px]", on: "pl-2.5", off: "pr-2.5" },
  lg: { track: "h-11 w-24 px-1.5", thumb: "h-8 w-8", text: "text-xs", on: "pl-3", off: "pr-3" },
};

const TONE: Record<SwitchTone, { on: string; ring: string }> = {
  indigo: { on: "bg-indigo-600 dark:bg-indigo-500", ring: "focus-visible:ring-indigo-500" },
  emerald: { on: "bg-emerald-600 dark:bg-emerald-500", ring: "focus-visible:ring-emerald-500" },
  violet: { on: "bg-violet-600 dark:bg-violet-500", ring: "focus-visible:ring-violet-500" },
  rose: { on: "bg-rose-600 dark:bg-rose-500", ring: "focus-visible:ring-rose-500" },
  sky: { on: "bg-sky-600 dark:bg-sky-500", ring: "focus-visible:ring-sky-500" },
  amber: { on: "bg-amber-500 dark:bg-amber-400", ring: "focus-visible:ring-amber-500" },
};

function IconCheck({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={3} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <path d="M20 6 9 17l-5-5" />
    </svg>
  );
}

function IconX({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={3} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <path d="M18 6 6 18M6 6l12 12" />
    </svg>
  );
}

function IconSun({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <circle cx="12" cy="12" r="4" />
      <path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
    </svg>
  );
}

function IconMoon({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" />
    </svg>
  );
}

interface LabelSwitchProps {
  checked: boolean;
  onCheckedChange: (value: boolean) => void;
  onLabel: string;
  offLabel: string;
  labelledBy?: string;
  describedBy?: string;
  ariaLabel?: string;
  tone?: SwitchTone;
  size?: SwitchSize;
  disabled?: boolean;
  renderIcon?: (checked: boolean) => ReactNode;
}

function LabelSwitch({
  checked,
  onCheckedChange,
  onLabel,
  offLabel,
  labelledBy,
  describedBy,
  ariaLabel,
  tone = "indigo",
  size = "md",
  disabled = false,
  renderIcon,
}: LabelSwitchProps) {
  const reduce = useReducedMotion();
  const s = SIZE[size];
  const t = TONE[tone];

  const spring = reduce
    ? { duration: 0 }
    : { type: "spring" as const, stiffness: 520, damping: 34, mass: 0.7 };

  return (
    <button
      type="button"
      role="switch"
      aria-checked={checked}
      aria-labelledby={labelledBy}
      aria-describedby={describedBy}
      aria-label={ariaLabel}
      disabled={disabled}
      onClick={() => onCheckedChange(!checked)}
      className={`relative inline-flex shrink-0 items-center rounded-full outline-none ring-1 ring-inset ring-black/5 transition-colors duration-200 dark:ring-white/10 ${
        checked ? t.on : "bg-slate-200 dark:bg-slate-700"
      } ${s.track} ${checked ? "justify-end" : "justify-start"} ${t.ring} cursor-pointer focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 dark:focus-visible:ring-offset-slate-900`}
    >
      <span
        key={checked ? "on" : "off-hidden"}
        aria-hidden="true"
        className={`tglsl-anim pointer-events-none absolute inset-y-0 left-0 flex items-center font-semibold uppercase tracking-wider text-white transition-opacity duration-200 ${s.text} ${s.on} ${
          checked ? "animate-[tglsl-label-in_240ms_ease-out] opacity-100" : "opacity-0"
        }`}
      >
        {onLabel}
      </span>
      <span
        key={checked ? "off-hidden" : "off"}
        aria-hidden="true"
        className={`tglsl-anim pointer-events-none absolute inset-y-0 right-0 flex items-center font-semibold uppercase tracking-wider text-slate-600 transition-opacity duration-200 dark:text-slate-300 ${s.text} ${s.off} ${
          checked ? "opacity-0" : "animate-[tglsl-label-in_240ms_ease-out] opacity-100"
        }`}
      >
        {offLabel}
      </span>
      <motion.span
        layout
        transition={spring}
        aria-hidden="true"
        className={`z-10 flex items-center justify-center rounded-full bg-white text-slate-700 shadow-[0_1px_3px_rgba(15,23,42,0.28)] ${s.thumb} dark:bg-slate-50`}
      >
        {renderIcon ? renderIcon(checked) : null}
      </motion.span>
    </button>
  );
}

interface PrefRow {
  key: string;
  title: string;
  desc: string;
  onLabel: string;
  offLabel: string;
  tone: SwitchTone;
  icons: boolean;
}

const PREF_ROWS: PrefRow[] = [
  { key: "push", title: "Push notifications", desc: "Alerts for mentions, replies, and system events.", onLabel: "Live", offLabel: "Off", tone: "emerald", icons: true },
  { key: "renew", title: "Auto-renew plan", desc: "Charge the card on file when your billing cycle ends.", onLabel: "On", offLabel: "Off", tone: "indigo", icons: false },
  { key: "profile", title: "Public profile", desc: "Let anyone with the link view your activity page.", onLabel: "On", offLabel: "Off", tone: "violet", icons: false },
  { key: "marketing", title: "Marketing emails", desc: "Product tips, changelog digests, and the odd offer.", onLabel: "Yes", offLabel: "No", tone: "amber", icons: false },
];

export default function TglSwitchLabels() {
  const baseId = useId();
  const [demo, setDemo] = useState<Record<SwitchSize, boolean>>({ sm: true, md: false, lg: true });
  const [prefs, setPrefs] = useState<Record<string, boolean>>({ push: true, renew: true, profile: false, marketing: false });
  const [themeDark, setThemeDark] = useState(false);

  const sizes: SwitchSize[] = ["sm", "md", "lg"];

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-6 py-16 text-slate-900 dark:bg-slate-950 dark:text-slate-100 sm:px-8 md:py-24">
      <style>{`
        @keyframes tglsl-label-in {
          0% { opacity: 0; transform: translateY(2px) scale(0.9); }
          100% { opacity: 1; transform: translateY(0) scale(1); }
        }
        @media (prefers-reduced-motion: reduce) {
          .tglsl-anim { animation: none !important; }
        }
      `}</style>

      <div className="mx-auto max-w-3xl">
        <header className="mb-12 md:mb-16">
          <span className="inline-flex items-center gap-2 rounded-full bg-indigo-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-indigo-700 ring-1 ring-inset ring-indigo-200 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-400/20">
            Toggles
          </span>
          <h2 className="mt-5 text-3xl font-semibold tracking-tight sm:text-4xl">
            Switches with labels inside
          </h2>
          <p className="mt-3 max-w-xl text-base leading-relaxed text-slate-600 dark:text-slate-400">
            The current state is written into the track, so there is never a guess about which way is on. Fully keyboard operable, screen-reader labelled, and legible in both themes.
          </p>
        </header>

        {/* Sizes */}
        <div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900 sm:p-8">
          <h3 className="text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
            Three sizes
          </h3>
          <div className="mt-6 flex flex-wrap items-end gap-x-10 gap-y-8">
            {sizes.map((sz) => {
              const id = `${baseId}-size-${sz}`;
              return (
                <div key={sz} className="flex flex-col items-center gap-3">
                  <LabelSwitch
                    size={sz}
                    tone="indigo"
                    onLabel="On"
                    offLabel="Off"
                    checked={demo[sz]}
                    onCheckedChange={(v) => setDemo((p) => ({ ...p, [sz]: v }))}
                    labelledBy={id}
                  />
                  <span id={id} className="text-xs font-medium uppercase tracking-wider text-slate-500 dark:text-slate-400">
                    {sz}
                  </span>
                </div>
              );
            })}
          </div>
        </div>

        {/* Preferences */}
        <div className="mt-8 rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
          <div className="border-b border-slate-200 px-6 py-5 dark:border-slate-800 sm:px-8">
            <h3 className="text-base font-semibold">Account preferences</h3>
            <p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
              Changes save the moment you flip a switch.
            </p>
          </div>
          <ul className="divide-y divide-slate-200 dark:divide-slate-800">
            {PREF_ROWS.map((row) => {
              const titleId = `${baseId}-${row.key}-title`;
              const descId = `${baseId}-${row.key}-desc`;
              const on = prefs[row.key];
              return (
                <li key={row.key} className="flex items-center justify-between gap-6 px-6 py-5 sm:px-8">
                  <div className="min-w-0">
                    <p id={titleId} className="text-sm font-semibold text-slate-800 dark:text-slate-100">
                      {row.title}
                    </p>
                    <p id={descId} className="mt-0.5 text-sm text-slate-500 dark:text-slate-400">
                      {row.desc}
                    </p>
                  </div>
                  <LabelSwitch
                    tone={row.tone}
                    size="md"
                    onLabel={row.onLabel}
                    offLabel={row.offLabel}
                    checked={on}
                    onCheckedChange={(v) => setPrefs((p) => ({ ...p, [row.key]: v }))}
                    labelledBy={titleId}
                    describedBy={descId}
                    renderIcon={
                      row.icons
                        ? (c) =>
                            c ? (
                              <IconCheck className="h-3.5 w-3.5 text-emerald-600" />
                            ) : (
                              <IconX className="h-3.5 w-3.5 text-slate-400" />
                            )
                        : undefined
                    }
                  />
                </li>
              );
            })}
          </ul>
        </div>

        {/* Theme */}
        <div className="mt-8 grid gap-6 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900 sm:grid-cols-[1fr_auto] sm:items-center sm:p-8">
          <div>
            <h3 className="text-base font-semibold">Appearance</h3>
            <p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
              Preview panel updates instantly — currently{" "}
              <span className="font-semibold text-slate-700 dark:text-slate-200">
                {themeDark ? "Dark" : "Light"} mode
              </span>
              .
            </p>
            <div
              className={`mt-5 flex items-center gap-3 rounded-xl border p-4 transition-colors duration-300 ${
                themeDark ? "border-slate-700 bg-slate-800" : "border-slate-200 bg-slate-50"
              }`}
            >
              <span
                className={`flex h-9 w-9 items-center justify-center rounded-lg text-lg font-bold transition-colors duration-300 ${
                  themeDark ? "bg-slate-700 text-sky-300" : "bg-white text-sky-600 shadow-sm"
                }`}
              >
                Aa
              </span>
              <div className="space-y-1.5">
                <span className={`block h-2 w-32 rounded-full transition-colors duration-300 ${themeDark ? "bg-slate-600" : "bg-slate-300"}`} />
                <span className={`block h-2 w-20 rounded-full transition-colors duration-300 ${themeDark ? "bg-slate-700" : "bg-slate-200"}`} />
              </div>
            </div>
          </div>
          <div className="flex justify-start sm:justify-end">
            <LabelSwitch
              tone="sky"
              size="lg"
              onLabel="Dark"
              offLabel="Light"
              checked={themeDark}
              onCheckedChange={setThemeDark}
              ariaLabel="Toggle dark mode preview"
              renderIcon={(c) =>
                c ? <IconMoon className="h-4 w-4 text-sky-600" /> : <IconSun className="h-4 w-4 text-amber-500" />
              }
            />
          </div>
        </div>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →