Create beautiful, cohesive Microsoft experiences using Fluent 2.
A Material-based Fluent 2 design system for Flutter.

Fluent2 UI Design

Android example APK build status Releases: manual

Installing

Add this to your pubspec.yaml file and run dart pub get to download the package.

dependencies:
  fluent2_kit:

Import it in the files where it will be used:

import 'package:fluent2_kit/fluent2_kit.dart';

Coexisting with fluent_ui

fluent2_kit targets the Fluent 2 mobile/iOS spec and uses class names like FluentButton, FluentCard, FluentTextField. The popular fluent_ui package targets the Fluent UI desktop/Windows spec and uses mostly unprefixed names (Button, Card, TextBox), so day-to-day collisions are rare.

If you do use both packages in the same project — for example, a cross-platform app with Windows desktop and iOS/Android targets — import one of them with an alias to avoid ambiguity on framework-level types (FluentIcons, Fluent2ThemeData vs FluentThemeData):

import 'package:fluent2_kit/fluent2_kit.dart';
import 'package:fluent_ui/fluent_ui.dart' as fluent_desktop;

Then reference desktop-only types via the prefix:

fluent_desktop.NavigationView(...)   // Windows desktop nav
FluentNavBar(...)                    // mobile Fluent 2 nav from fluent2_kit

✨ Features

  • Default theme based on Fluent 2
  • Useful components, including behavior
  • Almost no dependencies beyond Flutter

Local Android example builds

The example app can be packaged locally on demand:

scripts/build_android_example.sh --apk

Useful options:

scripts/build_android_example.sh --aab
scripts/build_android_example.sh --both --clean
scripts/build_android_example.sh --apk --build-number 49 --build-name 1.0.6
scripts/build_android_example.sh --apk --skip-tests

Signed release builds use example/android/key.properties when present. Without it, the Android project falls back to debug signing so the APK remains installable for local testing.

Example key.properties:

storeFile=upload-keystore.jks
storePassword=...
keyAlias=...
keyPassword=...

Getting started

cover_image

First, wrap your MaterialApp with FluentProvider


FluentProvider(
  child: MaterialApp(...),
);

The FluentScaffold

Use FluentScaffold instead of Scaffold when you need Fluent banners, toasts, or bottom-sheet behavior.


FluentScaffold(
  appBar: FluentNavBar(...),
  body: Placeholder(),
);

Theme

Import light and dark theme to your project:

import 'package:fluent2_kit/theme_data.dart' as theme_data;

final theme = theme_data.theme;
final darkTheme = theme_data.darkTheme;

Or you can pass your own brandColor:

Tip: use Smart Swatch Generator to generate your color palette.

import 'package:fluent2_kit/theme_data.dart';

const _brandColor = MaterialColor(
  0xFF7f22e2,
  <int, Color>{
    50: Color(0xFFf5e6ff),
    100: Color(0xFFd9bafa),
    200: Color(0xFFbf8df2),
    300: Color(0xFFa461eb),
    400: Color(0xFF8934e4),
    500: Color(0xFF701bcb),
    600: Color(0xFF57149f),
    700: Color(0xFF3e0e73),
    800: Color(0xFF250747),
  },
);

ThemeData get theme =>
    getTheme(brandColor: _brandColor, brightness: Brightness.light,);

ThemeData get darkTheme =>
    getTheme(brandColor: _brandColor, brightness: Brightness.dark);

And add the theme to your MaterialApp:


FluentProvider(
  child: MaterialApp(
    themeAnimationDuration: Duration.zero,
    darkTheme: darkTheme,
    themeMode: themeMode,
  ),
);

Fluent Icons

Import FluentIcons:

import 'package:fluent2_kit/fluent_icons.dart';

Design Tokens

Design tokens are stored values used to assign Fluent styles like color, typography, spacing, or elevation, without hardcoding pixels and hex codes.

CornerRadius

FluentThemeData includes predefined corner radius values. Use the FluentCornerRadius radius tokens to change the corner radius on elements. You can use the FluentContainer component, which is basically a Material Container with properties that are compatible with Fluent 2 UI design rules.

FluentContainer(
 cornerRadius: FluentCornerRadius.large,
)

Spacing Ramp

It’s used in every component and layout to create a familiar and cohesive product experience, regardless of device or environment. Use FluentSize values:

FluentContainer(
padding: EdgeInsets.symmetric(horizontal: FluentSize.size160.value),
)

