| name | ui-fluentui-react |
| description | Best practices for building business applications with Fluent UI React v9 (@fluentui/react-components). Covers FluentProvider setup, theming, styling with makeStyles, layout patterns, data tables, forms, dialogs, drawers, navigation, toast notifications, command bars, record forms, lookup fields, subgrids, timelines, BPF bars, app shell navigation, and accessible component composition. Triggers on "fluent ui", "fluentui", "@fluentui/react-components", "fluent v9", "fluent provider", "makeStyles", "fluent theme", "fluent form", "fluent dialog", "fluent table", "fluent data grid", "fluent drawer", "fluent toast", "fluent toolbar", "business app ui", "enterprise ui react", "command bar", "record form", "lookup field", "subgrid", "timeline", "BPF", "business process flow", "app shell", "left nav", "entity list", "sitemap". |
| user-invocable | false |
Fluent UI React v9 — Business App Patterns
You are an expert in building business/enterprise React applications with Fluent UI v9 (@fluentui/react-components). This skill covers the complete component library, styling system, theming, and composition patterns for data-driven line-of-business apps.
Version note: This skill targets Fluent UI React v9 only — the @fluentui/react-components package. Do NOT use patterns from v8 (@fluentui/react) or v0 (@fluentui/react-northstar). Those are legacy.
Resources
Component templates and code patterns are in the resources/ folder:
| Resource | Content |
|---|
resources/pattern-mandatory-scaffold.md | START HERE — Required shell components, dependencies, main.tsx/App.tsx/index.css structure, verification checklist |
resources/layout-patterns.md | Flex column/row, responsive grid, page layout, print/skeleton/scrollbar (replaces v8 Stack) |
resources/pattern-list-detail.md | List/Card view, Detail view with loading/error/empty states |
resources/pattern-form.md | Form component, <Field> usage, useFormValidation hook, dirty tracking, auto-save, unsaved changes warning |
resources/pattern-data-grid.md | DataGrid with sorting, selection, row actions |
resources/pattern-dialog-drawer.md | Dialog (uncontrolled + controlled), Drawer (overlay + inline), ConfirmDialog, BulkEditDialog, AssignDialog |
resources/pattern-toolbar-nav.md | Search/Filter bar, Toolbar, Toast notifications, Tabs, Breadcrumb |
resources/pattern-command-bar.md | MDA-style command bar with overflow menu, split buttons, entity-specific command orchestration |
resources/pattern-record-form.md | Record form with entity header, status badge, tab bar, collapsible form sections, loading skeleton |
resources/pattern-lookup-field.md | Inline lookup with debounced search, Popover results, advanced lookup Dialog, @odata.bind |
resources/pattern-subgrid-timeline.md | Subgrid on DataGrid, Timeline wall with compose area, date grouping, file attachments |
resources/pattern-entity-list-advanced.md | Entity list with view selector, edit-columns/filters Drawer, inline editing, pagination, CSV export |
resources/pattern-bpf-bar.md | Business Process Flow bar with stage navigation, stage details Popover, completed/current/future states |
resources/pattern-app-shell-nav.md | AppShell layout, collapsible LeftNav with area switching, global search, recent/pinned items, HashRouter |
resources/styling-griffel.md | makeStyles (Griffel) guide: mergeClasses rules, class composition checklist, spacing and typography token tables |
resources/pattern-errorboundary.md | Full ErrorBoundary class component with Fluent MessageBar fallback and per-route usage |
resources/pattern-notifications-zustand.md | Zustand notification store + Fluent Toaster bridge pattern for global notifications |
resources/lessons-learned.md | Verification checklist + index mapping 24 lessons to their resource files |
Load the relevant resource when building a specific component type.
IMPORTANT: Before writing any Fluent UI code, load resources/lessons-learned.md and
apply its checklist. The detailed rules now live in the individual resource files listed in the index.
Installation
npm install @fluentui/react-components
Optional icon packages:
npm install @fluentui/react-icons
NON-NEGOTIABLE: Mandatory Shell Scaffold
When scaffolding a new business app, certain components and wrappers are REQUIRED
in the initial scaffold before any feature development begins.
Load resources/pattern-mandatory-scaffold.md FIRST when starting any new project.
It defines the required dependencies, shell components, main.tsx / App.tsx structure,
and a verification checklist. Do NOT proceed to feature work until every item passes.
Key requirements (see the resource file for full templates and rules):
- Dependencies:
zustand is required alongside Fluent UI, React Query, React Router
- Shell components: AppShell + TopBar + LeftNav (from
pattern-app-shell-nav.md), ErrorBoundary, NotificationBridge + Zustand store, Suspense wrapper
- TopBar: Must include hamburger toggle, app title, and user profile display
- LeftNav: Must support expand/collapse with responsive auto-collapse at ≤768px
- Theme: Default to
webLightTheme for business apps
- CSS: Include Fluent scrollbar styling in
index.css
FluentProvider — Required Root Wrapper
Every Fluent UI v9 app must wrap its root in <FluentProvider> with a theme. Without it, components render with no styling.
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
export const App = () => (
<FluentProvider theme={webLightTheme}>
{/* All app content goes here */}
</FluentProvider>
);
Rules:
- Place
FluentProvider as high as possible in the component tree (usually in App.tsx or main.tsx)
- You can nest multiple
FluentProvider instances to override theme for a subtree (e.g., dark sidebar)
- Default to
webLightTheme for business/enterprise apps — this is the standard
- Available built-in themes:
webLightTheme, webDarkTheme
- For Teams apps:
teamsLightTheme, teamsDarkTheme, teamsHighContrastTheme
FluentProvider Height Chain
<FluentProvider> renders a wrapper <div> with no explicit height. If your app uses
percentage-based heights (e.g., AppShell with height: 100%), you MUST add
style={{ height: '100%' }} to the FluentProvider:
<FluentProvider theme={webLightTheme} style={{ height: '100%' }}>
<App />
</FluentProvider>
Why: Without this, the height chain breaks:
html (100%) → body (100%) → #root (100%) → FluentProvider (auto ← BREAK) → AppShell (100% of auto = 0)
This causes content overflow, invisible footers/pagination, and broken scroll containers.
Custom Theme Tokens
import { webLightTheme, type Theme } from "@fluentui/react-components";
const customTheme: Theme = {
...webLightTheme,
colorBrandBackground: "#0078d4",
colorBrandBackgroundHover: "#106ebe",
colorNeutralForeground1: "#242424",
};
Use semantic token names from the theme throughout your app — never hardcode colors.
Dark Mode Toggle — Required
Every Fluent UI app MUST include a dark mode toggle. Use webLightTheme / webDarkTheme
and respect prefers-color-scheme as the initial default:
import { webLightTheme, webDarkTheme } from "@fluentui/react-components";
const [isDark, setIsDark] = useState(
() => window.matchMedia("(prefers-color-scheme: dark)").matches
);
<FluentProvider theme={isDark ? webDarkTheme : webLightTheme} style={{ height: '100%' }}>
Add a toggle in the TopBar and persist the preference to localStorage.
See resources/styling-griffel.md for the complete Griffel styling guide: makeStyles usage,
mergeClasses rules, class composition checklist, spacing and typography token tables.
Buttons — Choosing the Right Variant
| Appearance | Use For | Example |
|---|
"primary" | Main action per page/section (1 per group) | Save, Submit, Create |
"secondary" | Supporting actions | Cancel, Back |
"subtle" | Inline or low-emphasis actions, icon buttons | Edit, Delete in a row |
"transparent" | Ghost buttons, navigation links | Breadcrumbs, inline links |
"outline" | Medium emphasis, secondary CTA | Export, Filter |
<Button appearance="primary">Save</Button>
<Button appearance="secondary" onClick={onCancel}>Cancel</Button>
<Button appearance="subtle" icon={<EditRegular />} aria-label="Edit" />
Compound button for actions with a description:
<CompoundButton
appearance="primary"
secondaryContent="Create a new project from scratch"
>
New Project
</CompoundButton>
Icons
Use @fluentui/react-icons. All icons follow the naming pattern: {Name}{Size}{Style}.
import { AddRegular, EditRegular, DeleteRegular, SearchRegular } from "@fluentui/react-icons";
<Button icon={<AddRegular />}>Add</Button>
<Button icon={<DeleteRegular />} appearance="subtle" aria-label="Delete" />
Sizing: Icons default to 20px. For other sizes import the sized variant: Add16Regular, Add24Regular, Add28Regular.
Styles: Regular (outline) for most UI, Filled for selected/active states.
Accessibility Checklist
- Always provide
aria-label on icon-only buttons
- Use
<Field> for all form controls — it generates correct <label> associations
- Use semantic
intent on MessageBar ("error", "warning", "success", "info")
- Use
useId() hook for generating unique IDs — never hardcode IDs
- Keyboard navigation works out of the box for Fluent components — do not override
tabIndex unless necessary
- Test with screen readers:
DataGrid already implements ARIA grid patterns
- Use
role="status" or aria-live="polite" for dynamic content updates
Scrollable Container Accessibility
Any container with overflow: auto or overflow: scroll MUST be keyboard-accessible:
<div
className={styles.page}
tabIndex={0}
role="region"
aria-label="Page content"
style={{ outline: 'none' }}
>
{}
</div>
Without tabIndex={0}, keyboard users cannot scroll the content with arrow keys or Page Up/Down.
import { useId } from "@fluentui/react-components";
const MyComponent = () => {
const inputId = useId("input");
};
Pattern Checklist for Every Business Component
- Loading state — Show
<Spinner label="Loading..." />
- Error state — Show
<MessageBar intent="error"><MessageBarBody>...</MessageBarBody></MessageBar>
- Empty state — Show
<MessageBar intent="info"><MessageBarBody>No items found.</MessageBarBody></MessageBar> or a custom empty illustration
- Disabled state — Disable buttons during mutations with
disabled={isPending}
- Validation — Use
<Field validationMessage="..." validationState="error"> for inline form errors
- Confirmation — Use
<Dialog> before destructive actions (delete, discard changes)
- Feedback — Use toast notifications for success/failure of async operations
- Responsive — Use
makeStyles with CSS grid/flex and @media queries for different viewports
See resources/pattern-errorboundary.md for the full ErrorBoundary class component with
Fluent MessageBar fallback and per-route usage pattern.
flexShrink in Flex Containers
When building page layouts with display: flex and overflow: auto, set flexShrink: 0
on ALL fixed-height children (headers, footers, toolbars, pagination):
const useStyles = makeStyles({
page: {
display: 'flex',
flexDirection: 'column',
height: '100%',
overflow: 'auto',
},
header: {
flexShrink: 0,
},
content: {
flex: 1,
minHeight: 0,
overflow: 'auto',
},
footer: {
flexShrink: 0,
},
});
IMPORTANT: When adding flexShrink: 0 to fix a page, grep for ALL pages that use the
same layout pattern and fix them all. Don't fix one page and leave the others vulnerable.
DataGrid Responsive Patterns
Always configure explicit column sizing to prevent columns from collapsing at narrow viewports:
const columnSizingOptions: TableColumnSizingOptions = {
title: { minWidth: 200, idealWidth: 300 },
status: { minWidth: 100, idealWidth: 120 },
date: { minWidth: 100, idealWidth: 140 },
};
<DataGrid
columnSizingOptions={columnSizingOptions}
resizableColumns
>
Use <TableCellLayout truncate> to handle overflow text instead of letting cells wrap and expand.
See resources/pattern-notifications-zustand.md for the complete Zustand notification store
and Fluent Toaster bridge pattern.
Lazy Loading Pages
Use React.lazy() and <Suspense> for route-level code splitting. This keeps the initial bundle small.
import { lazy, Suspense } from "react";
import { Spinner } from "@fluentui/react-components";
const AccountsPage = lazy(() => import("./pages/AccountsPage"));
const ContactsPage = lazy(() => import("./pages/ContactsPage"));
const PageFallback = () => (
<div style={{ display: "flex", justifyContent: "center", padding: "48px" }}>
<Spinner label="Loading..." size="medium" />
</div>
);
<Suspense fallback={<PageFallback />}>
<Routes>
<Route path="/accounts" element={<AccountsPage />} />
<Route path="/contacts" element={<ContactsPage />} />
</Routes>
</Suspense>
Rules:
- Pages must use
export default for lazy() to work
- Place
<Suspense> around <Routes>, not around each individual <Route>
- Use
<Spinner> as fallback — not a blank screen
HashRouter for Power Apps Code Apps
Power Apps Code Apps run inside an iframe where the host controls the URL. BrowserRouter will not work — always use HashRouter.
import { HashRouter } from "react-router-dom";
<HashRouter>
<App />
</HashRouter>
Status & Option Set Color Config
Map Dataverse option set values to Fluent UI Badge colors using a config object — avoid scattered switch/if chains.
import type { BadgeProps } from "@fluentui/react-components";
interface StatusConfig {
label: string;
color: BadgeProps["color"];
icon?: React.ReactElement;
}
export const caseStatusConfig: Record<number, StatusConfig> = {
0: { label: "Active", color: "success" },
1: { label: "Resolved", color: "informative" },
2: { label: "Cancelled", color: "danger" },
};
export const priorityConfig: Record<number, StatusConfig> = {
1: { label: "High", color: "danger" },
2: { label: "Normal", color: "warning" },
3: { label: "Low", color: "informative" },
};
import { Badge } from "@fluentui/react-components";
import { caseStatusConfig } from "../config/statusConfig";
const StatusBadge = ({ statusCode }: { statusCode: number }) => {
const config = caseStatusConfig[statusCode] ?? {
label: "Unknown",
color: "informative" as const,
};
return (
<Badge appearance="filled" color={config.color}>
{config.label}
</Badge>
);
};
QueryClient Configuration
Configure TanStack Query's QueryClient with sensible defaults for business apps.
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
retry: 1,
refetchOnWindowFocus: false,
},
mutations: {
retry: 0,
},
},
});
Rules:
staleTime prevents constant re-fetches while the user navigates between tabs/pages
refetchOnWindowFocus: false is recommended for Power Apps (the host iframe causes spurious focus events)
- Set
gcTime >= staleTime so cached data isn't garbage-collected while still considered fresh
Validation Checklist
After implementing a component, verify: npm run build passes with no errors, no mergeClasses() warnings appear in the browser console, the component renders inside <FluentProvider>, and keyboard navigation works for interactive elements.
Common Mistakes to Avoid
| Mistake | Correct Approach |
|---|
Using Stack from v8 | Use makeStyles with display: "flex" and gap |
Hardcoded colors ("#0078d4") | Use tokens.colorBrandBackground |
Hardcoded spacing ("16px", paddingBottom: "40px") | Use tokens.spacingVerticalL — hardcoded px values are workarounds |
| Hardcoded radii, font sizes, shadows | Use tokens.borderRadiusMedium, tokens.fontSizeBase300, etc. |
Missing FluentProvider | Wrap app root with <FluentProvider theme={webLightTheme}> |
Using style={{...}} inline | Use makeStyles for performance and consistency |
Reading event.target.value in onChange | Read from second arg: (_, data) => data.value |
Not wrapping inputs in <Field> | Always use <Field label="..."> for accessible forms |
v8 DetailsList for tables | Use v9 DataGrid with createTableColumn |
v8 Modal for dialogs | Use v9 Dialog + DialogSurface |
v8 Panel for side panels | Use v9 Drawer with position="end" |
Using @fluentui/react (v8 package) | Use @fluentui/react-components (v9) |
Importing from @fluentui/react-northstar | Deprecated — use @fluentui/react-components |
Missing aria-label on icon buttons | Always add aria-label="Action name" |
BrowserRouter in Power Apps | Use HashRouter — host controls URL |
| Hardcoded status colors | Use a status config map with Badge color prop |
| Custom dropdown for nav areas | Use Fluent Menu / MenuList |
| Custom HTML table for data | Use Fluent DataGrid with createTableColumn |
Hardcoded back-navigation (navigate("/list")) | Pass { state: { from } } via React Router and read it in the detail page — back should return to the page the user came from, not a hardcoded route |
Package Reference
| Package | Purpose |
|---|
@fluentui/react-components | Main v9 package — all components, hooks, tokens |
@fluentui/react-icons | Icon library (Fluent System Icons) |
@fluentui/react-datepicker-compat | DatePicker (compat layer, pending full v9 port) |
@fluentui/react-timepicker-compat | TimePicker (compat layer) |
Note: DatePicker and TimePicker are in *-compat packages. They use v8 internals but integrate with v9 themes. Import them alongside @fluentui/react-components. A full v9 native DatePicker is expected in a future release.