logo
Published on

SPFx Fluent UI v9 Migration Guide

If your SPFx solution still imports from office-ui-fabric-react or @fluentui/react v8, you are building on a package that Microsoft has stopped actively developing.
Fluent UI React v9 is a ground-up rewrite — new component APIs, a new theming engine, better accessibility, and significantly smaller bundle sizes.
This guide walks through everything you need to do to migrate an existing SPFx web part from v8 to v9, including the breaking changes that will catch you off guard.


📦 What Changed Between v8 and v9

Before touching any code, it helps to understand the scope of the change:

AreaFluent UI v8Fluent UI v9
Package@fluentui/react@fluentui/react-components
StylingCSS-in-JS (Merge Styles)Griffel (atomic CSS-in-JS)
ThemingThemeProvider + IThemeFluentProvider + design tokens
Component APIsClass + render props styleHooks-first, slot-based
Bundle sizeLarge monolithTree-shakeable per-component
React requirementReact 16+React 17+ (React 18 supported)

v9 is not a drop-in upgrade. Component names often match but their props do not. Plan for a full component-by-component replacement.


📦 Install the v9 Package

Remove the old package and install v9:

npm uninstall @fluentui/react
npm install @fluentui/react-components --save

If you were using office-ui-fabric-react (the older name), remove that too:

npm uninstall office-ui-fabric-react

SPFx 1.18+ ships with React 17, which satisfies the v9 peer dependency. No React upgrade is required for SPFx 1.18.


By default, SPFx bundles everything it finds in node_modules. For large packages like Fluent UI, you can declare them as externals so SharePoint serves them from its CDN — reducing your .sppkg size substantially.

In config/config.json, add:

{
  "externals": {
    "@fluentui/react-components": {
      "path": "https://res.cdn.office.net/files/fabric-cdn-prod_20240610.002/office-ui-fabric-js/1.10.0/css/fabric.min.css",
      "globalName": "FluentUIReactComponents"
    }
  }
}

Note: Check the current CDN path for v9 in the official SPFx documentation before committing to an external reference — the path changes with each Fluent UI release.


🧩 Wrap Your Root Component with FluentProvider

Every v9 component must be rendered inside a FluentProvider. The provider injects the design token context that all v9 components depend on.

v8 (old):

import { ThemeProvider } from '@fluentui/react';

public render(): void {
  ReactDOM.render(
    <ThemeProvider>
      <MyComponent {...props} />
    </ThemeProvider>,
    this.domElement
  );
}

v9 (new):

import { FluentProvider, webLightTheme } from '@fluentui/react-components';

public render(): void {
  ReactDOM.render(
    <FluentProvider theme={webLightTheme}>
      <MyComponent {...props} />
    </FluentProvider>,
    this.domElement
  );
}

Available built-in themes: webLightTheme, webDarkTheme, teamsLightTheme, teamsDarkTheme, teamsHighContrastTheme.

For SharePoint modern sites, webLightTheme is the correct default. If you need to match the site's custom theme, you will need to map SharePoint theme tokens to Fluent v9 design tokens manually — this is advanced and outside the scope of a standard migration.


🔁 Component Replacements — v8 to v9

The table below covers the most commonly used components. The v9 equivalents are imported from @fluentui/react-components.

v8 Componentv9 EquivalentNotes
PrimaryButton / DefaultButtonButtonUse appearance="primary" prop
TextFieldInputWraps with Field for label + validation
DropdownDropdown + OptionNew slot-based API
CheckboxCheckboxProps mostly compatible
ToggleSwitchRenamed
SpinnerSpinnerProps changed — size values differ
DialogDialog + DialogSurfaceSlot-based; no dialogContentProps
PanelDrawerRenamed and restructured
LabelLabelStandalone or via Field
MessageBarMessageBarNew slot-based API
StackCSS / makeStylesStack removed — use flex layout
PersonaPersonaSlot-based; avatar separate
IconUse @fluentui/react-iconsIcons are now a separate package

🔧 Common Migration Patterns

Button

v8:

import { PrimaryButton, DefaultButton } from '@fluentui/react';