Typography

The typography tokens are sets of global tokens that include font size, line height, weight and family. Use FluentText, a component created to accept Fluent typography tokens. So you get this value directly from the theme.

   // ✅
    FluentText(
      "Text 1",
      style: FluentThemeDataModel.of(context)
          .fluentTextTheme
          ?.body2,
    ),
    // ✅
    FluentText(
      "Text 2",
      style: FluentThemeDataModel.of(context)
          .fluentTextTheme
          ?.caption1,
    ),
    // ❌ use the typography tokens from the theme
    FluentText(
      "Wrong use",
      style: TextStyle(
        fontSize: 20,
        fontWeight: FontWeight.w800,
      ),
    )

If you need to change a text style value such as color, use fluentCopyWith():

FluentText(
 "Text",
 style: FluentThemeDataModel.of(context)
     .fluentTextTheme
     ?.body2?.fluentCopyWith(
   fluentColor: Colors.pink
 ),
)

Shadows

Fluent offers six sets of shadows, each consisting of two layers. We have 2 elevation ramps:

  • low elevation ramp (shadow2, shadow4, shadow8, shadow16)
  • high elevation ramp (shadow28, shadow64)

If you choose brand shadow tokens, the luminosity equation will be applied to the shadow color. You can use them choosing the value from the theme:

FluentContainer(
  shadow: FluentThemeDataModel.of(context).fluentShadowTheme?.shadow2,
  width: 100,
  height: 100,
),
FluentContainer(
  color: FluentColors.of(context)?.brandBackground1Rest,
  shadow: FluentThemeDataModel.of(context).fluentShadowTheme?.brandShadow64,
  width: 100,
  height: 100,
)

Stroke

FluentContainer has the strokeStyle property where you can pass stroke styles, and to create lines you can use FluentStrokeDivider:

FluentStrokeDivider(
 style: FluentStrokeStyle(color: Colors.blue,thickness: FluentStrokeThickness.strokeWidth20),
 startIndent: FluentStrokeBorderIndent.strokeIndent16,
)

You can choose the neutral strokes of the theme, the neutral strokes are: Accessible, Stroke1, Stroke2, StrokeDisabled.

FluentContainer(
  strokeStyle: FluentThemeDataModel.of(context).fluentStrokeTheme?.strokeAccessible,
  width: 100,
  height: 100,
)

Or you can pass the weight of the stroke, color, distribution, and endpoint properties.

FluentContainer(
  strokeStyle: FluentStrokeStyle(
    thickness: FluentStrokeThickness.strokeWidth15,
    color: Colors.red,
    padding: 2,
    dashArray: [
      2,1,2,1
    ]
  ),
  width: 100,
  height: 100,
)

Color Tokens

Use the FluentColors and FluentDarkColors classes. Interaction States: According to the documentation, the Fluent palettes are often used to indicate interaction states on components.

color: FluentColors.of(context)?.brandForeground1Selected
color: FluentColors.of(context)?.brandBackground1Pressed

Neutral Colors

These colors are used on surfaces, text, and layout elements. In Light Theme:

color: FluentColors.neutralBackground1Rest

In Dark Theme:

color: FluentDarkColors.neutralBackground1Rest

Brand colors

You pass the context, and the color dynamically adjusts for dark mode.

color: FluentColors.of(context)?.brandBackground1Rest
color: FluentColors.of(context).brandBackgroundTintRest
dark-theme-part1 dark-theme-part2
dark-theme-part1 dark-theme-part2

🧩 Components

Fluent Avatar

Types:

  • Standard: circular containers that generally represent an individual.
FluentAvatar(
  child: Image.asset(
    "assets/images/avatars/avatar1.jpeg",
    fit: BoxFit.cover,
    width: double.maxFinite,
    height: double.maxFinite,
  ),
),
  • Group: square containers that represent many people, like teams, organizations, or companies.
 FluentAvatar(
  isGroup: true,
  ...
)

Behavior

Presence Badges

There are 8 avatar badge variants: away, available, dnd, offline, unknown, busy, blocked, oof.

 FluentAvatar(
  statusPresenceBadge: StatusPresenceBadge.available,
  ...
)

Activity Rings

FluentAvatar(
  strokeStyle: FluentStrokeStyle(
    padding: 4,
    thickness: FluentStrokeThickness.strokeWidth30,
    color: Colors.purple
  ),
  child: Image.asset(
    "assets/images/avatars/avatar1.jpeg",
    fit: BoxFit.cover,
    width: double.maxFinite,
    height: double.maxFinite,
  ),
)

