Segmented

←→

Choix exclusif entre deux à cinq options courtes, toutes visibles en même temps (pratique, niveau, mode de couleur du profil).

stableÀ valider
// Titre : Par défaut
import { useState } from 'react';
import Segmented from '@pathou/components/Segmented';

export default function Exemple() {
  const [pratique, setPratique] = useState<'route' | 'gravel' | 'vtt'>('route');
  return (
    <Segmented
      label="Pratique"
      value={pratique}
      options={[
        ['route', 'Route'],
        ['gravel', 'Gravel'],
        ['vtt', 'VTT'],
      ]}
      onChange={setPratique}
    />
  );
}

Installation

Copie le fichier dans ton projet, ainsi que les tokens qu’il utilise.

src/components/Segmented.tsx
import type { KeyboardEvent } from 'react';

/**
 * Choix exclusif (pratique, niveau). Clavier comme un groupe de boutons radio : une seule étape de tabulation,
 * sur l'option choisie, et les flèches passent à l'option voisine (Début et Fin aux extrémités).
 */
export default function Segmented<T extends string>({ label, value, options, onChange }: { label: string; value: T; options: [T, string][]; onChange: (v: T) => void }) {
  const onKey = (e: KeyboardEvent<HTMLDivElement>) => {
    const i = options.findIndex(([v]) => v === value);
    const n = options.length;
    const next = { ArrowRight: i + 1, ArrowDown: i + 1, ArrowLeft: i - 1, ArrowUp: i - 1, Home: 0, End: n - 1 }[e.key];
    if (next === undefined) return;
    e.preventDefault();
    const j = (next + n) % n;
    onChange(options[j][0]);
    (e.currentTarget.children[j] as HTMLElement | undefined)?.focus();
  };
  return (
    <div className="segmented" role="radiogroup" aria-label={label} onKeyDown={onKey}>
      {options.map(([v, text]) => (
        <button key={v} role="radio" aria-checked={v === value} tabIndex={v === value ? 0 : -1} className={v === value ? 'on' : ''} onClick={() => onChange(v)}>
          {text}
        </button>
      ))}
    </div>
  );
}

Tokens : --panel --text --muted --chip

Utilisation

import Segmented from './components/Segmented';

const [pratique, setPratique] = useState<'route' | 'gravel' | 'vtt'>('route');

<Segmented
  label="Pratique"
  value={pratique}
  options={[['route', 'Route'], ['gravel', 'Gravel'], ['vtt', 'VTT']]}
  onChange={setPratique}
/>

Anatomie

#ÉlémentRôle
1RailLe conteneur radiogroup, qui porte le libellé (aria-label). Fond --chip, translucide.
2Option choisieUn button de rôle radio avec aria-checked="true". Fond --panel, texte --text.
3Option libreUn button de rôle radio avec aria-checked="false", hors de l’ordre de tabulation. Texte --muted.

États

checkedL’option choisie se détache sur le rail, sur fond de panneau avec une ombre légère. Elle est la seule à recevoir la tabulation.
hoverUne option non choisie survolée garde son style ; seul le curseur change.
focusVisibleAu clavier, l’option qui a le focus porte l’anneau de focus du navigateur.

Guidelines

  • MUST
    Donner un label qui nomme le choix (« Pratique », « Niveau ») ; il est annoncé par les lecteurs d’écran et n’est pas affiché.
  • SHOULD
    Garder des options d’un ou deux mots, pour que le groupe tienne sur une ligne en mobile.
  • MUST NOT
    Dépasser cinq options ; au-delà, passer à une liste déroulante.

Clavier

ToucheEffet
TabEntre dans le groupe sur l’option choisie, puis en sort ; une seule étape de tabulation pour tout le groupe.
FlèchesPassent à l’option voisine et la choisissent aussitôt, en boucle.
DébutFinChoisissent la première ou la dernière option.

Référence de l’API

PropTypeDéfaut
labelstringrequis
valueTrequis
options[T, string][]requis
onChange(v: T) => voidrequis