<PrimaryButton text="Submit" onClick={handleSubmit} />
<DefaultButton text="Cancel" onClick={handleCancel} />

v9:

import { Button } from '@fluentui/react-components';

<Button appearance="primary" onClick={handleSubmit}>Submit</Button>
<Button onClick={handleCancel}>Cancel</Button>

TextField → Input + Field

v8:

import { TextField } from '@fluentui/react';

<TextField label="Title" value={title} onChange={(_, val) => setTitle(val ?? '')} />

v9:

import { Field, Input } from '@fluentui/react-components';

<Field label="Title">
  <Input value={title} onChange={(_, data) => setTitle(data.value)} />
</Field>

The Field component handles label, hint text, and validation message — previously these were props on TextField directly.


v8:

import { Dropdown, IDropdownOption } from '@fluentui/react';

const options: IDropdownOption[] = [
  { key: 'apple', text: 'Apple' },
  { key: 'banana', text: 'Banana' }
];

<Dropdown
  label="Fruit"
  options={options}
  selectedKey={selected}
  onChange={(_, opt) => setSelected(opt?.key as string)}
/>

v9:

import { Dropdown, Option, Field } from '@fluentui/react-components';

<Field label="Fruit">
  <Dropdown
    value={selected}
    onOptionSelect={(_, data) => setSelected(data.optionValue ?? '')}
  >
    <Option value="apple">Apple</Option>
    <Option value="banana">Banana</Option>
  </Dropdown>
</Field>

Icons

v9 does not include icons in the main package. Install the icons package separately:

npm install @fluentui/react-icons --save

v8:

import { Icon } from '@fluentui/react';
<Icon iconName="Add" />

v9:

import { AddRegular } from '@fluentui/react-icons';
<AddRegular />

Icons are now individual SVG React components — fully tree-shakeable. Browse the full catalogue at react.fluentui.dev.


Styling — makeStyles replaces mergeStyles

v8:

import { mergeStyles } from '@fluentui/react';

const containerClass = mergeStyles({ padding: '16px', backgroundColor: 'white' });

v9:

import { makeStyles } from '@fluentui/react-components';

const useStyles = makeStyles({
  container: { padding: '16px', backgroundColor: 'white' }
});

// Inside component:
const styles = useStyles();
<div className={styles.container}>...</div>

makeStyles uses Griffel under the hood — atomic CSS-in-JS that generates minimal, de-duplicated class names. It must be called at the module level (outside the component function), with the returned hook called inside the component.


⚠️ Common Gotchas

Stack is gone. Stack and Stack.Item do not exist in v9. Replace with standard flexbox using makeStyles or inline styles. Most usages of Stack map to a <div style={{ display: 'flex', gap: '8px' }}>.

onChange signature changed. Most v9 input components pass (event, data) to onChange — the new value is in data.value, not the second argument directly. Update every onChange handler.

FluentProvider must wrap everything. Rendering a v9 component outside a FluentProvider produces unstyled output with no error in the console. If components look broken, check that FluentProvider is in the tree.

Cannot mix v8 and v9 in the same render tree. The two theming systems are incompatible. Migrate one web part at a time to a clean v9 setup rather than mixing imports within the same component tree.


🚀 Deploy the Migrated Solution

Once the migration is complete, build, bundle, and package as normal:

npm run build
npm run bundle -- --ship
npm run package-solution -- --ship

Upload the updated .sppkg to your App Catalog. Because only the bundle content changes (not the solution ID or feature IDs), existing web part instances on pages retain their saved properties.


✅ Summary

  • Replace @fluentui/react with @fluentui/react-components — they cannot coexist in the same component tree.
  • Wrap your root component in FluentProvider with a theme — v9 components are unstyled without it.
  • Button, Input/Field, Dropdown/Option, Switch, Drawer replace their v8 counterparts with new slot-based APIs.
  • Stack is removed — use flexbox via makeStyles.
  • Icons move to @fluentui/react-icons as individual tree-shakeable SVG components.
  • makeStyles + Griffel replaces mergeStyles for component-level styling.
  • Migrate one web part at a time. The before/after GitHub sample is your reference.

Happy coding!

Ad image