Cutout

Avatars at 40 and 56 pixels can display a cutout that communicates other dynamic information, like reactions, mentions, and recent file activity.

FluentAvatar(
  size: FluentAvatarSize.size56,
  cutoutSize: CutoutSize.size28,
  cutout: Icon(
    FluentIcons.heart_12_filled
  ),
  child: Image.asset(...),
)
FluentAvatar(
  size: FluentAvatarSize.size56,
  cutoutSize: CutoutSize.size20,
  cutout: Icon(
    FluentIcons.heart_12_filled,
    size: 16,
  ),
  child: Image.asset(...),
)

Fluent Button

Standard action button. title and onPressed are required; everything else is optional.

Sizes

The FluentButtonSize class has the following variations:

  • Large (52pt)
  • Medium (40pt, default)
  • Small (28pt)
FluentButton(
  title: "Click Me",
  size: FluentButtonSize.medium,
  onPressed: () {},
)

Variants

The FluentButtonVariant class contains the following variants:

  • Accent (default)
  • Outline Accent
  • Outline
  • Subtle
FluentButton(
  title: "Click Me",
  variant: FluentButtonVariant.accent,
  onPressed: () {},
)

Leading icon

Pass an Icon to render before the label.

FluentButton(
  title: "Like",
  icon: Icon(FluentIcons.heart_12_filled),
  onPressed: () {},
)

Disabled

Pass onPressed: null to render the disabled state.

FluentButton(
  title: "Click Me",
  onPressed: null,
)

Full width

Set isFullWidget: true to stretch the button to the available width.

FluentButton(
  title: "Continue",
  isFullWidget: true,
  onPressed: () {},
)

Fluent FAB

Floating action button (FAB) for a screen's primary action. Renders as a circular icon-only button by default, or as an extended pill when a label is provided.

Sizes

The FluentFabSize class has the following variations:

  • Large (56pt, default)
  • Small (48pt)

Variants

The FluentFabVariant class contains the following variants:

  • Accent (brand background, default)
  • Subtle (neutral background)
// Icon-only FAB
FluentFab(
  icon: FluentIcons.add_24_filled,
  onPressed: () {},
)

// Extended FAB with label
FluentFab(
  icon: FluentIcons.add_24_filled,
  label: "Create",
  onPressed: () {},
)

Fluent Navigation Bar

You can choose the brand or neutral variant in the Fluent NavBar.

FluentNavBar(
  themeColorVariation: FluentThemeColorVariation.brand,
)

Leading

FluentNavBar(
  leading: Icon(FluentIcons.leaf_two_32_filled),
)

Gradient

Supports the gradient property:

FluentNavBar(
  title: NavCenterTitle(title: "Title"),
  themeColorVariation: FluentThemeColorVariation.brand,
  gradient: LinearGradient(
    colors: [
      Colors.purple,
      Colors.deepPurple,
    ],
  ),
)

There are 4 variants for nav title: Left title, Left subtitle, Center title, Center

FluentNavBar(
  themeColorVariation: FluentThemeColorVariation.brand,
  title: NavLeftSubtitle(title: "Title", subtitle: "My subtitle",),
)

A list of widgets, the actions that will appear in the right side of nav bar:

FluentNavBar(
  themeColorVariation: FluentThemeColorVariation.brand,
  title: NavLeftSubtitle(title: "Title", subtitle: "My subtitle",),
  actions: [
    Icon(FluentIcons.airplane_24_regular),
    Icon(FluentIcons.access_time_20_regular),
    Icon(FluentIcons.sparkle_20_filled),
  ],
)

Fluent List

There are two variants and you can choose them using the named constructor:

  • OneLine
FluentList.oneLine(
  //  Here you can only pass a list of FluentListItemOneLine
  listItems: [...],
)
Screenshot 2024-02-20 at 16 07 11
  • MultiLine
FluentList.multiLine(
  //  Here you can only pass a list of FluentListItemMultiLine
  listItems: [...],
)
Screenshot 2024-02-20 at 16 06 47

FluentList has the following props:

  • sectionHeaderTitle

  • sectionHeaderTitleIcon: The leading icon of section header

  • sectionHeaderBackgroundColor

  • sectionHeaderTitleVariant: You can choose the bold or subtle value.

  • sectionHeaderActions: Receives a FluentSectionHeaderActions where you can pass two action widgets.

  • sectionDescriptionText

  • sectionDescriptionBackgroundColor

  • sectionDescriptionIcon: The icon that will appear in your section description.

  • separator: A separator to your list.

