# Segmented control

> A row of options where a colored pill slides over to whichever one you pick.

- Category: [Filters](https://library.aimran.es/components/filters)
- Built with: React + Motion
- Live editor: https://library.aimran.es/blocks/segmented-control
- Library: AIMRAN Library (https://library.aimran.es), free, no sign-up, by Aimran Oukham

## Where to use it

A segmented control is for switching views, not for filtering: “Monthly / Yearly” on a pricing page, “List / Map” in a property search, or “Day / Week / Month” in a calendar. Because the pill slides instead of jumping, people see what changed. Reach for this segmented control when there are two to four choices.

It gets cramped beyond four options, especially on phones, so trim the list or switch to tabs. Pick an accent with enough contrast for white text, since the selected label turns white.

## Code

### React (Motion)

```tsx
import { useId, useState } from "react";
import { motion } from "framer-motion";

const sizeMap = {
  sm: { pad: "px-3 py-1.5", text: "text-xs" },
  md: { pad: "px-4 py-2", text: "text-sm" },
  lg: { pad: "px-5 py-2.5", text: "text-base" },
} as const;

interface SegmentedControlProps {
  accent?: string;
  size?: keyof typeof sizeMap;
  options?: string[];
  value?: string;
  onChange?: (value: string) => void;
}

export default function SegmentedControl({
  accent = "#0071e3",
  size = "md",
  options = ["Design", "Code", "Audio"],
  value,
  onChange,
}: SegmentedControlProps) {
  const [internal, setInternal] = useState(options[0] ?? "");
  const layoutId = useId();
  const active = value ?? internal;
  const current = options.includes(active) ? active : options[0];
  const s = sizeMap[size];

  const select = (o: string) => {
    if (value === undefined) setInternal(o);
    onChange?.(o);
  };

  return (
    <div className="flex rounded-full bg-black/5 p-1 dark:bg-white/10">
      {options.map((o) => (
        <button
          key={o}
          type="button"
          aria-pressed={current === o}
          onClick={() => select(o)}
          className={`relative rounded-full font-medium ${s.pad} ${s.text}`}
        >
          {current === o && (
            <motion.span
              layoutId={layoutId}
              className="absolute inset-0 rounded-full"
              style={{ backgroundColor: accent }}
              transition={{ type: "spring", stiffness: 380, damping: 30 }}
            />
          )}
          <span
            className="relative z-10"
            style={{ color: current === o ? "#ffffff" : undefined }}
          >
            {o}
          </span>
        </button>
      ))}
    </div>
  );
}
```

## Prompt for AI assistants

Paste this into ChatGPT, Claude, Cursor, v0 or Lovable to adapt the block to your stack (Tailwind, Vue, Svelte…):

````text
I want to add the “Segmented control” block (Filters) from AIMRAN Library to my project.

## What it is
A row of options where a colored pill slides over to whichever one you pick.

## Chosen settings
- Accent color: #0071e3
- Number of options: 3 (from 2 to 8)
- Size: Medium (other options: Small, Large)

## What I need
1. Add it to my project with the same look and animation as the reference code: same sizes, colours, radii, shadows, timings and easing.
2. It is built with React + TypeScript, with framer-motion for the animation and lucide-react for the icons. If my project uses something else (Vue, Svelte, Astro, plain HTML…), translate it without losing any behaviour. If I use Tailwind, turn the styles into classes; if not, keep the CSS in its own file.
3. Expose the settings above as props (or CSS variables) with those values as defaults.
4. Make it work well on phones: nothing that depends only on hover, touch targets of at least 44 px and no overflow on 320 px screens.
5. Make it look right in light and dark mode.
6. Keep it accessible: semantic HTML, roles and aria-* where needed, visible focus and keyboard support.
7. Respect prefers-reduced-motion: with reduced motion, no big animations but the same end result.

## Reference code
```tsx
import { useId, useState } from "react";
import { motion } from "framer-motion";

const sizeMap = {
  sm: { pad: "px-3 py-1.5", text: "text-xs" },
  md: { pad: "px-4 py-2", text: "text-sm" },
  lg: { pad: "px-5 py-2.5", text: "text-base" },
} as const;

interface SegmentedControlProps {
  accent?: string;
  size?: keyof typeof sizeMap;
  options?: string[];
  value?: string;
  onChange?: (value: string) => void;
}

export default function SegmentedControl({
  accent = "#0071e3",
  size = "md",
  options = ["Design", "Code", "Audio"],
  value,
  onChange,
}: SegmentedControlProps) {
  const [internal, setInternal] = useState(options[0] ?? "");
  const layoutId = useId();
  const active = value ?? internal;
  const current = options.includes(active) ? active : options[0];
  const s = sizeMap[size];

  const select = (o: string) => {
    if (value === undefined) setInternal(o);
    onChange?.(o);
  };

  return (
    <div className="flex rounded-full bg-black/5 p-1 dark:bg-white/10">
      {options.map((o) => (
        <button
          key={o}
          type="button"
          aria-pressed={current === o}
          onClick={() => select(o)}
          className={`relative rounded-full font-medium ${s.pad} ${s.text}`}
        >
          {current === o && (
            <motion.span
              layoutId={layoutId}
              className="absolute inset-0 rounded-full"
              style={{ backgroundColor: accent }}
              transition={{ type: "spring", stiffness: 380, damping: 30 }}
            />
          )}
          <span
            className="relative z-10"
            style={{ color: current === o ? "#ffffff" : undefined }}
          >
            {o}
          </span>
        </button>
      ))}
    </div>
  );
}
```

Give me the complete final code, ready to paste, and tell me in one line where to put it in my project.
````
