Button

←→

Les boutons de Pathou ne sont pas un composant React. Ce sont des <button> natifs mis en forme par des classes de styles.css. Cette fiche les réunit tous, des variantes de texte (primary, secondary, ghost, lien) aux boutons icône et aux boutons flottants de la carte.

stableÀ valider
// Titre : Variantes
export default function Exemple() {
  return (
    <>
      <button type="button" className="primary">
        Rester sur la trace
      </button>
      <button type="button" className="secondary">
        Revenir à l’accueil
      </button>
      <button type="button">Exporter le GPX</button>
      <button type="button" className="ghost">
        Visite guidée
      </button>
      <button type="button" className="link">
        estimée
      </button>
    </>
  );
}

Installation

Copie ces règles dans ta feuille de style, ainsi que les tokens qu’elles utilisent.

src/styles.css (extrait)
button, select, input { font: inherit; color: inherit; }

button {
  border: 1px solid var(--line);
  background: var(--panel);
  border-radius: 8px;
  padding: 6px 12px;
  cursor: pointer;
}

button:hover { border-color: var(--muted); }

button.primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); font-weight: 600; }

/* action secondaire : même couleur que l'action principale, sur le fond de page */
button.secondary { background: var(--bg); color: var(--accent); border: 1.5px solid var(--accent); font-weight: 600; }

button.secondary:hover { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, var(--bg)); }

button.ghost { background: transparent; border-color: transparent; }

button.ghost:hover { background: var(--chip); }

/* zone de clic sur toute la largeur et 40 px de haut ; la marge négative du titre garde le texte aligné sur le contenu */
.fold-btn {
  display: flex; align-items: center; gap: 0; width: 100%; min-height: 40px; padding: 8px; border: 0; border-radius: 10px;
  background: none; font: inherit; color: inherit; text-align: left; cursor: pointer;
}

.fold-btn:hover { color: var(--text); background: var(--chip); }

.fold-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 0; }

.fold-chevron { margin-left: auto; flex: none; transition: transform 0.2s var(--ease-out); }

@media (pointer: coarse) {
  .fold-btn { min-height: 44px; }
}

@media (prefers-reduced-motion: reduce) {
  .fold-chevron, .map-content-chevron, .conf-chevron, .switch i { transition: none; }
}

/* Boutons flottants de la carte */
.map-actions { position: absolute; top: 10px; left: 10px; z-index: 5; display: flex; gap: 8px; }

.map-fab {
  display: flex; align-items: center; gap: 8px; height: 40px; padding: 0 14px 0 12px; border: 0; border-radius: 14px;
  background: var(--panel); color: var(--text); font-size: 14px; font-weight: 600; box-shadow: var(--shadow); white-space: nowrap;
}

.map-fab:hover { background: color-mix(in srgb, var(--panel) 90%, var(--text)); }

.map-fab.active { background: var(--accent); color: var(--accent-ink); }

.map-menu-close { width: 36px; height: 36px; padding: 0; border: 0; border-radius: 10px; display: grid; place-items: center; background: transparent; color: var(--text); }

.map-menu-close:hover { background: var(--chip); }

.info-btn { width: 30px; height: 30px; padding: 0; border: 0; border-radius: 50%; display: grid; place-items: center; background: transparent; color: var(--muted); }

.info-btn:hover { color: var(--text); background: var(--chip); }

button.link { border: 0; background: none; padding: 0; font-size: 11px; color: var(--accent); cursor: pointer; }

button.link:hover { text-decoration: underline; }

@media (max-width: 900px) {
  .map-fab-label { display: none; }
  .map-fab { padding: 0 11px; }
}

/* toujours en place, désactivé à la première étape : la rangée de boutons ne bouge pas d'une étape à l'autre */
.tour-back { display: grid; place-items: center; width: 36px; height: 36px; padding: 0; }

.tour-back:disabled { opacity: 0.45; cursor: not-allowed; }

.tour-back:disabled:hover { border-color: var(--line); }

Tokens : --line --panel --muted --accent --accent-ink --bg --chip --text --ease-out --shadow

Utilisation

<div className="modal-actions">
  <button type="button" className="secondary" onClick={onLeave}>Revenir à l’accueil</button>
  <button type="button" className="primary" onClick={close}>Rester sur la trace</button>
</div>

Exemples

Boutons flottants de la carte (libellé masqué sous 900 px de large, comme dans cet aperçu)

// Titre : Boutons flottants de la carte (libellé masqué sous 900 px de large, comme dans cet aperçu)
import { useState } from 'react';

export default function Exemple() {
  const [flying, setFlying] = useState(false);
  return (
    // la carte : .map-actions se place en haut à gauche de son parent positionné
    <div style={{ position: 'relative', width: 420, height: 110, borderRadius: 12, background: 'var(--chip)' }}>
      <div className="map-actions">
        <button type="button" className={`map-fab${flying ? ' active' : ''}`} aria-pressed={flying} onClick={() => setFlying((f) => !f)} title={flying ? 'Mettre le survol en pause' : 'Survoler la trace en 3D'}>
          <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true">
            {flying ? <path d="M7 5h3.5v14H7zM13.5 5H17v14h-3.5z" /> : <path d="M8 5.5v13l10.5-6.5z" />}
          </svg>
          <span className="map-fab-label">{flying ? 'Pause' : 'Survol'}</span>
        </button>
        <button type="button" className="map-fab" title="Voir toute la trace">
          <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" />
          </svg>
          <span className="map-fab-label">Vue d’ensemble</span>
        </button>
      </div>
    </div>
  );
}

