Segmented Control

Pronto

Botão segmentado com pílula deslizante (estilo iOS). Toggle entre 2-5 opções de mesma categoria com mudança imediata.

Usar quando

Toggle entre views (Lista/Quadro/Calendário). Ranges temporais (Mês/Trimestre/Ano). Filtros mutuamente excludentes.

Não usar quando

Mais de 5 opções (use Tabs ou Select). Multi-seleção (use Checkbox). Sem mudança imediata (use Radio + Submit).

Variantes

Microinterações

MicrointeraçãoDisparada porComportamentoTiming
Pílula slideclick outra opçãoBackground ink desliza para nova posição300ms cubic-bezier(0.32,0.72,0,1)
Color invertopção ativaTexto inkMuted → surface (branco), background ink150ms ease
Hover (não-ativo)mouseenter em inactiveColor → ink (mais escuro)150ms ease
Keyboard nav← →Move seleção para opção adjacenteinstant

Acessibilidade

Acessibilidade — checklist

Teclado
TabFoco no controle
← / →Move seleção entre opções
Home / EndPrimeira / última
ARIA esperado
  • role="radiogroup" no container
  • role="radio" + aria-checked="true|false" em cada
  • aria-label="Período" no group
Notas
  • Use radiogroup, NÃO tablist (semântica diferente).
  • Mudança aplica imediato — sem botão Salvar.
  • Em mobile, toque-friendly (mínimo 32px de altura).
  • Pílula deslizante é parte da experiência — não pule animação.

Código

'use client';
import { useState } from 'react';
import { T } from '@/lib/tokens';

export function SegmentedControl({ options, value, onChange }) {
  const idx = options.findIndex(o => o.id === value);
  return (
    <div role="radiogroup" style={{
      display: 'inline-flex', position: 'relative',
      background: T.surface3, padding: 4, borderRadius: 980,
    }}>
      <span style={{
        position: 'absolute', top: 4, bottom: 4,
        left: `calc(${(idx * 100) / options.length}% + 4px)`,
        width: `calc(${100 / options.length}% - 8px)`,
        background: T.ink, borderRadius: 980,
        transition: 'left 300ms cubic-bezier(0.32,0.72,0,1)',
      }} />
      {options.map(o => (
        <button key={o.id} role="radio" aria-checked={value === o.id}
          onClick={() => onChange(o.id)}
          style={{ position: 'relative', zIndex: 1, padding: '6px 14px',
            color: value === o.id ? T.surface : T.inkMuted }}>
          {o.label}
        </button>
      ))}
    </div>
  );
}

Regras

Faça

  • Pílula deslizante (essência do componente).
  • Background surface3, pílula ink.
  • Texto inkMuted (off) / surface (on).
  • Aplica imediato.
  • Tamanho sm 24px / md 32px / lg 40px de altura.

Não faça

  • Não use para >5 opções (caos visual).
  • Não force submit após selecionar.
  • Não esconda animação (pílula precisa deslizar).
  • Não use cor decorativa nas opções.
  • Não tenha labels muito longos (caps em ~12 caracteres).

Tokens usados

TokenValorPapel
T.ink#0A0A0Apílula ativa
T.surface3#F3F0E6background do track
T.surface#FFFFFFtexto da opção ativa
T.inkMuted#5A5A5Eopções inativas
RADIUS.pill980forma
MOTION.base250mspílula slide