FluentList.multiLine(
  sectionHeaderTitle: "Notifications",
  sectionHeaderTitleVariant: SectionHeaderTitleVariant.bold,
  sectionHeaderActions: FluentSectionHeaderActions(
    action1: Icon(FluentIcons.circle_20_regular),
    action2: Icon(FluentIcons.circle_20_regular),
  ),
  sectionDescriptionText: "Choose how alerts appear on your device.",
  separator: FluentStrokeDivider(),
  sectionDescriptionIcon: FluentIcons.leaf_three_16_filled,
  listItems: [
    FluentListItemMultiLine(text: "Item 1"),
    FluentListItemMultiLine(text: "Item 2"),
  ],
)

Fluent Card

FluentCard(
  text: "Text",
  subText: "Subtext",
  leading: Image.asset(
    "assets/images/cards/card2.jpeg",
    fit: BoxFit.cover,
    width: double.maxFinite,
    height: double.maxFinite,
  ),
  coverImage: Image.asset(
    "assets/images/cards/card2.jpeg",
    fit: BoxFit.cover,
    width: double.maxFinite,
    height: double.maxFinite,
  ),
  onPressed: () {},
)

If you just want to use a flexible container with card styles, you can use FluentCardContainer:

FluentCardContainer(
  padding: EdgeInsets.all(FluentSize.size160.value),
  child: Text("Card content"),
)

Fluent Radio Button

FluentRadioButton<Option>(
  value: Option.option1,
  groupValue: _option,
  onChanged: (value) {},
)

Disabled:

FluentRadioButton<Option>(
    value: Option.option1,
    groupValue: _option,
    onChanged: null,
  )

Fluent Checkbox

FluentCheckbox(
  value: isCheckbox1,
  onChanged: (value) {},
)

Disabled:

FluentCheckbox(
  value: isCheckbox1,
  onChanged: null,
)

Fluent Switch Toggle

FluentSwitchToggle(
  value: showIcons,
  onChanged: (value) => setState(() {
    showIcons = value;
  }),
)

Disabled:

FluentSwitchToggle(
  value: showIcons,
  onChanged: null,
)

Fluent Segmented Control

Lets people pick one option from a small set of mutually exclusive choices. Use the named constructors .textItems or .iconItems.

Types

FluentSegmentedControlType values:

  • tabs (default): a sliding thumb inside a single pill track.
  • pillButton: separate, spaced pills.

Variants

FluentSegmentedControlVariant values:

  • neutral (default): neutral track with brand-colored selected segment.
  • brand: brand track with neutral-colored selected segment.

Text items

final controller = FluentSegmentedController<Sky>(value: Sky.midnight);

FluentSegmentedControl<Sky>.textItems(
  fluentController: controller,
  textItems: const {
    Sky.midnight: "Midnight",
    Sky.viridian: "Viridian",
    Sky.cerulean: "Cerulean",
  },
  onValueChanged: (value) {
    // handle selection
  },
)

Icon items (pill button, brand)

FluentSegmentedControl<ViewMode>.iconItems(
  segmentType: FluentSegmentedControlType.pillButton,
  variant: FluentSegmentedControlVariant.brand,
  iconItems: const {
    ViewMode.grid: FluentIcons.grid_20_regular,
    ViewMode.list: FluentIcons.list_20_regular,
  },
  onValueChanged: (value) {
    // handle selection
  },
)

Fluent Banner

final myBanner = FluentBanner(
      bannerColor: FluentBannerColor.accent,
      text: "Your changes were saved.",
    );

Adding Fluent Banner

FluentButton(
  title: "Open Banner",
  onPressed: () async {
    FluentScaffoldMessenger.of(context).addBanner(myBanner);
  },
)

Removing Fluent Banner

 FluentButton(
  title: "Close Banner",
  onPressed: () async {
    FluentScaffoldMessenger.of(context).removeBanner(myBanner);
  },
)

Fluent Toast

FluentToast has 4 variants of FluentToastColor:

  • Accent

  • Neutral

  • Danger

  • Warning

