MioUI

Segmented Control

胶囊轨道 + SNAPPY 弹簧滑块。

安装

components/ui/segmented.tsx
'use client';
import { motion } from 'motion/react';
import { useId, type ReactNode } from 'react';
import { SPRING_SNAPPY } from '@/lib/motion';

export interface SegmentOption<T extends string> {
  value: T;
  label: ReactNode;
  icon?: ReactNode;
}

/** 胶囊轨道 + 弹簧滑块(2px 内边距,999px 圆角) */
export function Segmented<T extends string>({
  value,
  onChange,
  options,
  ariaLabel,
}: {
  value: T;
  onChange: (v: T) => void;
  options: SegmentOption<T>[];
  ariaLabel: string;
}) {
  const id = useId();
  return (
    <div role="radiogroup" aria-label={ariaLabel} className="inline-flex rounded-full bg-ghost-selected p-[2px]">
      {options.map((o) => {
        const active = o.value === value;
        return (
          <button
            key={o.value}
            role="radio"
            aria-checked={active}
            onClick={() => onChange(o.value)}
            className={
              'cds-focus relative inline-flex h-7 items-center gap-1.5 rounded-full px-3 text-[13px] font-medium transition-colors duration-[120ms] [&_svg]:size-4 ' +
              (active ? 'text-text-primary' : 'text-text-secondary hover:text-text-primary')
            }
          >
            {active && (
              <motion.span
                layoutId={`seg-${id}`}
                transition={SPRING_SNAPPY}
                className="absolute inset-0 rounded-full bg-surface-2 shadow-[0_0_0_0.5px_var(--cds-border-strong),0_1px_2px_rgba(0,0,0,0.08)] dark:bg-surface-popover"
              />
            )}
            <span className="relative z-10 inline-flex items-center gap-1.5">
              {o.icon}
              {o.label}
            </span>
          </button>
        );
      })}
    </div>
  );
}

用法

<Segmented ariaLabel="视图" value={v} onChange={setV}
  options={[{ value: "grid", label: "网格" }, { value: "list", label: "列表" }]} />

本页目录