Web InnoventixFreeCode

Checkbox Custom Toggle

Original · free

custom animated tick checkboxes

byWeb InnoventixReact + Tailwind
tglcheckboxcustomtoggles
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-checkbox-custom.json
tgl-checkbox-custom.tsx
"use client";

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

type ColorKey = "indigo" | "violet" | "emerald" | "rose" | "amber" | "sky";
type SizeKey = "sm" | "md" | "lg";

const COLORS: Record<ColorKey, { on: string; ring: string; hover: string; tick: string }> = {
  indigo: {
    on: "border-indigo-600 bg-indigo-600 dark:border-indigo-500 dark:bg-indigo-500",
    ring: "peer-focus-visible:ring-indigo-500/60",
    hover: "peer-hover:border-indigo-400 dark:peer-hover:border-indigo-500",
    tick: "text-white",
  },
  violet: {
    on: "border-violet-600 bg-violet-600 dark:border-violet-500 dark:bg-violet-500",
    ring: "peer-focus-visible:ring-violet-500/60",
    hover: "peer-hover:border-violet-400 dark:peer-hover:border-violet-500",
    tick: "text-white",
  },
  emerald: {
    on: "border-emerald-600 bg-emerald-600 dark:border-emerald-500 dark:bg-emerald-500",
    ring: "peer-focus-visible:ring-emerald-500/60",
    hover: "peer-hover:border-emerald-400 dark:peer-hover:border-emerald-500",
    tick: "text-white",
  },
  rose: {
    on: "border-rose-600 bg-rose-600 dark:border-rose-500 dark:bg-rose-500",
    ring: "peer-focus-visible:ring-rose-500/60",
    hover: "peer-hover:border-rose-400 dark:peer-hover:border-rose-500",
    tick: "text-white",
  },
  amber: {
    on: "border-amber-500 bg-amber-500 dark:border-amber-400 dark:bg-amber-400",
    ring: "peer-focus-visible:ring-amber-500/60",
    hover: "peer-hover:border-amber-400 dark:peer-hover:border-amber-500",
    tick: "text-amber-950",
  },
  sky: {
    on: "border-sky-600 bg-sky-600 dark:border-sky-500 dark:bg-sky-500",
    ring: "peer-focus-visible:ring-sky-500/60",
    hover: "peer-hover:border-sky-400 dark:peer-hover:border-sky-500",
    tick: "text-white",
  },
};

const SIZES: Record<SizeKey, { box: string; svg: string; label: string; gap: string }> = {
  sm: { box: "h-[18px] w-[18px] rounded-[6px]", svg: "h-3.5 w-3.5", label: "text-sm", gap: "gap-2.5" },
  md: { box: "h-[22px] w-[22px] rounded-[7px]", svg: "h-4 w-4", label: "text-[15px]", gap: "gap-3" },
  lg: { box: "h-[26px] w-[26px] rounded-lg", svg: "h-[18px] w-[18px]", label: "text-base", gap: "gap-3" },
};

const KEYFRAMES = `
@keyframes tglcc-pop {
  0% { transform: scale(1); }
  38% { transform: scale(0.82); }
  70% { transform: scale(1.08); }
  100% { transform: scale(1); }
}
@keyframes tglcc-rise {
  from { opacity: 0; transform: translateY(14px); }
  to { opacity: 1; transform: translateY(0); }
}
.tglcc-pop { animation: tglcc-pop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); }
.tglcc-rise { animation: tglcc-rise 0.5s ease-out both; }
@media (prefers-reduced-motion: reduce) {
  .tglcc-pop, .tglcc-rise { animation: none !important; }
}
`;

