Créez un espace de travail Fluent UI React v9 avec de véritables tokens sémantiques, les échelles typographiques et d’espacement Web, des onglets contrôlés et une boîte de dialogue d’accès validée. Comparez les valeurs du thème, examinez l’anatomie des composants et essayez une interaction locale avant d’utiliser l’exemple complet React.
Interprétation autonome HTML/CSS des jetons web Fluent UI React v9 et de l’anatomie documentée de Field, TabList et Dialog. Il ne s’agit pas d’un environnement d’exécution natif des composants Fluent. · Référence de mise en œuvre
02
Référence de conception du système de design Microsoft Fluent 2
Références web Fluent UI React v9. Les valeurs numériques globales proviennent des sources de jetons React associées. Les valeurs de couleur ci-dessous résultent de l’association des alias clairs à brandWeb, et non des commentaires hexadécimaux obsolètes du fichier alias/lightColor.ts. Les couleurs sombres et les thèmes Teams utilisent des associations distinctes. Le tableau typographique du site indique actuellement Subtitle1 à 20/26 ; la source React fonts.ts utilise 20/28, valeur retenue par cette implémentation. Sources consultées le 3 octobre 2026 ; vérifiez-les par rapport à la version indiquée dans votre fichier de verrouillage.
Système de design Microsoft Fluent 2 et implémentation React v9
Choisissez explicitement l’implémentation web
Fluent 2 est un langage de conception multiplateforme. Ce guide fait un choix plus ciblé et directement exploitable : @fluentui/react-components, la bibliothèque convergée React v9. L’élément racine est FluentProvider, et cet exemple utilise webLightTheme ou webDarkTheme. Un thème Teams est une configuration de marque distincte ; le violet emblématique de Teams ne correspond pas à la gamme bleue web par défaut. Les ressources natives Windows, iOS et Android ne sont pas non plus interchangeables avec les packages React.
L’exemple complet ci-dessous affiche une présentation de projet et une liste d’accès locale. Il combine Title3 et Body1 pour établir la hiérarchie, TabList et Tab pour proposer deux vues, ainsi que Dialog avec Field, Input et Select pour prendre une décision d’accès. Le code ne prétend pas inviter une personne réelle. Cette distinction permet de montrer le comportement des composants sans inventer de service de partage.
FluentProvider place les variables de thème dans son sous-arbre. Une surface personnalisée stylée avec tokens.colorNeutralBackground2 suit le thème sélectionné, contrairement à une surface dont la couleur est codée en dur sur blanc. Placez le fournisseur au-dessus des composants qui ont besoin du thème. La bibliothèque stylise ses composants au moyen de son moteur de styles ; l’exemple ne vous demande pas d’importer un fichier fluent.css fictif.
Système de design Microsoft Fluent 2 : couleurs sémantiques et véritable gamme de marque web
Résolvez les alias selon la marque choisie
La source de l’alias clair associe colorBrandBackground à brand[80]. Le thème web fournit brandWeb, dont l’étape 80 est #0f6cbd. La couleur de remplissage principale obtenue dans cet exemple est donc #0f6cbd. Le fichier d’alias contient d’anciens commentaires hexadécimaux illustratifs ; les interpréter comme la palette web actuelle donnerait un résultat incorrect. C’est la relation entre les sources, et non le commentaire, qui détermine la valeur.
Utilisez colorNeutralForeground1 pour le texte du brief, colorNeutralForeground2 pour les libellés secondaires et colorNeutralBackground3 pour l’espace de travail environnant. Une action principale peut utiliser colorBrandBackground avec colorNeutralForegroundOnBrand. Les noms des rôles restent la référence lorsque le thème change. Les autorisations d’une personne sont indiquées par du texte ; une bordure bleue ou un indicateur bleu sur un onglet exprime une sélection, pas une autorisation.
En mode sombre, la surface principale devient #292929 et le texte principal #ffffff. Le remplissage de marque principal utilise le niveau 70, tandis que le premier plan des liens utilise le niveau 100. C’est pourquoi l’aperçu fait évoluer ensemble les rôles des jetons, au lieu de simplement inverser l’arrière-plan de la page. Après avoir basculé le thème, comparez la boîte de dialogue d’accès, l’onglet sélectionné et les métadonnées atténuées : ils relèvent tous de la même limite de thème.
Microsoft Fluent 2 Typographie du système de design : associer les rôles de texte aux jetons d’exécution
Utilisez des valeurs associées pour la taille et la hauteur de ligne
Body1 est le style de lecture de référence : 14px avec une hauteur de ligne de 20px. Caption1 est en 12/16, Subtitle2 en 16/22 avec une graisse semibold, et Title3 en 24/32 avec une graisse semibold. Ces associations créent une hiérarchie distincte entre titres, libellés et texte courant, sans tailles intermédiaires arbitraires. Le tableau ci-dessus présente aussi les niveaux supérieurs à titre de référence, mais un espace de travail compact n’a pas besoin d’un titre de grande taille.
La source des jetons React et le site de design présentent actuellement une différence utile à noter : fontSizeBase500 / lineHeightBase500 vaut 20/28 dans la source d’exécution, tandis que le tableau de design en ligne indique 20/26 pour Subtitle1. Cet exemple suit les valeurs de la source d’exécution. Considérez ces écarts comme un point à vérifier pour la version et l’implémentation, plutôt que de combiner silencieusement les valeurs qui conviennent à la maquette.
Utilisez la sémantique HTML en complément du style visuel prédéfini : Title3 as="h1" est un titre ; le style seul ne crée pas la structure du document. La pile de base commence par Segoe UI et comprend des polices de remplacement adaptées aux plateformes. N’intégrez pas de police propriétaire simplement pour faire correspondre une capture d’écran. L’exemple utilise la pile documentée ; le rendu exact des caractères dépend donc des polices installées sur l’appareil utilisé pour l’afficher.
Pour du texte personnalisé, typographyStyles.body1 regroupe la famille, la taille, la graisse et la hauteur de ligne. Préférez cette combinaison plutôt que de copier uniquement fontSizeBase300. La liste d’accès affiche des adresses e-mail lisibles qui passent à la ligne ; le libellé d’autorisation est mis en valeur par une graisse semibold, sans rivaliser avec le titre de la page.
Microsoft Fluent 2 Espacement, formes et densité du système de design dans un espace de travail
Les noms d’espacement React ne sont pas ceux de l’outil de design
Les recommandations générales de Fluent pour la mise en page utilisent des noms de taille globaux et un rythme de quatre unités, avec notamment 2, 6 et 10 pour les ajustements d’alignement. React expose des alias propres aux axes : spacingHorizontalS vaut 8px, M 12px, L 16px et XXL 24px, avec des noms verticaux correspondants. Ne traduisez pas size120 en spacingHorizontal120 : cet identifiant React n’existe pas.
Cette composition utilise un remplissage de panneau de 24px, un espacement de 16px entre les champs du formulaire et un espacement de 12px entre les lignes d’accès. La largeur exacte de la page et la disposition sur deux colonnes relèvent des décisions de l’application, et non de points de rupture Fluent officiels. Sur un écran étroit, le contexte d’équipe secondaire passe sous le brief, tandis que les deux libellés d’onglet restent visibles.
Microsoft Fluent 2 TabList du système de design : sélection, panneaux et comportement au clavier
La valeur sélectionnée relève de l’état de l’application
L’exemple gère l’onglet dans l’état React et transmet sa valeur à selectedValue. onTabSelect reçoit data.value, qui met à jour cet état. Chaque Tab possède une valeur stable et pointe vers son panneau via aria-controls. Chaque panneau a le rôle "tabpanel", les attributs aria-labelledby et hidden. Le composant ne fournit ni le brief du projet ni la liste d’accès ; ces éléments sont affichés explicitement.
API distingue la mise au point de la sélection : selectTabOnFocus vaut false par défaut. Dans l’exemple autonome, les touches Left/Right et Home/End déplacent le focus, tandis que Enter ou Space active l’onglet ciblé. Ce mode d’activation manuelle permet d’examiner les options d’onglet sans provoquer de changement de contenu inattendu. Gardez les libellés d’onglet courts : les recommandations officielles de mise en page indiquent que les onglets horizontaux ne passent pas automatiquement à la ligne et ne défilent pas.
Choisissez un autre modèle lorsque les catégories se multiplient
Brief et People sont deux vues étroitement liées d’un même espace de travail : les onglets conviennent donc. Une liste de tous les projets de l’organisation relèverait de la navigation, et non d’une autre rangée d’onglets. Si les catégories ne tiennent plus sur un écran étroit, concevez délibérément un mécanisme de débordement ou un autre modèle de sélection ; réduire les libellés jusqu’à les rendre illisibles n’est pas une stratégie adaptative.
Microsoft Fluent 2 Field du système de design : libellés accessibles et validation pertinente
Un champ est une composition, pas un simple champ de saisie stylisé
Field associe un emplacement pour le libellé, son contrôle enfant, une indication facultative et un message de validation. Le contrat documenté du contrôle comprend id, aria-labelledby, aria-describedby, aria-invalid et aria-required. Les champs de saisie Fluent peuvent être des enfants directs ; les contrôles personnalisés peuvent utiliser les propriétés transmises par la fonction de rendu. Voilà pourquoi il est plus utile d’encapsuler Input pour les e-mails dans Field que de placer du texte sans lien au-dessus d’un champ de saisie.
Dans l’exemple, validationState ne passe à error qu’après l’envoi d’une valeur locale non valide. L’application vérifie que la valeur a la forme d’une adresse e-mail et qu’elle ne figure pas déjà dans la liste en mémoire. Elle conserve la valeur saisie et propose une correction avec validationMessage. Field affiche ce résultat et l’associe au champ ; il ne détermine pas si l’adresse existe, n’envoie pas d’invitation et n’accorde pas l’accès.
Le composant Select des autorisations propose Can view et Can edit, tandis que le champ e-mail possède son propre état d’erreur. Évitez de rendre toute la boîte de dialogue rouge parce qu’une adresse est incomplète. required est un indicateur accessible, et non un substitut à l’autorisation ou à la validation côté serveur. Le formulaire d’exemple utilise noValidate afin de présenter systématiquement son message de validation explicite ; un service en production doit valider la requête à nouveau.
Adresse vide ou mal formée : laissez la boîte de dialogue ouverte et indiquez le format accepté.
Adresse déjà présente dans la liste d’accès locale : affichez un message spécifique aux doublons, sans ajouter de ligne supplémentaire.
Nouvelle adresse valide : ajoutez localement l’autorisation choisie, fermez la boîte de dialogue et indiquez honnêtement que le résultat est uniquement local.
Microsoft Fluent 2 Système de conception Dialog : un formulaire dans la structure prise en charge
Placez le bouton d’envoi dans le formulaire
La composition comprend Dialog, DialogTrigger, DialogSurface, un élément de formulaire et DialogBody, qui contient DialogTitle, DialogContent et DialogActions. Le formulaire englobe le corps afin que le bouton d’envoi du pied de page participe à l’envoi. Cancel est de type type="button" et se trouve dans un DialogTrigger qui ferme la boîte de dialogue ; sinon, le type de bouton par défaut pourrait déclencher l’envoi du formulaire par inadvertance.
open et onOpenChange contrôlent la visibilité. disableButtonEnhancement évite que le déclencheur n’améliore inutilement un bouton Fluent qui fournit déjà le comportement d’un bouton. Le gestionnaire de réussite ne ferme la boîte de dialogue qu’une fois la validation locale réussie. En cas d’échec de validation, l’adresse e-mail et l’autorisation restent disponibles pour correction.
Choisissez le comportement modal selon la décision à prendre
Cette décision d’accès simple utilise le type modal par défaut. Une boîte de dialogue non modale permettrait de poursuivre le travail en arrière-plan, tandis qu’une boîte de dialogue d’alerte est destinée aux décisions urgentes et impose des règles de fermeture plus strictes. Un formulaire d’accès classique n’a pas besoin de la sémantique d’une alerte. Les recommandations officielles préconisent de placer le focus dans la boîte de dialogue et de le ramener sur son déclencheur à la fermeture ; l’interprétation autonome utilise l’élément natif dialog et rétablit explicitement le focus sur son déclencheur.
Microsoft Fluent 2 Système de conception Personnaliser le style sans nuire au thème
Utilisez makeStyles et des variables sémantiques
Le code complet utilise makeStyles avec des tokens pour les surfaces, les espacements et les rayons. Le panneau personnalisé reste ainsi cohérent avec le fournisseur de thème. Un nom de classe généré est un détail d’implémentation : ne le copiez pas depuis DevTools dans une feuille de style. Pour tester des thèmes, modifiez le thème du fournisseur et examinez ensemble les contrôles natifs et les zones personnalisées.
L’échelle de durées publiée va de 50ms à 500ms. L’exemple HTML utilise durationFast, soit 150ms, pour de simples changements au survol et supprime ces transitions lorsque prefers-reduced-motion est activé. Il s’agit d’un choix d’application utilisant un token disponible, et non d’une affirmation selon laquelle chaque interaction Fluent doit durer 150ms. Le contenu et les messages d’erreur restent compréhensibles même lorsque les animations sont désactivées.
Microsoft Fluent 2 Système de conception Installer et vérifier la composition complète
Un point d’entrée exécutable, pas un plan de tâches
La configuration cible une application React existante, avec un élément dont l’id est root. Installez la suite v9, puis placez le point d’entrée JSX dans l’application. Les imports, le fournisseur, l’arborescence des composants, les gestionnaires, l’état et le rendu à la racine sont fournis ensemble. Aucun service de partage externe API n’est appelé. Si votre projet de démarrage effectue déjà le rendu de App, conservez son point d’entrée racine et exportez App au lieu de créer une seconde racine.
Consignez la version résolue du package dans le fichier de verrouillage. Les sources liées décrivent l’implémentation v9 de React qui a été examinée, tandis que les URL master peuvent évoluer. Avant une mise à niveau, comparez le thème exporté ainsi que le comportement exact de Field, TabList et Dialog utilisé ici. Cette configuration évite délibérément de combiner des composants v8 @fluentui/react avec des exemples v9.
Installez @fluentui/react-components@9 dans le projet React existant et conservez le fichier de verrouillage généré.
Effectuez le rendu du point d’entrée fourni sous un seul FluentProvider, avec webLightTheme comme thème initial.
Ouvrez la boîte de dialogue, envoyez une adresse invalide, corrigez-la et vérifiez que la personne ajoutée n’apparaît que dans l’état local.
Répétez avec webDarkTheme et la navigation au clavier ; vérifiez l’association du panneau, l’annonce de l’erreur et le rétablissement du focus.
Éléments à connecter dans un véritable produit de partage
Ne remplacez l’ajout local qu’après avoir défini le contrat du service : acteur authentifié, espace de travail cible, valeurs d’autorisation permises, résultat de l’invitation et gestion des doublons. Si le service rejette une requête, laissez la boîte de dialogue ouverte et affichez le résultat confirmé au lieu d’annoncer l’accès de façon optimiste. Ces règles ne relèvent pas de Fluent, mais le code indique clairement le point de transfert au lieu de le masquer derrière un bouton d’envoi générique.
Microsoft Fluent 2 Système de conception Exemples de composants
Exemple interactif autonome complet
Enregistrez le document complet sous le nom index.html et ouvrez-le dans un navigateur. Interprétation autonome HTML/CSS des tokens Web Fluent UI React v9 et de la structure documentée de Field, TabList et Dialog. Il ne s’agit pas d’un environnement d’exécution natif des composants Fluent.
html
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Fluent web workspace study</title><style>
:root{color-scheme:light;--colorNeutralForeground1:#242424;--colorNeutralForeground2:#424242;--colorNeutralForeground3:#616161;--colorNeutralBackground1:#fff;--colorNeutralBackground2:#fafafa;--colorNeutralBackground3:#f5f5f5;--colorNeutralStroke1:#d1d1d1;--colorNeutralStroke2:#e0e0e0;--colorBrandBackground:#0f6cbd;--colorBrandBackgroundHover:#115ea3;--colorBrandBackgroundPressed:#0c3b5e;--colorBrandBackground2:#ebf3fc;--colorBrandForegroundLink:#115ea3;--colorNeutralForegroundOnBrand:#fff;--borderRadiusMedium:4px;--borderRadiusXLarge:8px;--spacingHorizontalS:8px;--spacingHorizontalM:12px;--spacingHorizontalL:16px;--spacingHorizontalXXL:24px;--durationFast:150ms}
[data-theme=dark]{color-scheme:dark;--colorNeutralForeground1:#fff;--colorNeutralForeground2:#d6d6d6;--colorNeutralForeground3:#adadad;--colorNeutralBackground1:#292929;--colorNeutralBackground2:#1f1f1f;--colorNeutralBackground3:#141414;--colorNeutralStroke1:#666;--colorNeutralStroke2:#525252;--colorBrandBackground:#115ea3;--colorBrandBackgroundHover:#0f6cbd;--colorBrandBackground2:#082338;--colorBrandForegroundLink:#479ef5}
*{box-sizing:border-box}body{margin:0;background:var(--colorNeutralBackground3);color:var(--colorNeutralForeground1);font-family:'Segoe UI','Segoe UI Web (West European)',-apple-system,BlinkMacSystemFont,Roboto,'Helvetica Neue',sans-serif;font-size:14px;line-height:20px}button,input,select{font:inherit}button{cursor:pointer;min-height:32px;padding:5px 12px;border:1px solid var(--colorNeutralStroke1);border-radius:var(--borderRadiusMedium);background:var(--colorNeutralBackground1);color:var(--colorNeutralForeground1);font-weight:600;transition:background var(--durationFast)}button:hover{background:var(--colorNeutralBackground3)}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--colorBrandForegroundLink);outline-offset:3px}.primary{background:var(--colorBrandBackground);color:var(--colorNeutralForegroundOnBrand);border-color:transparent}.primary:hover{background:var(--colorBrandBackgroundHover)}.primary:active{background:var(--colorBrandBackgroundPressed)}.topbar{display:flex;justify-content:space-between;align-items:center;padding:12px 24px;background:var(--colorNeutralBackground1);border-bottom:1px solid var(--colorNeutralStroke2);gap:16px}.brand{display:flex;align-items:center;gap:12px;font-weight:600}.mark{display:grid;grid-template-columns:repeat(2,7px);gap:3px}.mark i{width:7px;height:7px;background:var(--colorBrandBackground)}.top-note{font-size:12px;color:var(--colorNeutralForeground3)}.page{max-width:1100px;margin:auto;padding:24px}.heading{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:16px}.eyebrow{font-size:12px;line-height:16px;color:var(--colorNeutralForeground3);margin-bottom:4px}h1{font-size:24px;line-height:32px;font-weight:600;margin:0 0 4px}h2{font-size:20px;line-height:28px;font-weight:600;margin:0 0 16px}h3{font-size:16px;line-height:22px;font-weight:600;margin:20px 0 8px}p{margin:0 0 12px}.muted{color:var(--colorNeutralForeground3)}.tabs{display:flex;gap:16px;border-bottom:1px solid var(--colorNeutralStroke2);margin-bottom:24px}.tab{position:relative;background:none;border:0;border-radius:0;font-weight:400;padding:12px 4px;color:var(--colorNeutralForeground2)}.tab[aria-selected=true]{font-weight:600;color:var(--colorBrandForegroundLink)}.tab[aria-selected=true]:after{content:'';position:absolute;bottom:0;left:4px;right:4px;height:3px;background:var(--colorBrandBackground);border-radius:3px}.layout{display:grid;grid-template-columns:minmax(0,1fr) 240px;gap:24px}.document,.aside,.people{background:var(--colorNeutralBackground1);border:1px solid var(--colorNeutralStroke2);border-radius:var(--borderRadiusXLarge);padding:24px}.document{max-width:720px}.docmeta{display:flex;gap:8px;align-items:center;color:var(--colorNeutralForeground3);font-size:12px;line-height:16px;margin-bottom:24px}.avatar{flex:none;display:grid;place-items:center;background:var(--colorBrandBackground2);color:var(--colorBrandForegroundLink);font-weight:600;width:32px;height:32px;border-radius:50%}.quote{padding:12px 16px;background:var(--colorNeutralBackground2);border-left:3px solid var(--colorBrandBackground);margin:16px 0}.checklist{list-style:none;padding:0;margin:12px 0}.checklist li{display:flex;gap:8px;margin:8px 0}.checklist input{accent-color:var(--colorBrandBackground);width:16px;height:16px;margin-top:2px}.aside{padding:20px;height:fit-content}.aside h2{font-size:16px;line-height:22px;margin-bottom:16px}.person{display:flex;gap:8px;align-items:center;margin-bottom:16px}.person p{margin:0}.small{font-size:12px;line-height:16px}.aside hr{border:0;border-top:1px solid var(--colorNeutralStroke2);margin:20px 0}.tag{display:inline-block;border:1px solid var(--colorNeutralStroke1);padding:2px 8px;border-radius:4px;font-size:12px;color:var(--colorNeutralForeground2)}.people-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;gap:12px}.people-head h2{margin:0}.people ul{list-style:none;padding:0;margin:0}.member{display:flex;align-items:center;gap:12px;padding:16px 0;border-top:1px solid var(--colorNeutralStroke2)}.member-info{flex:1;min-width:0;overflow-wrap:anywhere}.member-info p{margin:0}.member .tag{flex:none}.caption{margin:20px 0 0;color:var(--colorNeutralForeground3);font-size:12px;line-height:16px}.status{margin-top:16px;overflow-wrap:anywhere;color:var(--colorNeutralForeground2)}.status:empty{display:none}[hidden]{display:none!important}dialog{width:min(520px,calc(100vw - 32px));padding:24px;border:1px solid var(--colorNeutralStroke1);border-radius:8px;background:var(--colorNeutralBackground1);color:var(--colorNeutralForeground1);max-height:calc(100dvh - 32px);overflow:auto}dialog::backdrop{background:rgba(0,0,0,.4)}dialog h2{font-size:20px;line-height:28px;margin-bottom:12px}.field{margin:20px 0}.field label{display:block;font-weight:600;margin-bottom:8px}.required{color:#b10e1e}.field input,.field select{width:100%;min-height:36px;background:var(--colorNeutralBackground1);color:var(--colorNeutralForeground1);padding:7px 12px;border:1px solid var(--colorNeutralStroke1);border-bottom-color:var(--colorNeutralForeground3);border-radius:4px}.field input:focus{border-bottom:2px solid var(--colorBrandBackground);padding-bottom:6px}.hint{font-size:12px;line-height:16px;color:var(--colorNeutralForeground3);margin-top:8px}.error{font-size:12px;line-height:16px;color:#b10e1e;margin-top:8px}.field input[aria-invalid=true]{border-color:#b10e1e}.actions{display:flex;justify-content:flex-end;gap:8px;margin-top:24px}[data-theme=dark] .error,[data-theme=dark] .required{color:#ff99a4}footer{padding:0 24px 20px;text-align:center;font-size:12px;color:var(--colorNeutralForeground3)}@media(max-width:700px){.page{padding:16px}.topbar{padding:12px 16px}.top-note{display:none}.layout{grid-template-columns:1fr}.document,.people{padding:20px}.aside{display:grid;grid-template-columns:1fr 1fr;gap:8px 16px}.aside h2,.aside hr{grid-column:1/-1}.aside hr{margin:0}.heading{align-items:flex-start}.heading .primary{flex:none}.member{flex-wrap:wrap}.member .tag{margin-left:44px}.topbar button,.heading button{min-height:40px}}@media(max-width:420px){h1{font-size:20px;line-height:28px}.heading{flex-direction:column;gap:12px}.aside{display:block}.member .tag{margin-left:0}}@media(prefers-reduced-motion:reduce){*{transition:none!important}}
</style></head><body>
<header class="topbar"><div class="brand"><span class="mark" aria-hidden="true"><i></i><i></i><i></i><i></i></span>Northstar workspace <span class="top-note">/ Product team</span></div><button id="theme" aria-pressed="false">Dark theme</button></header>
<main class="page"><div class="heading"><div><div class="eyebrow">Team workspace · October launch</div><h1>Launch workspace</h1><p class="muted">One brief. A clear decision. The right people.</p></div><button class="primary" id="share">Add a person</button></div>
<div class="tabs" role="tablist" aria-label="Workspace views"><button class="tab" id="brief-tab" role="tab" aria-controls="brief-panel" aria-selected="true" tabindex="0">Brief</button><button class="tab" id="people-tab" role="tab" aria-controls="people-panel" aria-selected="false" tabindex="-1">People <span id="count">2</span></button></div>
<section id="brief-panel" role="tabpanel" aria-labelledby="brief-tab" tabindex="0"><div class="layout"><article class="document"><div class="docmeta"><span class="avatar" aria-hidden="true">AC</span><span>Alex Chen · Product design<br>Updated today at 09:30</span></div><h2>A more welcoming first project</h2><p>Help a new team move from an empty workspace to its first useful result. This review focuses on the welcome screen and the first-project setup.</p><h3>The decision</h3><div class="quote">Should a sample project be created automatically, or offered as an option?</div><p>We want a clear starting point without filling the workspace with content someone did not choose. Review both paths before the team walkthrough.</p><h3>Review checklist</h3><ul class="checklist"><li><input id="c1" type="checkbox" checked><label for="c1">Welcome copy explains the next step</label></li><li><input id="c2" type="checkbox"><label for="c2">Empty workspace offers a sample project</label></li><li><input id="c3" type="checkbox"><label for="c3">Keyboard path reaches the first project action</label></li></ul><p class="small muted">Checklist changes are local to this example.</p></article><aside class="aside"><h2>Working together</h2><div class="person"><span class="avatar" aria-hidden="true">AC</span><div><p>Alex Chen</p><span class="small muted">Workspace owner</span></div></div><div class="person"><span class="avatar" aria-hidden="true">MP</span><div><p>Maya Patel</p><span class="small muted">Can edit</span></div></div><hr><div><h3>Next review</h3><p>Thursday, 10:00<br><span class="muted small">Team walkthrough · UTC</span></p></div><div><h3>Access</h3><span class="tag">Specific people</span><p class="small muted" style="margin-top:8px">Adding a person here updates only this local preview.</p></div></aside></div></section>
<section id="people-panel" role="tabpanel" aria-labelledby="people-tab" tabindex="0" hidden><div class="people"><div class="people-head"><h2>People with access</h2><span class="tag">Local example</span></div><p class="muted">The roles below are fictional. No invitation or real access change is made.</p><ul id="members"><li class="member"><span class="avatar" aria-hidden="true">AC</span><div class="member-info"><p>Alex Chen</p><span class="small muted">[email protected]</span></div><span class="tag">Owner</span></li><li class="member"><span class="avatar" aria-hidden="true">MP</span><div class="member-info"><p>Maya Patel</p><span class="small muted">[email protected]</span></div><span class="tag">Can edit</span></li></ul></div></section><p class="status" id="status" role="status"></p><p class="caption">Fluent UI React v9 web-token study · HTML/CSS interpretation, not native library components. Try tabs, theme, checkboxes and the access form.</p></main>
<dialog id="access" aria-labelledby="dialog-title"><form id="access-form" novalidate><h2 id="dialog-title">Add a person to Launch workspace</h2><p class="muted">Choose an address and permission. This demonstration updates the local list only.</p><div class="field"><label for="email">Email address <span class="required" aria-hidden="true">*</span></label><input id="email" type="email" autocomplete="email" aria-required="true" aria-describedby="email-hint" autofocus><div class="hint" id="email-hint">For example, [email protected]</div><div class="error" id="error" role="alert" hidden></div></div><div class="field"><label for="permission">Permission</label><select id="permission" aria-describedby="permission-hint"><option>Can view</option><option>Can edit</option></select><div class="hint" id="permission-hint">Viewers can read; editors can change the brief in a real application</div></div><div class="actions"><button type="button" id="cancel">Cancel</button><button class="primary" type="submit">Add locally</button></div></form></dialog>
<script>
const $=id=>document.getElementById(id),tabs=[...document.querySelectorAll('[role=tab]')],emails=new Set(['[email protected]','[email protected]']);
function selectTab(tab){tabs.forEach(t=>{const chosen=t===tab;t.setAttribute('aria-selected',String(chosen));t.tabIndex=chosen?0:-1;$(t.getAttribute('aria-controls')).hidden=!chosen;});}
tabs.forEach((tab,index)=>{tab.addEventListener('click',()=>selectTab(tab));tab.addEventListener('keydown',event=>{let next;if(event.key==='ArrowRight')next=(index+1)%tabs.length;if(event.key==='ArrowLeft')next=(index+tabs.length-1)%tabs.length;if(event.key==='Home')next=0;if(event.key==='End')next=tabs.length-1;if(next!==undefined){event.preventDefault();tabs.forEach(t=>t.tabIndex=-1);tabs[next].tabIndex=0;tabs[next].focus();}});});
$('theme').onclick=()=>{const dark=document.documentElement.dataset.theme!=='dark';document.documentElement.dataset.theme=dark?'dark':'light';$('theme').textContent=dark?'Light theme':'Dark theme';$('theme').setAttribute('aria-pressed',String(dark));};
const dialog=$('access'),email=$('email'),error=$('error');function clearError(){error.hidden=true;error.textContent='';email.removeAttribute('aria-invalid');email.setAttribute('aria-describedby','email-hint');}function showError(message){error.textContent=message;error.hidden=false;email.setAttribute('aria-invalid','true');email.setAttribute('aria-describedby','email-hint error');email.focus();}
$('share').onclick=()=>{clearError();dialog.showModal();email.focus();};$('cancel').onclick=()=>dialog.close();dialog.addEventListener('close',()=>$('share').focus());dialog.addEventListener('click',event=>{if(event.target===dialog){const r=dialog.getBoundingClientRect();if(event.clientX<r.left||event.clientX>r.right||event.clientY<r.top||event.clientY>r.bottom)dialog.close();}});email.addEventListener('input',clearError);
$('access-form').addEventListener('submit',event=>{event.preventDefault();const value=email.value.trim().toLowerCase();if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)){showError('Enter an email address such as [email protected]');return;}if(emails.has(value)){showError('This address already appears in the local access list');return;}const permission=$('permission').value;emails.add(value);const item=document.createElement('li');item.className='member';const avatar=document.createElement('span');avatar.className='avatar';avatar.setAttribute('aria-hidden','true');avatar.textContent=value.slice(0,2).toUpperCase();const info=document.createElement('div');info.className='member-info';const name=document.createElement('p');name.textContent=value;const sub=document.createElement('span');sub.className='small muted';sub.textContent='Added in this local example';info.append(name,sub);const role=document.createElement('span');role.className='tag';role.textContent=permission;item.append(avatar,info,role);$('members').append(item);$('count').textContent=emails.size;$('status').textContent='Added '+value+' as '+permission.toLowerCase()+' locally. No invitation was sent.';email.value='';clearError();selectTab($('people-tab'));dialog.close();});
</script></body></html>
La boîte de dialogue d’accès et les onglets de ce document utilisent des contrôles natifs HTML ; exécutez séparément le point d’entrée React v9 pour évaluer le comportement de FluentProvider, Field et Dialog.
Entrée complète de l’implémentation native
L’implémentation complète présentée dans la configuration, réunie ici avec les explications ciblées sur API. Suivez les prérequis documentés concernant les packages et la compilation dans la section de configuration.
jsx
// Terminal: npm install @fluentui/react-components@9
// Existing React application; index.html must contain <div id="root"></div>.
// Entry file: src/main.jsx
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import {
FluentProvider, webLightTheme, webDarkTheme, makeStyles, tokens,
Button, Body1, Title3, TabList, Tab, Field, Input, Select,
Dialog, DialogTrigger, DialogSurface, DialogBody, DialogTitle,
DialogContent, DialogActions
} from '@fluentui/react-components';
const useStyles = makeStyles({
page: { maxWidth: '880px', margin: '0 auto', padding: tokens.spacingHorizontalXXL },
header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: tokens.spacingHorizontalL },
panel: { padding: tokens.spacingHorizontalXXL, marginTop: tokens.spacingVerticalL,
backgroundColor: tokens.colorNeutralBackground2, borderRadius: tokens.borderRadiusXLarge },
form: { display: 'grid', gap: tokens.spacingVerticalL },
row: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: tokens.spacingHorizontalM,
paddingTop: tokens.spacingVerticalM, paddingBottom: tokens.spacingVerticalM },
});
function Workspace({ dark, setDark }) {
const styles = useStyles();
const [tab, setTab] = React.useState('brief');
const [open, setOpen] = React.useState(false);
const [email, setEmail] = React.useState('');
const [permission, setPermission] = React.useState('Can view');
const [error, setError] = React.useState('');
const [notice, setNotice] = React.useState('');
const [people, setPeople] = React.useState([{ email: '[email protected]', permission: 'Owner' }]);
function submit(event) {
event.preventDefault();
const value = email.trim().toLowerCase();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
setError('Enter an email address such as [email protected]'); return;
}
if (people.some(person => person.email === value)) {
setError('This address already appears in the local access list'); return;
}
setPeople(previous => [...previous, { email: value, permission }]);
setNotice(`Added ${value} as ${permission.toLowerCase()} in this local example. No invitation was sent.`);
setEmail(''); setError(''); setOpen(false); setTab('people');
}
return <main className={styles.page}>
<div className={styles.header}>
<Title3 as="h1">Launch workspace</Title3>
<Button onClick={() => setDark(!dark)}>{dark ? 'Light theme' : 'Dark theme'}</Button>
</div>
<Body1>Project brief and access settings · local demonstration</Body1>
<TabList aria-label="Workspace views" selectedValue={tab}
onTabSelect={(_, data) => setTab(data.value)}>
<Tab id="brief-tab" value="brief" aria-controls="brief-panel">Brief</Tab>
<Tab id="people-tab" value="people" aria-controls="people-panel">People</Tab>
</TabList>
<section id="brief-panel" role="tabpanel" aria-labelledby="brief-tab"
hidden={tab !== 'brief'} className={styles.panel} tabIndex={0}>
<h2>Launch a clearer onboarding flow</h2>
<p>Review the welcome screen and first-project setup before the team walkthrough.</p>
<p>Decision needed: should the sample project be created automatically or offered as an option?</p>
</section>
<section id="people-panel" role="tabpanel" aria-labelledby="people-tab"
hidden={tab !== 'people'} className={styles.panel} tabIndex={0}>
<h2>People with access</h2>
<ul>{people.map(person => <li key={person.email} className={styles.row}>
<span>{person.email}</span><strong>{person.permission}</strong>
</li>)}</ul>
</section>
<Dialog open={open} onOpenChange={(_, data) => setOpen(data.open)}>
<DialogTrigger disableButtonEnhancement><Button appearance="primary">Add a person</Button></DialogTrigger>
<DialogSurface>
<form noValidate onSubmit={submit}>
<DialogBody>
<DialogTitle>Add a person to Launch workspace</DialogTitle>
<DialogContent className={styles.form}>
<p>This example updates local state only. It does not send email or change real access.</p>
<Field label="Email address" required validationState={error ? 'error' : 'none'}
validationMessage={error || undefined}>
<Input type="email" value={email} onChange={(_, data) => { setEmail(data.value); setError(''); }} />
</Field>
<Field label="Permission" hint="Viewers can read; editors can change the brief in a real application">
<Select value={permission} onChange={(_, data) => setPermission(data.value)}>
<option>Can view</option><option>Can edit</option>
</Select>
</Field>
</DialogContent>
<DialogActions>
<DialogTrigger disableButtonEnhancement><Button type="button">Cancel</Button></DialogTrigger>
<Button type="submit" appearance="primary">Add locally</Button>
</DialogActions>
</DialogBody>
</form>
</DialogSurface>
</Dialog>
<p role="status">{notice}</p>
</main>;
}
function App() {
const [dark, setDark] = React.useState(false);
return <FluentProvider theme={dark ? webDarkTheme : webLightTheme}>
<Workspace dark={dark} setDark={setDark} />
</FluentProvider>;
}
createRoot(document.getElementById('root')).render(<App />);
Composition complète des composants React v9 pour une application React existante. L’aperçu autonome est une interprétation HTML/CSS distincte. Ce code effectue uniquement une validation locale et des mises à jour d’état ; aucun e-mail n’est envoyé et aucune autorisation réelle n’est modifiée. Les API des sources ont été vérifiées ; la compilation native du package n’est pas garantie tant qu’elle n’a pas été testée dans le projet cible.
Field + Input + Select : un formulaire d’accès avec libellés
Ces fragments utilisent les variables d’état et les imports de la configuration complète ci-dessous. Field gère l’affichage du libellé, de l’aide et de la validation ; l’application gère la règle de validation de l’adresse e-mail et la détection des doublons.
required fournit une indication accessible de champ obligatoire ; il ne met pas en œuvre la règle de validation de l’application.
L’état de validation par défaut passe à error lorsqu’un validationMessage existe. Définissez explicitement none si aucune erreur ne doit s’afficher.
Input onChange fournit la chaîne actuelle dans data.value ; ne confondez pas son gestionnaire avec un événement DOM standard pour un champ de saisie.
TabList contrôlée avec panneaux explicitement associés
TabList gère la sélection et le comportement des onglets. Affichez vous-même les panneaux de contenu et associez leurs identifiants et noms accessibles à chaque Tab.
selectedValue doit être défini sur TabList ; chaque Tab doit avoir une valeur distincte.
selectTabOnFocus vaut false par défaut. Le déplacement du focus avec les flèches et l’activation sont distincts, sauf si ce comportement est modifié délibérément.
Les onglets horizontaux Fluent ne passent pas à la ligne et ne défilent pas automatiquement ; l’exemple utilise deux libellés courts pour qu’ils restent visibles.
04
Configuration de l’agent d’IA du système de design Microsoft Fluent 2
Point de départ de l’implémentation
Composition complète des composants React v9 pour une application React existante. L’aperçu autonome est une interprétation HTML/CSS distincte. Ce code effectue uniquement une validation locale et des mises à jour d’état ; aucun e-mail n’est envoyé et aucune autorisation réelle n’est modifiée. Les API des sources ont été vérifiées ; la compilation native du package n’est pas garantie tant qu’elle n’a pas été testée dans le projet cible.
jsx
// Terminal: npm install @fluentui/react-components@9
// Existing React application; index.html must contain <div id="root"></div>.
// Entry file: src/main.jsx
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import {
FluentProvider, webLightTheme, webDarkTheme, makeStyles, tokens,
Button, Body1, Title3, TabList, Tab, Field, Input, Select,
Dialog, DialogTrigger, DialogSurface, DialogBody, DialogTitle,
DialogContent, DialogActions
} from '@fluentui/react-components';
const useStyles = makeStyles({
page: { maxWidth: '880px', margin: '0 auto', padding: tokens.spacingHorizontalXXL },
header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: tokens.spacingHorizontalL },
panel: { padding: tokens.spacingHorizontalXXL, marginTop: tokens.spacingVerticalL,
backgroundColor: tokens.colorNeutralBackground2, borderRadius: tokens.borderRadiusXLarge },
form: { display: 'grid', gap: tokens.spacingVerticalL },
row: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: tokens.spacingHorizontalM,
paddingTop: tokens.spacingVerticalM, paddingBottom: tokens.spacingVerticalM },
});
function Workspace({ dark, setDark }) {
const styles = useStyles();
const [tab, setTab] = React.useState('brief');
const [open, setOpen] = React.useState(false);
const [email, setEmail] = React.useState('');
const [permission, setPermission] = React.useState('Can view');
const [error, setError] = React.useState('');
const [notice, setNotice] = React.useState('');
const [people, setPeople] = React.useState([{ email: '[email protected]', permission: 'Owner' }]);
function submit(event) {
event.preventDefault();
const value = email.trim().toLowerCase();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
setError('Enter an email address such as [email protected]'); return;
}
if (people.some(person => person.email === value)) {
setError('This address already appears in the local access list'); return;
}
setPeople(previous => [...previous, { email: value, permission }]);
setNotice(`Added ${value} as ${permission.toLowerCase()} in this local example. No invitation was sent.`);
setEmail(''); setError(''); setOpen(false); setTab('people');
}
return <main className={styles.page}>
<div className={styles.header}>
<Title3 as="h1">Launch workspace</Title3>
<Button onClick={() => setDark(!dark)}>{dark ? 'Light theme' : 'Dark theme'}</Button>
</div>
<Body1>Project brief and access settings · local demonstration</Body1>
<TabList aria-label="Workspace views" selectedValue={tab}
onTabSelect={(_, data) => setTab(data.value)}>
<Tab id="brief-tab" value="brief" aria-controls="brief-panel">Brief</Tab>
<Tab id="people-tab" value="people" aria-controls="people-panel">People</Tab>
</TabList>
<section id="brief-panel" role="tabpanel" aria-labelledby="brief-tab"
hidden={tab !== 'brief'} className={styles.panel} tabIndex={0}>
<h2>Launch a clearer onboarding flow</h2>
<p>Review the welcome screen and first-project setup before the team walkthrough.</p>
<p>Decision needed: should the sample project be created automatically or offered as an option?</p>
</section>
<section id="people-panel" role="tabpanel" aria-labelledby="people-tab"
hidden={tab !== 'people'} className={styles.panel} tabIndex={0}>
<h2>People with access</h2>
<ul>{people.map(person => <li key={person.email} className={styles.row}>
<span>{person.email}</span><strong>{person.permission}</strong>
</li>)}</ul>
</section>
<Dialog open={open} onOpenChange={(_, data) => setOpen(data.open)}>
<DialogTrigger disableButtonEnhancement><Button appearance="primary">Add a person</Button></DialogTrigger>
<DialogSurface>
<form noValidate onSubmit={submit}>
<DialogBody>
<DialogTitle>Add a person to Launch workspace</DialogTitle>
<DialogContent className={styles.form}>
<p>This example updates local state only. It does not send email or change real access.</p>
<Field label="Email address" required validationState={error ? 'error' : 'none'}
validationMessage={error || undefined}>
<Input type="email" value={email} onChange={(_, data) => { setEmail(data.value); setError(''); }} />
</Field>
<Field label="Permission" hint="Viewers can read; editors can change the brief in a real application">
<Select value={permission} onChange={(_, data) => setPermission(data.value)}>
<option>Can view</option><option>Can edit</option>
</Select>
</Field>
</DialogContent>
<DialogActions>
<DialogTrigger disableButtonEnhancement><Button type="button">Cancel</Button></DialogTrigger>
<Button type="submit" appearance="primary">Add locally</Button>
</DialogActions>
</DialogBody>
</form>
</DialogSurface>
</Dialog>
<p role="status">{notice}</p>
</main>;
}
function App() {
const [dark, setDark] = React.useState(false);
return <FluentProvider theme={dark ? webDarkTheme : webLightTheme}>
<Workspace dark={dark} setDark={setDark} />
</FluentProvider>;
}
createRoot(document.getElementById('root')).render(<App />);
Un prompt pour votre projet
Prompt pour l’agent IA
Utilisez Fluent UI React v9 depuis @fluentui/react-components. Affichez FluentProvider avec webLightTheme et une bascule vers webDarkTheme. Composez le résumé avec Title3 et Body1, des vues TabList/Tab contrôlées avec panneaux explicites, et une boîte de dialogue d’accès contenant Field/Input/Select et un véritable formulaire. Utilisez des jetons pour les surfaces personnalisées et l’espacement. Suivez l’exemple de composant fourni ainsi que les paires de types d’exécution, notamment Subtitle1 en 20/28. Préservez les saisies non valides, rejetez les adresses locales en double, rétablissez le focus sur la boîte de dialogue et décrivez honnêtement les ajouts locaux. Connectez un service de partage uniquement si ses autorisations et le contrat de résultat sont fournis.
05
Sources et périmètre du système de design Microsoft Fluent 2
Une étude d’application OpenDesign de Fluent UI React v9, et non un produit Microsoft ni un service de partage en ligne. L’aperçu autonome est une interprétation HTML/CSS des jetons web vérifiés et de l’anatomie des composants ; le code React ci-dessous utilise les composants Fluent natifs. Les valeurs de référence reflètent l’instantané de la source liée, vérifié le 3 octobre 2026, et ne garantissent pas que chaque version, plateforme ou marque utilise des valeurs identiques.