FluentButton(
  title: "Accent Toast",
  onPressed: (){
    FluentToast(
        toastColor: FluentToastColor.accent,
        text: "Fluent 2 is here",
        subText: "See what’s changed.",
        icon: Icon(FluentIcons.sparkle_20_filled),
        action: Builder(
          builder: (context) => IconButton(
            onPressed: () {
              FluentToastOverlayEntry.of(context).remove();
            },
            icon: Icon(Icons.cancel),
          ),
        )).show(
      context: context,
      duration: null,
      onDismissed: () {
        print("Closed!");
      },
    );
  },
)

Fluent Text Field

Underlined text input mirroring the Microsoft Fluent 2 iOS spec. Supports a floating label, hint (placeholder), assistive/error text, prefix and suffix icons, and an automatic trailing dismiss icon when the field is focused and not empty.

FluentTextField(
  label: "Last Name",
  hintText: "Ballinger",
  onChanged: (value) {},
  obscureText: false,
  readOnly: false,
  suffixIcon: Icon(FluentIcons.leaf_three_16_filled),
  hasError: error != null,
  assistiveText: error ?? "assistive",
)

Visual states

The component renders the six states defined by the Fluent 2 iOS spec:

// Filled — has value, not focused
FluentTextField(
  label: "Label",
  controller: FluentTextFieldController()..textEditingController.text = "Input text",
  assistiveText: "Assistive text",
  prefixIcon: Icon(FluentIcons.search_24_regular),
)

// Placeholder — empty, not focused
FluentTextField(
  label: "Label",
  hintText: "Hint text",
  assistiveText: "Assistive text",
  prefixIcon: Icon(FluentIcons.search_24_regular),
)

// Focused — empty, focused (label and underline turn brand blue)
FluentTextField(
  label: "Label",
  hintText: "Hint text",
  assistiveText: "Assistive text",
  prefixIcon: Icon(FluentIcons.search_24_regular),
  autofocus: true,
)

// Typing — has value, focused (auto-shows trailing dismiss icon)
FluentTextField(
  label: "Label",
  controller: FluentTextFieldController()..textEditingController.text = "Input text",
  assistiveText: "Assistive text",
  prefixIcon: Icon(FluentIcons.search_24_regular),
)

// Error — label, underline, and assistive text turn red
FluentTextField(
  label: "Label",
  controller: FluentTextFieldController()..textEditingController.text = "Input text",
  hasError: true,
  assistiveText: "Password must contain 8 characters and include letters, numbers and symbols",
  prefixIcon: Icon(FluentIcons.search_24_regular),
)

// Disabled — hint text uses the disabled neutral foreground (#BDBDBD)
FluentTextField(
  label: "Label",
  hintText: "Hint text",
  assistiveText: "Assistive text",
  prefixIcon: Icon(FluentIcons.lock_closed_24_regular),
  enabled: false,
)

Additional capabilities

FluentTextField also exposes the underlying Flutter TextField surface for advanced use cases:

  • Multi-line input: maxLines, expands, scrollController.
  • Character counter: maxLength, buildCounter.
  • Password / obscured input: obscureText, obscuringCharacter.
  • Prefix variants: prefixIcon (icon column on the left) and prefix (inline widget inside the input).
  • Cursor customization: cursorErrorColor, cursorHeight, custom magnifier via magnifierConfiguration.
  • Input behavior: keyboardType, inputFormatters, autocorrect, enableSuggestions, autofocus, canRequestFocus, clipBehavior, textAlign.
  • Callbacks: onChanged, onTap, onEditingComplete, onSubmitted.

Fluent Progress Bar

if (isUpdating)
  FluentProgressBar(
    value: null,
  )

Fluent Heads-up Display

 FluentButton(
    title: "Open HUD",
    onPressed: () {
      FluentHeadsUpDisplayDialog(
        future: Future.delayed(Duration(seconds: 1)),
        confirmStopMessage: "Are you sure you want to close this?",
        hud: FluentHeadsUpDisplay(
          text: "Refreshing Data...",
        ),
      ).show(context);
    },
  )

📚 Additional information

This is mainly inspired by the Fluent 2 iOS Figma UI Kit. It will be gradually adapted to Android as Microsoft releases its Figma UI Kit.

References

💛 Acknowledgments

Special thanks to the two main developers behind this project, whose work shaped most of the components and patterns in fluent2_kit:

Libraries

color_mode
fluent2_debug
fluent2_kit
Flutter components, design tokens, and themes for Fluent 2 mobile apps.
fluent_icons
theme_data
utils/debouncer