function TickCheckbox({
  checked,
  indeterminate = false,
  onChange,
  label,
  description,
  color = "indigo",
  size = "md",
  disabled = false,
}: {
  checked: boolean;
  indeterminate?: boolean;
  onChange: (next: boolean) => void;
  label: string;
  description?: string;
  color?: ColorKey;
  size?: SizeKey;
  disabled?: boolean;
}) {
  const reduce = useReducedMotion();
  const id = useId();
  const ref = useRef<HTMLInputElement>(null);
  const c = COLORS[color];
  const s = SIZES[size];
  const active = checked || indeterminate;
  const showTick = checked && !indeterminate;

  useEffect(() => {
    if (ref.current) ref.current.indeterminate = indeterminate;
  }, [indeterminate]);

  return (
    <label
      className={[
        "group flex items-start",
        s.gap,
        disabled ? "cursor-not-allowed" : "cursor-pointer",
      ].join(" ")}
    >
      <span className="relative mt-0.5 inline-flex shrink-0 items-center justify-center">
        <input
          ref={ref}
          id={id}
          type="checkbox"
          className="peer sr-only"
          checked={checked}
          disabled={disabled}
          onChange={(e) => onChange(e.target.checked)}
          aria-describedby={description ? `${id}-desc` : undefined}
        />
        <span
          aria-hidden="true"
          className={[
            "flex items-center justify-center border-2 transition-colors duration-200",
            s.box,
            active ? c.on : "border-slate-300 bg-white dark:border-slate-600 dark:bg-slate-900",
            disabled ? "" : c.hover,
            "peer-focus-visible:outline-none peer-focus-visible:ring-2 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-white dark:peer-focus-visible:ring-offset-slate-950",
            c.ring,
            c.tick,
            active && !reduce ? "tglcc-pop" : "",
            disabled ? "opacity-50" : "",
          ].join(" ")}
        >
          <svg viewBox="0 0 24 24" fill="none" className={s.svg}>
            <motion.path
              d="M5 12.5 L10 17.5 L19 7"
              stroke="currentColor"
              strokeWidth={3}
              strokeLinecap="round"
              strokeLinejoin="round"
              initial={false}
              animate={{ pathLength: showTick ? 1 : 0, opacity: showTick ? 1 : 0 }}
              transition={reduce ? { duration: 0 } : { duration: 0.25, ease: "easeOut" }}
            />
            <motion.line
              x1="6"
              y1="12"
              x2="18"
              y2="12"
              stroke="currentColor"
              strokeWidth={3}
              strokeLinecap="round"
              initial={false}
              animate={{ pathLength: indeterminate ? 1 : 0, opacity: indeterminate ? 1 : 0 }}
              transition={reduce ? { duration: 0 } : { duration: 0.2, ease: "easeOut" }}
            />
          </svg>
        </span>
      </span>
      <span className="flex min-w-0 flex-col">
        <span
          className={[
            "font-medium leading-tight text-slate-800 dark:text-slate-100",
            s.label,
            disabled ? "opacity-60" : "",
          ].join(" ")}
        >
          {label}
        </span>
        {description ? (
          <span
            id={`${id}-desc`}
            className={[
              "mt-1 text-sm leading-snug text-slate-500 dark:text-slate-400",
              disabled ? "opacity-60" : "",
            ].join(" ")}
          >
            {description}
          </span>
        ) : null}
      </span>
    </label>
  );
}

function Card({ title, hint, children }: { title: string; hint: string; children: ReactNode }) {
  return (
    <div className="tglcc-rise rounded-2xl border border-slate-200 bg-white p-6 shadow-sm shadow-slate-900/5 dark:border-slate-800 dark:bg-slate-900 dark:shadow-black/20">
      <div className="mb-5 flex items-baseline justify-between gap-3">
        <h3 className="text-base font-semibold text-slate-900 dark:text-white">{title}</h3>
        <span className="shrink-0 text-xs font-medium tabular-nums text-slate-400 dark:text-slate-500">
          {hint}
        </span>
      </div>
      {children}
    </div>
  );
}

const TAGS: { key: ColorKey; label: string }[] = [
  { key: "indigo", label: "Feature" },
  { key: "violet", label: "Design" },
  { key: "emerald", label: "Docs" },
  { key: "sky", label: "Research" },
  { key: "amber", label: "Chore" },
  { key: "rose", label: "Bug" },
];

const TASKS = [
  "Run the full test suite",
  "Update the changelog",
  "Tag release v2.4.0",
  "Notify the #releases channel",
];

