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.
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ément | Rôle |
|---|---|---|
| 1 | Action principale | button.primary : une par groupe d’actions, la plus à droite (LeaveConfirm). |
| 2 | Action secondaire | button.secondary : l’autre choix franc, à gauche de l’action principale. |
| 3 | Bouton par défaut | button sans classe : bordure --line, rayon 8 px, marge interne 6 × 12 px. |
| 4 | Ghost | button.ghost : visible seulement au survol, pour les actions discrètes (barre du haut, visite guidée, bandeau de modification). |
| 5 | Lien | button.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
- SHOULDUne seule action
primarypar groupe d’actions ; l’autre choix ensecondary, ou enghosts’il est mineur. - MUSTNommer un bouton icône par
aria-label(ettitlepour l’infobulle), puisqu’il n’a pas de texte visible. - MUSTPoser
aria-pressedsur un bouton flottant qui bascule, en même temps que la classeactive. - 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-btnrond de 30 px,.map-menu-closede 36 px sans bordure,.tour-backde 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-backen a un. À trancher : un stylebutton:disabledpartagé.
Référence des classes
| Sélecteur | Rôle | Employé dans |
|---|---|---|
button | Le style de base de tout <button> (police héritée, bordure, rayon, marge interne) et son survol. | tous les <button> |
button.primary | Action principale. | EditBannerLeaveConfirmShareDialogTourWelcome |
button.secondary | Action secondaire, de même couleur que l’action principale. | LeaveConfirmWelcome |
button.ghost | Sans fond ni bordure ; fond --chip au survol. | EditBannerSidebarTourTrackView |
button.link | Bouton qui se lit comme un lien. | RouteDock |
.info-btn | Bouton icône rond de 30 px, gris --muted, qui ouvre une explication. | LevelInfoTechLadder |
.map-menu-close | Bouton icône de 36 px, sans bordure, qui ferme un menu de la carte ou la Modal. | DayPlanMapContentMenuMapLayersMenuModal |
.tour-back | Bouton icône de 36 px, avec la bordure par défaut, qui revient à l’étape précédente de la visite. | Tour |
.map-actions | Le conteneur des boutons flottants, en haut à gauche de la carte. | TrackView |
.map-fab | Bouton flottant de la carte, 40 px de haut, rayon 14 px, ombre --shadow. | DayPlanMapContentMenuTrackView |
.map-fab.active | Bouton flottant enclenché (survol en cours) ; à poser avec aria-pressed. | TrackView |
.map-fab-label | Le libellé du bouton flottant, masqué sous 900 px de large (l’icône reste). | DayPlanMapContentMenuTrackView |
.fold-btn | Titre de section repliable, sur toute la largeur, 40 px de haut (44 px au doigt). | Sidebar |
.fold-chevron | Le chevron du titre de section, tourné d’un quart de tour quand la section est fermée. | AlertListSidebar |