Web InnoventixFreeCode

Radio Segmented Toggle

Original · free

segmented control radios

byWeb InnoventixReact + Tailwind
tglradiosegmentedtoggles
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-radio-segmented.json
tgl-radio-segmented.tsx
"use client";

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

type SegOption<T extends string> = {
  value: T;
  label: string;
  hint?: string;
  icon?: ReactNode;
};

type SegSize = "sm" | "md" | "lg";

const SIZE: Record<SegSize, { text: string; pad: string; gap: string }> = {
  sm: { text: "text-xs", pad: "px-3 py-1.5", gap: "gap-1.5" },
  md: { text: "text-sm", pad: "px-4 py-2", gap: "gap-2" },
  lg: { text: "text-[15px]", pad: "px-5 py-2.5", gap: "gap-2.5" },
};

function labelOf(
  opts: ReadonlyArray<{ value: string; label: string }>,
  v: string,
): string {
  return opts.find((o) => o.value === v)?.label ?? v;
}

function SegmentedControl<T extends string>({
  name,
  label,
  options,
  value,
  onChange,
  size = "md",
  fullWidth = false,
}: {
  name: string;
  label: string;
  options: ReadonlyArray<SegOption<T>>;
  value: T;
  onChange: (value: T) => void;
  size?: SegSize;
  fullWidth?: boolean;
}) {
  const pillId = useId();
  const reduce = useReducedMotion();
  const s = SIZE[size];

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

  return (
    <fieldset className="m-0 min-w-0 border-0 p-0">
      <legend className="sr-only">{label}</legend>
      <div
        className={`${
          fullWidth ? "flex w-full" : "inline-flex"
        } gap-1 rounded-xl bg-zinc-100 p-1 ring-1 ring-inset ring-zinc-200/80 dark:bg-zinc-800/80 dark:ring-white/5`}
      >
        {options.map((opt) => {
          const selected = opt.value === value;
          return (
            <label
              key={opt.value}
              className={`relative flex ${
                fullWidth ? "flex-1" : ""
              } cursor-pointer select-none`}
            >
              <input
                type="radio"
                name={name}
                value={opt.value}
                checked={selected}
                onChange={() => onChange(opt.value)}
                className="peer sr-only"
              />

              {selected && (
                <motion.span
                  aria-hidden="true"
                  layoutId={pillId}
                  transition={spring}
                  className="absolute inset-0 rounded-lg bg-white shadow-sm ring-1 ring-black/5 dark:bg-zinc-700 dark:ring-white/10"
                />
              )}

              <span
                className={`relative z-10 flex w-full items-center justify-center ${s.gap} ${s.pad} ${s.text} rounded-lg font-medium transition-colors ${
                  selected
                    ? "text-zinc-900 dark:text-white"
                    : "text-zinc-500 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200"
                } peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-1 peer-focus-visible:ring-offset-zinc-100 dark:peer-focus-visible:ring-indigo-400 dark:peer-focus-visible:ring-offset-zinc-800`}
              >
                {opt.icon}
                <span>{opt.label}</span>
                {opt.hint && (
                  <span
                    className={`ml-0.5 rounded px-1.5 py-0.5 text-[10px] font-semibold leading-none ${
                      selected
                        ? "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300"
                        : "bg-zinc-200/70 text-zinc-500 dark:bg-zinc-700/70 dark:text-zinc-400"
                    }`}
                  >
                    {opt.hint}
                  </span>
                )}
              </span>
            </label>
          );
        })}
      </div>
    </fieldset>
  );
}

function ListIcon({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.8}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <circle cx="4.5" cy="6" r="1" />
      <circle cx="4.5" cy="12" r="1" />
      <circle cx="4.5" cy="18" r="1" />
      <line x1="9" y1="6" x2="20" y2="6" />
      <line x1="9" y1="12" x2="20" y2="12" />
      <line x1="9" y1="18" x2="20" y2="18" />
    </svg>
  );
}

function GridIcon({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.8}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <rect x="3.5" y="3.5" width="7" height="7" rx="1.5" />
      <rect x="13.5" y="3.5" width="7" height="7" rx="1.5" />
      <rect x="3.5" y="13.5" width="7" height="7" rx="1.5" />
      <rect x="13.5" y="13.5" width="7" height="7" rx="1.5" />
    </svg>
  );
}

function BoardIcon({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.8}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <rect x="3.5" y="4" width="4.5" height="16" rx="1.5" />
      <rect x="9.75" y="4" width="4.5" height="11" rx="1.5" />
      <rect x="16" y="4" width="4.5" height="14" rx="1.5" />
    </svg>
  );
}

function TimelineIcon({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.8}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <rect x="3" y="5" width="12" height="3.2" rx="1.6" />
      <rect x="7" y="10.4" width="14" height="3.2" rx="1.6" />
      <rect x="4.5" y="15.8" width="9" height="3.2" rx="1.6" />
    </svg>
  );
}

function SunIcon({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.8}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <circle cx="12" cy="12" r="4" />
      <line x1="12" y1="2.5" x2="12" y2="4.5" />
      <line x1="12" y1="19.5" x2="12" y2="21.5" />
      <line x1="4.2" y1="4.2" x2="5.7" y2="5.7" />
      <line x1="18.3" y1="18.3" x2="19.8" y2="19.8" />
      <line x1="2.5" y1="12" x2="4.5" y2="12" />
      <line x1="19.5" y1="12" x2="21.5" y2="12" />
      <line x1="4.2" y1="19.8" x2="5.7" y2="18.3" />
      <line x1="18.3" y1="5.7" x2="19.8" y2="4.2" />
    </svg>
  );
}

