Crea un espacio de trabajo con Fluent UI React v9 que incluya tokens semánticos reales, las escalas web de tipografía y espaciado, pestañas controladas y un diálogo de acceso con validación. Compara los valores del tema, inspecciona la anatomía de los componentes y prueba una interacción local antes de usar el ejemplo completo de React.
Interpretación independiente de HTML/CSS de los tokens web de Fluent UI React v9 y de la estructura documentada de Field, TabList y Dialog. No es un entorno de ejecución nativo de componentes Fluent. · Referencia de implementación
02
Microsoft Fluent 2 Referencia de diseño del sistema de diseño
Referencias web de Fluent UI React v9. Los valores numéricos globales proceden de las fuentes de tokens enlazadas de React. Los valores de color de abajo corresponden al mapeo de alias claro con brandWeb, no a los comentarios hexadecimales obsoletos del ejemplo en alias/lightColor.ts. Los colores oscuros y los temas de Teams tienen mapeos independientes. Actualmente, la tabla tipográfica del sitio indica Subtitle1 a 20/26; la fuente React fonts.ts usa 20/28, valor que sigue esta implementación. Fuentes consultadas el 3 de octubre de 2026; comprueba la versión indicada en tu archivo de bloqueo.
Token disponible en tiempo de ejecución; el ejemplo usa Fast al pasar el cursor y elimina las transiciones cuando se activa la preferencia de movimiento reducido.
durationFaster
100ms
Token disponible en tiempo de ejecución; el ejemplo usa Fast al pasar el cursor y elimina las transiciones cuando se activa la preferencia de movimiento reducido.
durationFast
150ms
Token disponible en tiempo de ejecución; el ejemplo usa Fast al pasar el cursor y elimina las transiciones cuando se activa la preferencia de movimiento reducido.
durationNormal
200ms
Token disponible en tiempo de ejecución; el ejemplo usa Fast al pasar el cursor y elimina las transiciones cuando se activa la preferencia de movimiento reducido.
durationGentle
250ms
Token disponible en tiempo de ejecución; el ejemplo usa Fast al pasar el cursor y elimina las transiciones cuando se activa la preferencia de movimiento reducido.
durationSlow
300ms
Token disponible en tiempo de ejecución; el ejemplo usa Fast al pasar el cursor y elimina las transiciones cuando se activa la preferencia de movimiento reducido.
durationSlower
400ms
Token disponible en tiempo de ejecución; el ejemplo usa Fast al pasar el cursor y elimina las transiciones cuando se activa la preferencia de movimiento reducido.
durationUltraSlow
500ms
Token disponible en tiempo de ejecución; el ejemplo usa Fast al pasar el cursor y elimina las transiciones cuando se activa la preferencia de movimiento reducido.
Los cambios de tema son semánticos, no una inversión
Sistema de diseño Microsoft Fluent 2 e implementación v9 de React
Elige explícitamente la implementación web
Fluent 2 es un lenguaje de diseño multiplataforma. Esta guía toma una decisión más específica y práctica: @fluentui/react-components, la biblioteca convergente React v9. El proveedor principal es FluentProvider, y este ejemplo usa webLightTheme o webDarkTheme. Un tema de Teams es una configuración de marca independiente; el conocido morado de Teams no es la escala azul predeterminada para la web. Los recursos nativos de Windows, iOS y Android tampoco son paquetes React intercambiables.
El ejemplo completo de abajo muestra una descripción del proyecto y una lista local de accesos. Combina Title3 y Body1 para establecer la jerarquía, TabList y Tab para ofrecer dos vistas, y Dialog con Field, Input y Select para tomar una decisión de acceso. El código no afirma que invite a una persona real. Esta separación permite demostrar el comportamiento de los componentes sin inventar un servicio para compartir.
FluentProvider coloca las variables del tema en su subárbol. Una superficie personalizada con el estilo tokens.colorNeutralBackground2 sigue el tema seleccionado, mientras que una superficie con el color blanco codificado no lo hace. Coloca el proveedor por encima de los componentes que necesitan el tema. La biblioteca aplica estilos a sus componentes mediante su entorno de ejecución de estilos; el ejemplo no te pide importar un archivo fluent.css inexistente.
Colores semánticos del sistema de diseño Microsoft Fluent 2 y la escala de marca web real
Resuelve los alias según la marca elegida
La fuente de alias del tema claro asigna colorBrandBackground a brand[80]. El tema web proporciona brandWeb, donde el paso 80 es #0f6cbd. Por lo tanto, #0f6cbd es el relleno principal resultante en este ejemplo. El archivo de alias contiene comentarios ilustrativos antiguos con valores hexadecimales; interpretar esos comentarios como la paleta web actual daría un resultado incorrecto. El valor se establece mediante la relación de origen, no mediante el comentario.
Usa colorNeutralForeground1 para la descripción, colorNeutralForeground2 para las etiquetas complementarias y colorNeutralBackground3 para el espacio de trabajo circundante. Una acción principal puede usar colorBrandBackground con colorNeutralForegroundOnBrand. Los nombres de los roles siguen siendo el contrato cuando cambia el tema. El permiso de una persona se representa con texto; un borde azul o un indicador azul en una pestaña expresa la selección, no la autorización.
En el modo oscuro, la superficie principal se resuelve en #292929 y el texto principal en #ffffff. El relleno principal de marca usa el paso 70, mientras que el color de primer plano de los enlaces usa el paso 100. Por eso, la vista previa cambia conjuntamente los roles de los tokens en lugar de limitarse a invertir el fondo de la página. Al alternar el tema, compara el cuadro de diálogo de acceso, la pestaña seleccionada y los metadatos atenuados: todos forman parte del mismo límite temático.
Tipografía del sistema de diseño Microsoft Fluent 2: asigna roles de texto a tokens de tiempo de ejecución
Usa valores emparejados de tamaño y altura de línea
Body1 es el estilo de lectura base: 14px con una altura de línea de 20px. Caption1 es 12/16, Subtitle2 es 16/22 con grosor semibold y Title3 es 24/32 con grosor semibold. Estos pares crean una jerarquía diferenciada de títulos, etiquetas y cuerpo de texto sin tamaños intermedios arbitrarios. La tabla de arriba incluye los pasos más grandes como referencia, pero un espacio de trabajo compacto no necesita un titular de tamaño de presentación.
La fuente de tokens React y el sitio web de diseño discrepan actualmente en un detalle útil: fontSizeBase500 / lineHeightBase500 es 20/28 en la fuente de tiempo de ejecución, mientras que el gráfico de diseño web indica Subtitle1 en 20/26. Este ejemplo sigue el par de tiempo de ejecución. Considera estas diferencias una comprobación de versión e implementación, en lugar de combinar sin indicarlo los números que mejor encajen en la maqueta.
Usa la semántica HTML junto con el estilo visual predefinido: Title3 as="h1" es un encabezado; aplicar estilos por sí solo no crea la estructura del documento. La pila base empieza con Segoe UI e incluye alternativas para distintas plataformas. No incluyas una fuente propietaria solo para que una captura de pantalla coincida. El ejemplo usa la pila documentada, por lo que los glifos exactos dependen de las fuentes instaladas en el dispositivo donde se visualice.
Para texto personalizado, typographyStyles.body1 agrupa la familia, el tamaño, el grosor y la altura de línea. Prefiere esa combinación en lugar de copiar solo fontSizeBase300. La lista de accesos usa direcciones de correo legibles que se ajustan a varias líneas; la etiqueta del permiso usa énfasis semibold, pero no compite con el título de la página.
Espaciado, forma y densidad en un espacio de trabajo del sistema de diseño Microsoft Fluent 2
Los nombres de espaciado de React no son los nombres de la herramienta de diseño
Las directrices generales de diseño Fluent usan nombres de tamaño globales y un ritmo de cuatro unidades, que incluye 2, 6 y 10 para los ajustes de alineación. React ofrece alias específicos por eje: spacingHorizontalS es 8px, M es 12px, L es 16px y XXL es 24px, con nombres verticales equivalentes. No traduzcas size120 como spacingHorizontal120; ese identificador de React no existe.
Esta composición utiliza un relleno de 24px en el panel, una separación de 16px entre los campos del formulario y un espaciado de 12px para las filas de acceso. El ancho exacto de la página y la disposición en dos columnas son decisiones de la aplicación, no puntos de quiebre oficiales de Fluent. En una pantalla estrecha, el contexto complementario del equipo pasa debajo de la descripción, mientras las dos etiquetas de pestaña siguen visibles.
Sistema de diseño Microsoft Fluent 2 TabList: selección, paneles y comportamiento del teclado
El valor seleccionado es estado de la aplicación
El ejemplo mantiene tab en el estado de React y lo pasa a selectedValue. onTabSelect recibe data.value, que actualiza ese estado. Cada Tab tiene un valor estable y apunta a su panel mediante aria-controls. Cada panel tiene role="tabpanel", aria-labelledby y una condición hidden. El componente no proporciona la descripción del proyecto ni la lista de accesos; estos elementos se representan explícitamente.
API separa el enfoque de la selección: selectTabOnFocus tiene el valor predeterminado false. En el ejemplo independiente, las teclas Left/Right y Home/End mueven el foco, y Enter o Space activa la pestaña enfocada. Este comportamiento de activación manual permite revisar las opciones de pestaña sin provocar un cambio inesperado de contenido. Mantén cortas las etiquetas de las pestañas; las directrices oficiales de diseño indican que las pestañas horizontales no se ajustan automáticamente ni tienen desplazamiento.
Elige otro patrón cuando aumente el número de categorías
Descripción y Personas son dos vistas estrechamente relacionadas de un mismo espacio de trabajo, por lo que las pestañas son adecuadas. Una lista de todos los proyectos de la organización sería navegación, no otra fila de pestañas. Si las categorías ya no caben en un ancho reducido, diseña deliberadamente un patrón de desbordamiento u otra forma de selección; reducir las etiquetas hasta volverlas ilegibles no es una estrategia adaptable.
Campo del sistema de diseño Fluent de Microsoft Fluent 2: etiquetas accesibles y validación significativa
Un campo es una composición, no una entrada con estilos
Field combina un espacio para la etiqueta, el control secundario, una indicación opcional y un mensaje de validación. Su contrato documentado para el control incluye id, aria-labelledby, aria-describedby, aria-invalid y aria-required. Los controles de entrada de Fluent pueden ser elementos secundarios directos; los controles personalizados pueden consumir las propiedades de la función de renderizado. Por eso, envolver el componente Input de correo electrónico con Field resulta más útil que colocar texto sin relación encima de una entrada.
En el ejemplo, validationState solo tiene el valor error después de enviar un valor no válido. La aplicación comprueba que el valor tenga formato de correo electrónico y que no haya duplicados en su lista en memoria. Conserva el valor introducido y proporciona una indicación para corregirlo mediante validationMessage. Field presenta y vincula ese resultado; no determina si la dirección existe, envía una invitación ni concede acceso.
Mantén el estado de validación separado del permiso
El componente Select de permisos contiene Puede ver y Puede editar, mientras que el campo de correo electrónico tiene su propio estado de error. Evita que todo el cuadro de diálogo se vuelva rojo porque una dirección esté incompleta. required es una indicación accesible, no un sustituto de la autorización o validación del servidor. El formulario de ejemplo usa noValidate para mostrar de forma coherente su mensaje de validación explícito; un servicio de producción debe volver a validar la solicitud.
Dirección vacía o con formato incorrecto: mantén abierto el cuadro de diálogo y explica el formato aceptado.
Dirección que ya está en la lista de accesos local: muestra un mensaje específico sobre el duplicado sin añadir otra fila.
Dirección nueva y válida: añade localmente el permiso elegido, cierra el cuadro de diálogo y muestra un resultado que indique con claridad que solo se ha aplicado de forma local.
Cuadro de diálogo del sistema de diseño Fluent de Microsoft Fluent 2: un formulario dentro de la estructura compatible
Mantén el botón de envío dentro del formulario
La composición consta de Dialog, DialogTrigger, DialogSurface, un elemento form y DialogBody, que contiene DialogTitle, DialogContent y DialogActions. El formulario envuelve el cuerpo para que el botón de envío del pie participe en el envío. Cancel tiene type="button" y se encuentra dentro de un DialogTrigger que cierra el cuadro; de lo contrario, el tipo predeterminado del botón podría enviar el formulario accidentalmente.
open y onOpenChange controlan la visibilidad. disableButtonEnhancement impide que el disparador aplique mejoras redundantes a un Button de Fluent que ya proporciona el comportamiento de botón. El controlador de éxito cierra el cuadro solo después de que la validación local se complete correctamente. Si la validación falla, el mismo correo electrónico y permiso siguen disponibles para corregirlos.
Elige el comportamiento modal según la decisión real
Esta decisión breve sobre el acceso utiliza el tipo modal predeterminado. Un cuadro de diálogo no modal permitiría trabajar en segundo plano, mientras que un cuadro de diálogo de alerta está pensado para decisiones urgentes y tiene un comportamiento de cierre más estricto. Un formulario de acceso normal no necesita semántica de alerta. Las directrices oficiales indican que el foco debe estar dentro del cuadro de diálogo y volver a su disparador al cerrarlo; la implementación independiente utiliza el elemento nativo dialog y restablece explícitamente el foco en su disparador.
Estilos personalizados del sistema de diseño Fluent de Microsoft Fluent 2 sin alterar el tema
Usa makeStyles y variables semánticas
El código completo utiliza makeStyles con tokens para las superficies, las separaciones y el radio. Así, el panel personalizado mantiene la coherencia con el proveedor. El nombre de clase generado es un detalle de implementación; no lo copies de DevTools en una hoja de estilos. Para probar otros temas, cambia el tema del proveedor y examina conjuntamente los controles nativos y las áreas personalizadas.
La escala de duración publicada va de 50ms a 500ms. El ejemplo HTML usa durationFast, de 150ms, para cambios sencillos al pasar el cursor y elimina esas transiciones cuando está activo prefers-reduced-motion. Es una decisión de implementación que utiliza un token disponible, no una afirmación de que todas las interacciones de Fluent deban durar 150ms. El contenido y los mensajes de error siguen siendo comprensibles aunque se elimine el movimiento.
Sistema de diseño Microsoft Fluent 2: instala y verifica la composición completa
Un punto de entrada ejecutable, no un esquema de tareas
La configuración está pensada para una aplicación React existente que tenga un elemento con id root. Instala el conjunto v9 y luego añade la entrada JSX a la aplicación. Se incluyen juntos los imports, el proveedor, el árbol de componentes, los controladores, el estado y el renderizado en la raíz. No se invoca ningún servicio externo para compartir API. Si tu plantilla inicial ya renderiza App, conserva su entrada raíz y exporta App en lugar de crear una segunda raíz.
Registra la versión resuelta del paquete en el archivo de bloqueo. Las referencias al código fuente enlazadas describen la implementación de React v9 que se inspeccionó, mientras que las URL de master pueden cambiar. Antes de actualizar, compara el tema exportado y el comportamiento exacto de Field, TabList y Dialog que se usan aquí. La configuración evita deliberadamente combinar componentes de v8 @fluentui/react con ejemplos de v9.
Instala @fluentui/react-components@9 en el proyecto React existente y conserva el archivo de bloqueo generado.
Renderiza la entrada proporcionada dentro de un único FluentProvider, con webLightTheme como tema inicial.
Abre el cuadro de diálogo, envía una dirección no válida, corrígela y confirma que la persona añadida aparezca únicamente en el estado local.
Repite la prueba con webDarkTheme y navegación mediante teclado; verifica la asociación del panel, el anuncio del error y la devolución del foco.
Sustituye la adición local solo después de definir el contrato del servicio: actor autenticado, espacio de trabajo de destino, valores de permiso permitidos, resultado de la invitación y comportamiento ante duplicados. Mantén abierto el cuadro de diálogo si el servicio rechaza una solicitud y muestra el resultado confirmado en lugar de afirmar de forma optimista que se concedió el acceso. Estas reglas no forman parte de Fluent, pero el código deja explícito el punto de integración en vez de ocultarlo tras un botón de envío genérico.
Ejemplos de componentes del sistema de diseño Microsoft Fluent 2
Muestra interactiva independiente completa
Guarda el documento completo como index.html y ábrelo en un navegador. Interpretación independiente en HTML/CSS de los tokens web de Fluent UI React v9 y de la estructura documentada de Field, TabList y Dialog. No es un entorno de ejecución nativo de componentes 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>
El cuadro de diálogo de acceso y las pestañas de este documento usan controles nativos de HTML; ejecuta la entrada independiente de React v9 para evaluar el comportamiento de FluentProvider, Field y Dialog.
Entrada de implementación nativa completa
La implementación completa de la configuración, reunida aquí junto con las explicaciones específicas de API. Sigue los requisitos documentados de paquetes y compilación en la sección de configuración.
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 />);
Composición completa de componentes de React v9 para una aplicación React existente. La vista previa independiente es una interpretación aparte en HTML/CSS. Este código solo realiza validaciones locales y actualizaciones de estado; no envía correos electrónicos ni modifica permisos reales. Se comprobaron las API de origen; no se afirma que la compilación con el paquete nativo funcione hasta probarla en el proyecto de destino.
Field + Input + Select: un formulario de acceso con etiquetas
Estos fragmentos usan las variables de estado y los imports de la configuración completa que aparece a continuación. Field se encarga de mostrar la etiqueta, la indicación y la validación; la aplicación se encarga de la regla para el correo electrónico y de comprobar si hay duplicados.
required proporciona una indicación accesible de que el campo es obligatorio; no implementa la regla de validación de la aplicación.
El estado de validación predeterminado pasa a ser error cuando existe validationMessage. Establece none explícitamente cuando no deba mostrarse ningún error.
El onChange de Input proporciona la cadena actual en data.value; no confundas su controlador con un evento de entrada del DOM sin más.
TabList controlado con paneles asociados explícitamente
TabList controla la selección y el comportamiento de las pestañas. Renderiza los paneles de contenido por tu cuenta y mantén conectados sus ids y nombres accesibles con cada Tab.
selectedValue debe definirse en TabList; cada Tab tiene un valor distinto.
selectTabOnFocus es false de forma predeterminada. El foco con las flechas y la activación son acciones independientes, salvo que se cambie deliberadamente ese comportamiento.
Las pestañas horizontales de Fluent no se ajustan a varias líneas ni se desplazan automáticamente; el ejemplo usa dos etiquetas cortas para que sigan visibles.
04
Configuración del agente del sistema de diseño Microsoft Fluent 2
Punto de partida para la implementación
Composición completa de componentes de React v9 para una aplicación React existente. La vista previa independiente es una interpretación aparte en HTML/CSS. Este código solo realiza validaciones locales y actualizaciones de estado; no envía correos electrónicos ni modifica permisos reales. Se comprobaron las API de origen; no se afirma que la compilación con el paquete nativo funcione hasta probarla en el proyecto de destino.
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 />);
Una instrucción para tu proyecto
Instrucción para el agente
Usa Fluent UI React v9 desde @fluentui/react-components. Renderiza FluentProvider con webLightTheme y un control para alternar a webDarkTheme. Compón la descripción con Title3 y Body1, vistas controladas de TabList/Tab con paneles explícitos y un cuadro de diálogo de acceso con Field/Input/Select y un formulario real. Usa tokens para las superficies personalizadas y el espaciado. Sigue el ejemplo de componentes proporcionado y los pares de tipos en tiempo de ejecución, incluido Subtitle1 en 20/28. Conserva las entradas no válidas, rechaza las direcciones duplicadas locales, devuelve el foco al cuadro de diálogo y describe con precisión las adiciones locales. Conecta un servicio para compartir solo cuando se proporcione su contrato de autorización y resultados.
05
Fuentes y alcance del sistema de diseño Microsoft Fluent 2
Un estudio de una aplicación OpenDesign de Fluent UI React v9; no es un producto de Microsoft ni un servicio activo para compartir. La vista previa independiente es una interpretación HTML/CSS de los tokens web verificados y de la estructura de los componentes; el código React de abajo usa componentes nativos de Fluent. Los valores de referencia corresponden a la instantánea del código fuente enlazado que se comprobó el 3 de octubre de 2026; no garantizan que todas las versiones, plataformas o marcas usen valores idénticos.