export default function TglCheckboxCustom() {
  const [prefs, setPrefs] = useState({
    updates: false,
    security: true,
    billing: false,
    community: true,
  });
  const prefsCount = Object.values(prefs).filter(Boolean).length;

  const [tasks, setTasks] = useState<boolean[]>([true, true, false, false]);
  const doneCount = tasks.filter(Boolean).length;
  const allDone = tasks.every(Boolean);
  const someDone = tasks.some(Boolean);
  const parentIndeterminate = someDone && !allDone;

  const [sizes, setSizes] = useState({ sm: true, md: true, lg: false });

  const [tagState, setTagState] = useState<Record<ColorKey, boolean>>({
    indigo: true,
    violet: false,
    emerald: true,
    rose: false,
    amber: true,
    sky: false,
  });
  const tagCount = Object.values(tagState).filter(Boolean).length;

  return (
    <section className="relative w-full bg-slate-50 px-6 py-20 dark:bg-slate-950 sm:px-8 lg:py-24">
      <style>{KEYFRAMES}</style>

      <div className="mx-auto max-w-5xl">
        <header className="tglcc-rise max-w-2xl">
          <span className="inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-indigo-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-indigo-700 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300">
            Toggles
          </span>
          <h2 className="mt-4 text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
            Custom tick checkboxes
          </h2>
          <p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-slate-400">
            Real native inputs behind a hand-drawn tick that strokes in on check. Full keyboard
            support, visible focus rings, an indeterminate select-all, and a reduced-motion
            fallback that skips the animation.
          </p>
        </header>

        <div className="mt-12 grid gap-6 lg:grid-cols-2">
          <Card title="Email preferences" hint={`${prefsCount} of 4 enabled`}>
            <div className="flex flex-col gap-5">
              <TickCheckbox
                color="indigo"
                label="Product updates"
                description="New features, improvements, and changelog highlights."
                checked={prefs.updates}
                onChange={(next) => setPrefs((p) => ({ ...p, updates: next }))}
              />
              <TickCheckbox
                color="indigo"
                label="Security alerts"
                description="Sign-in attempts, new devices, and password changes."
                checked={prefs.security}
                onChange={(next) => setPrefs((p) => ({ ...p, security: next }))}
              />
              <TickCheckbox
                color="indigo"
                label="Billing receipts"
                description="Monthly invoices and payment confirmations."
                checked={prefs.billing}
                onChange={(next) => setPrefs((p) => ({ ...p, billing: next }))}
              />
              <TickCheckbox
                color="indigo"
                label="Community digest"
                description="Locked to your plan while your trial is active."
                checked={prefs.community}
                disabled
                onChange={(next) => setPrefs((p) => ({ ...p, community: next }))}
              />
            </div>
          </Card>

          <Card title="Deploy checklist" hint={`${doneCount}/${TASKS.length} complete`}>
            <TickCheckbox
              color="emerald"
              label="Select all steps"
              checked={allDone}
              indeterminate={parentIndeterminate}
              onChange={(next) => setTasks(TASKS.map(() => next))}
            />
            <div className="my-4 h-px bg-slate-200 dark:bg-slate-800" />
            <div className="flex flex-col gap-4 pl-1">
              {TASKS.map((task, i) => (
                <TickCheckbox
                  key={task}
                  color="emerald"
                  size="sm"
                  label={task}
                  checked={tasks[i]}
                  onChange={(next) =>
                    setTasks((prev) => prev.map((t, idx) => (idx === i ? next : t)))
                  }
                />
              ))}
            </div>
          </Card>
        </div>

        <div className="mt-6 grid gap-6 sm:grid-cols-2">
          <Card title="Sizes" hint="sm / md / lg">
            <div className="flex flex-col gap-5">
              <TickCheckbox
                color="sky"
                size="sm"
                label="Small"
                checked={sizes.sm}
                onChange={(next) => setSizes((v) => ({ ...v, sm: next }))}
              />
              <TickCheckbox
                color="sky"
                size="md"
                label="Medium"
                checked={sizes.md}
                onChange={(next) => setSizes((v) => ({ ...v, md: next }))}
              />
              <TickCheckbox
                color="sky"
                size="lg"
                label="Large"
                checked={sizes.lg}
                onChange={(next) => setSizes((v) => ({ ...v, lg: next }))}
              />
            </div>
          </Card>

          <Card title="Filter by label" hint={`${tagCount} active`}>
            <div className="grid grid-cols-2 gap-x-4 gap-y-5">
              {TAGS.map((tag) => (
                <TickCheckbox
                  key={tag.key}
                  color={tag.key}
                  label={tag.label}
                  checked={tagState[tag.key]}
                  onChange={(next) => setTagState((v) => ({ ...v, [tag.key]: next }))}
                />
              ))}
            </div>
          </Card>
        </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 →