function MoonIcon({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.8}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="M21 12.8A8.5 8.5 0 1 1 11.2 3a6.6 6.6 0 0 0 9.8 9.8Z" />
    </svg>
  );
}

function MonitorIcon({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.8}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <rect x="3" y="4" width="18" height="12" rx="2" />
      <line x1="8" y1="20" x2="16" y2="20" />
      <line x1="12" y1="16" x2="12" y2="20" />
    </svg>
  );
}

type Billing = "monthly" | "quarterly" | "annual";
type BoardView = "list" | "grid" | "board" | "timeline";
type Appearance = "light" | "dark" | "system";
type Speed = "standard" | "express" | "priority";

const CSS = `
@keyframes tglseg-fadein {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}
@keyframes tglseg-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.4; transform: scale(0.72); }
}
.tglseg-enter { animation: tglseg-fadein 0.5s ease-out both; }
.tglseg-dot { animation: tglseg-pulse 1.8s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .tglseg-enter, .tglseg-dot { animation: none !important; }
}
`;

function Card({
  title,
  description,
  readout,
  children,
}: {
  title: string;
  description: string;
  readout: string;
  children: ReactNode;
}) {
  return (
    <div className="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
      <div className="mb-4 flex items-start justify-between gap-4">
        <div className="min-w-0">
          <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
            {title}
          </h3>
          <p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
            {description}
          </p>
        </div>
        <span className="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-zinc-100 px-2.5 py-1 text-xs font-medium text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300">
          <span
            className="tglseg-dot h-1.5 w-1.5 rounded-full bg-emerald-500"
            aria-hidden="true"
          />
          {readout}
        </span>
      </div>
      {children}
    </div>
  );
}

export default function TglRadioSegmented() {
  const [billing, setBilling] = useState<Billing>("monthly");
  const [view, setView] = useState<BoardView>("grid");
  const [appearance, setAppearance] = useState<Appearance>("system");
  const [speed, setSpeed] = useState<Speed>("express");

  const billingOptions: ReadonlyArray<SegOption<Billing>> = [
    { value: "monthly", label: "Monthly" },
    { value: "quarterly", label: "Quarterly" },
    { value: "annual", label: "Annual", hint: "-20%" },
  ];

  const viewOptions: ReadonlyArray<SegOption<BoardView>> = [
    { value: "list", label: "List", icon: <ListIcon className="h-4 w-4 shrink-0" /> },
    { value: "grid", label: "Grid", icon: <GridIcon className="h-4 w-4 shrink-0" /> },
    { value: "board", label: "Board", icon: <BoardIcon className="h-4 w-4 shrink-0" /> },
    {
      value: "timeline",
      label: "Timeline",
      icon: <TimelineIcon className="h-4 w-4 shrink-0" />,
    },
  ];

  const appearanceOptions: ReadonlyArray<SegOption<Appearance>> = [
    { value: "light", label: "Light", icon: <SunIcon className="h-4 w-4 shrink-0" /> },
    { value: "dark", label: "Dark", icon: <MoonIcon className="h-4 w-4 shrink-0" /> },
    {
      value: "system",
      label: "System",
      icon: <MonitorIcon className="h-4 w-4 shrink-0" />,
    },
  ];

  const speedOptions: ReadonlyArray<SegOption<Speed>> = [
    { value: "standard", label: "Standard" },
    { value: "express", label: "Express" },
    { value: "priority", label: "Priority" },
  ];

  return (
    <section className="relative w-full overflow-hidden bg-zinc-50 px-6 py-20 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-50 sm:px-8 lg:py-28">
      <style>{CSS}</style>

      <div className="tglseg-enter mx-auto max-w-2xl">
        <header className="mb-10 text-center">
          <span className="inline-flex items-center gap-1.5 rounded-full border border-zinc-200 bg-white px-3 py-1 text-xs font-medium text-zinc-600 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300">
            <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" aria-hidden="true" />
            Segmented controls
          </span>
          <h2 className="mt-4 text-2xl font-semibold tracking-tight sm:text-3xl">
            Pick one, cleanly
          </h2>
          <p className="mx-auto mt-3 max-w-md text-sm text-zinc-500 dark:text-zinc-400">
            Accessible radio groups styled as segmented controls. Tab to focus a
            group, then use the arrow keys to switch options.
          </p>
        </header>

        <div className="space-y-5">
          <Card
            title="Billing cycle"
            description="Choose how often your workspace is charged."
            readout={labelOf(billingOptions, billing)}
          >
            <SegmentedControl
              name="billing-cycle"
              label="Billing cycle"
              options={billingOptions}
              value={billing}
              onChange={setBilling}
              size="md"
            />
          </Card>

          <Card
            title="Board layout"
            description="How tasks are arranged in the project view."
            readout={labelOf(viewOptions, view)}
          >
            <SegmentedControl
              name="board-layout"
              label="Board layout"
              options={viewOptions}
              value={view}
              onChange={setView}
              size="md"
              fullWidth
            />
          </Card>

          <Card
            title="Appearance"
            description="Theme applied across the editor and sidebar."
            readout={labelOf(appearanceOptions, appearance)}
          >
            <SegmentedControl
              name="appearance"
              label="Appearance"
              options={appearanceOptions}
              value={appearance}
              onChange={setAppearance}
              size="sm"
            />
          </Card>

          <Card
            title="Delivery speed"
            description="Applies to every export queued from this project."
            readout={labelOf(speedOptions, speed)}
          >
            <SegmentedControl
              name="delivery-speed"
              label="Delivery speed"
              options={speedOptions}
              value={speed}
              onChange={setSpeed}
              size="lg"
              fullWidth
            />
          </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 →