Boutons icône

// Titre : Boutons icône
export default function Exemple() {
  return (
    <>
      <button type="button" className="info-btn" aria-label="Comprendre les niveaux" title="Comprendre les niveaux">
        <svg viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.6" aria-hidden="true">
          <circle cx="10" cy="10" r="7.5" />
          <path d="M10 9v5" strokeLinecap="round" />
          <circle cx="10" cy="6.3" r="0.6" fill="currentColor" />
        </svg>
      </button>
      <button type="button" className="map-menu-close" aria-label="Fermer">
        <svg viewBox="0 0 20 20" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" aria-hidden="true">
          <path d="M5 5l10 10M15 5L5 15" />
        </svg>
      </button>
      <button type="button" className="tour-back" aria-label="Étape précédente" title="Étape précédente">
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M19 12H5M11 6l-6 6 6 6" />
        </svg>
      </button>
    </>
  );
}

Titre de section repliable

// Titre : Titre de section repliable
import { useState } from 'react';

export default function Exemple() {
  const [open, setOpen] = useState(true);
  return (
    <section className={`fold${open ? '' : ' closed'}`} style={{ width: 320 }}>
      <h3>
        <button type="button" className="fold-btn" aria-expanded={open} aria-controls="fold-demo" onClick={() => setOpen(!open)}>
          Points d’attention <span className="count">4</span>
          <svg className="fold-chevron" viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <path d="M5.5 8l4.5 4.5L14.5 8" />
          </svg>
        </button>
      </h3>
      <div id="fold-demo" hidden={!open}>
        <p className="muted small">Le contenu de la section.</p>
      </div>
    </section>
  );
}

Anatomie

#ÉlémentRôle
1Action principalebutton.primary : une par groupe d’actions, la plus à droite (LeaveConfirm).
2Action secondairebutton.secondary : l’autre choix franc, à gauche de l’action principale.
3Bouton par défautbutton sans classe : bordure --line, rayon 8 px, marge interne 6 × 12 px.
4Ghostbutton.ghost : visible seulement au survol, pour les actions discrètes (barre du haut, visite guidée, bandeau de modification).
5Lienbutton.link : une petite action dans une ligne de texte. Un seul emploi aujourd’hui : « estimée », qui rend au dock sa vitesse estimée (RouteDock).

États

hoverAu survol, la bordure passe à --muted ; ghost, boutons icône et titre de section prennent le fond --chip ; secondary se teinte d’accent.
focusVisibleAu clavier, un anneau --accent de 2 px, décalé de 2 px (règle commune à tout l’intérieur de .app).
disabledSeul .tour-back a un style désactivé (opacité 0,45, curseur interdit). Les autres variantes n’en ont pas.
activeBouton flottant enclenché (.map-fab.active, avec aria-pressed) ; il prend les couleurs de l’action principale.

Guidelines

  • SHOULD
    Une seule action primary par groupe d’actions ; l’autre choix en secondary, ou en ghost s’il est mineur.
  • MUST
    Nommer un bouton icône par aria-label (et title pour l’infobulle), puisqu’il n’a pas de texte visible.
  • MUST
    Poser aria-pressed sur un bouton flottant qui bascule, en même temps que la classe active.
  • SHOULD
    Écrire type="button" hors formulaire, pour qu’un bouton n’envoie jamais un formulaire par mégarde.
  • SHOULD
    Écart constaté : trois boutons icône coexistent, de tailles et de formes différentes (.info-btn rond de 30 px, .map-menu-close de 36 px sans bordure, .tour-back de 36 px avec bordure). À trancher : un seul bouton icône, ou des usages distincts à nommer.
  • SHOULD
    Écart constaté : aucun style désactivé commun ; seul .tour-back en a un. À trancher : un style button:disabled partagé.

Référence des classes

SélecteurRôleEmployé dans
buttonLe style de base de tout <button> (police héritée, bordure, rayon, marge interne) et son survol.tous les <button>
button.primaryAction principale.EditBannerLeaveConfirmShareDialogTourWelcome
button.secondaryAction secondaire, de même couleur que l’action principale.LeaveConfirmWelcome
button.ghostSans fond ni bordure ; fond --chip au survol.EditBannerSidebarTourTrackView
button.linkBouton qui se lit comme un lien.RouteDock
.info-btnBouton icône rond de 30 px, gris --muted, qui ouvre une explication.LevelInfoTechLadder
.map-menu-closeBouton icône de 36 px, sans bordure, qui ferme un menu de la carte ou la Modal.DayPlanMapContentMenuMapLayersMenuModal
.tour-backBouton icône de 36 px, avec la bordure par défaut, qui revient à l’étape précédente de la visite.Tour
.map-actionsLe conteneur des boutons flottants, en haut à gauche de la carte.TrackView
.map-fabBouton flottant de la carte, 40 px de haut, rayon 14 px, ombre --shadow.DayPlanMapContentMenuTrackView
.map-fab.activeBouton flottant enclenché (survol en cours) ; à poser avec aria-pressed.TrackView
.map-fab-labelLe libellé du bouton flottant, masqué sous 900 px de large (l’icône reste).DayPlanMapContentMenuTrackView
.fold-btnTitre de section repliable, sur toute la largeur, 40 px de haut (44 px au doigt).Sidebar
.fold-chevronLe chevron du titre de section, tourné d’un quart de tour quand la section est fermée.AlertListSidebar