All posts

Minimal Flutter UI kits compared: fossui vs Material (2026)

July 25, 2026

Every Flutter app starts with Material, because Material is what ships in the box. It is complete, first-party, and battle-tested. It also means most Flutter apps look like each other.

fossui is a minimal alternative: a small component set built on package:flutter/widgets.dart that reads its own theme and carries one runtime dependency. This post compares the two on the two things you actually weigh when you pick a UI kit: how it looks, and how the API feels to write. Numbers here come from our own component-by-component audit, so treat them as one opinion with the receipts attached, not a neutral benchmark. The screenshots are the same widgets rendered both ways, so you can judge the look yourself. Toggle the site theme and they follow.

Short version: Material wins on maturity, breadth, and accessibility depth. fossui wins on default look, API shape, and footprint. The rest of this post shows where each of those is true.

How it looks

Same screen, same content, rendered with each kit. The only variable is the design system.

The same sign-up card in Material and fossui

Material 3 gives you tinted surfaces, a seeded color scheme (the purple is the default), rounded-pill buttons, and filled input boxes. fossui renders a neutral palette, superellipse (squircle) corners, a mono primary button, and tighter outlined inputs. Neither is wrong. They are different houses.

What follows is every component, rendered both ways. Each image swaps with the site theme, so flip the toggle to see the dark pair.

Buttons and toggles

Buttons. Material spreads emphasis across five classes; fossui folds it into one FossButton with a variant.

Material button classes next to fossui button variants
FilledButton(onPressed: save, child: const Text('Save'));
OutlinedButton(onPressed: cancel, child: const Text('Cancel'));
FossButton(onPressed: save, child: const Text('Save'));
FossButton(
  variant: FossButtonVariant.outline,
  onPressed: cancel,
  child: const Text('Cancel'),
);

Toggle. Material has no toggle button, so the nearest is a selectable IconButton; fossui's FossToggle holds a pressed state and joins into a bar.

Material selectable icon button next to a fossui toggle

Toggle group. Material's SegmentedButton is fixed-shape; fossui's group is a set of connectable toggles.

Material segmented button next to a fossui toggle group

Text and layout

Text. Material reads the type scale from TextTheme; FossText reads named token sizes and works under any app.

Material text styles next to fossui text sizes

Card. Material's Card is a bare surface you fill; FossCard has title/description/footer slots.

A Material card next to a fossui card

Separator. Material splits Divider and VerticalDivider; FossSeparator is one widget for both orientations.

A Material divider next to a fossui separator

Tabs. Material underlines the active tab and assembles three objects; fossui's segmented tabs sit in one self-contained widget.

Material tabs next to fossui segmented tabs

Inputs

Text field. Material fills the field and floats the label into the border; fossui keeps the label above with a lighter outline.

Material and fossui text fields with helper and error states
TextField(
  decoration: InputDecoration(
    labelText: 'Email',
    helperText: 'We never share it.',
    prefixIcon: const Icon(Icons.mail_outline),
    errorText: error,
  ),
);
FossTextField(
  label: 'Email',
  helperText: 'We never share it.',
  leading: const Icon(LucideIcons.mail),
  errorText: error,
);

Number field. Material has no number field, so you assemble a text field and steppers; fossui ships FossNumberField.

A hand-built Material stepper next to a fossui number field

OTP field. Material has no OTP field; fossui ships one with grouping, paste, and validation.

Hand-built Material OTP boxes next to a fossui OTP field

Select. Both look like a field closed; fossui keeps the trigger a button and offers multi-select as a separate widget.

A Material dropdown menu next to a fossui select
DropdownMenu<String>(
  initialSelection: 'pro',
  label: const Text('Plan'),
  dropdownMenuEntries: const [
    DropdownMenuEntry(value: 'free', label: 'Free'),
    DropdownMenuEntry(value: 'pro', label: 'Pro'),
  ],
);
FossSelect<String>(
  label: 'Plan',
  value: plan,
  onChanged: (v) => setState(() => plan = v),
  items: const [
    FossSelectItem(value: 'free', label: 'Free'),
    FossSelectItem(value: 'pro', label: 'Pro'),
  ],
);

Combobox. Material folds search into the dropdown; fossui keeps a dedicated filtered combobox.

A Material filtering dropdown next to a fossui combobox

Slider. Material's track is thick with a large knob; fossui uses a thin track and a white knob.

A Material slider next to a fossui slider

Selection

Checkbox. Material tints the box; fossui is monochrome and folds in the label and description.

Material checkboxes next to fossui checkboxes

Radio. Material tints the dot; fossui folds label and description into each radio while the group owns the value.

A Material radio group next to a fossui radio group

Switch. Material tints the track; fossui is monochrome and adds drag and a press squish.

A Material switch next to a fossui switch

Dates

Calendar. Both render a month grid; fossui applies the neutral palette and a squircle selection.

A Material calendar next to a fossui calendar

Date picker. Material opens showDatePicker from a button; fossui's trigger is a field with a controllable open state.

A Material date button next to a fossui date picker field

Disclosure and dialogs

Accordion. Material wraps the panels in a tinted card with a divider; fossui uses a flat hairline and no surface.

Material expansion tiles next to a fossui accordion

Dialog. Material's dialog is tinted and high-elevation; fossui's is flatter with a divided footer bar.

A Material dialog next to a fossui dialog

Alert dialog. Same split, sharper intent: Material's AlertDialog is dismissible, fossui's requires an action to close and puts destructive in red.

A Material alert dialog next to a fossui alert dialog

Feedback and status

Alert. Material's banner is top-pinned via ScaffoldMessenger; FossAlert is an inline callout with five status variants.

A Material banner next to a fossui alert

Badge. Material's Badge is a notification dot and Chip is heavier; fossui's badge carries the status in its color.

Material chips next to fossui status badges

Progress. Material's bar is bare; FossProgress adds a label and value row and animates changes.

A Material progress bar next to a fossui progress bar

Meter. Material only has a 0..1 bar; FossMeter is a bounded gauge with a label and value.

A repurposed Material bar next to a fossui meter

Spinner. Material sweeps a two-phase arc; FossSpinner is a steady indeterminate spin.

Material progress spinners next to fossui spinners

Skeleton. Material has no skeleton, so you hand-roll placeholders or add a package; fossui ships FossSkeleton.

Hand-built Material placeholders next to a fossui skeleton

Avatar. Material's CircleAvatar needs manual image and error wiring; FossAvatar handles absent, loading, and error automatically.

Material circle avatars next to fossui avatars

How the API feels

Look is taste. The API is where you live all day. Five differences come up again and again across the component set.

One widget and a variant, not many classes

Material models emphasis as separate types. You pick the class up front:

FilledButton(onPressed: f, child: t);       // high emphasis
FilledButton.tonal(onPressed: f, child: t); // medium
OutlinedButton(onPressed: f, child: t);     // low
TextButton(onPressed: f, child: t);         // lowest

fossui has one button and a variant enum:

FossButton(onPressed: f, child: t);                                  // primary
FossButton(variant: FossButtonVariant.secondary, onPressed: f, ...);
FossButton(variant: FossButtonVariant.outline, onPressed: f, ...);
FossButton(variant: FossButtonVariant.ghost, onPressed: f, ...);

The same pattern repeats: tabs are segmented or underline on one FossTabs, badges carry their status as a variant, dialogs switch footer treatment with an enum. Material tends to spread; fossui tends to fold.

Color comes from a token, not a ColorScheme

Material components read ColorScheme and assume a MaterialApp ancestor. fossui components read semantic roles from context.fossTheme, which resolves under a MaterialApp, a CupertinoApp, or a bare WidgetsApp:

final c = context.fossTheme.colors;
c.primary;      // role, not a raw color
c.destructive;
c.border;

Light and dark fall out of the same roles. Because nothing reaches into Material, the components drop into a Cupertino or bare app unchanged.

One style object, not scattered props

Material spreads per-instance styling across loose constructor arguments and large theme objects. fossui gives each component a single style bundle with nullable fields that merge over the theme-resolved defaults:

FossButton(
  style: const FossButtonStyle(borderRadius: 4),
  onPressed: f,
  child: const Text('Square'),
)

The trade is deliberate: fewer knobs per instance, because the intended path is to retheme globally, not to restyle at each call site. That keeps a hundred buttons looking like one design.

Controlled or uncontrolled, with an honest value type

Selection widgets (tabs, radio, checkbox, select) all take value + onChanged for controlled use or an initialValue for uncontrolled, with a value type that says what it is: T for single, Set<T> for multi.

FossTabs<String>(
  value: _tab,
  onChanged: (v) => setState(() => _tab = v),
  tabs: const [...],
)

Material sometimes splits this across widget types or leaves the wiring to you (TabBar + TabBarView + TabController, three objects you assemble).

Zero dependencies, glyphs included

fossui takes no icon package and paints the common glyphs itself: the button loading spinner, the checkbox tick, the radio dot, the accordion chevron, the dialog close. The whole kit is one runtime dependency. Material is first-party, so "dependency" is the wrong axis for it, but it does require a Material app and theme; fossui does not.

The scorecard

From our own audit, scored 0 to 10 per dimension, averaged across the 28 components that have a full scorecard. It is our scoring of our own library, so read it as a pointer, not a verdict.

DimensionfossuiMaterial
Default look8.66.3
API surface8.26.6
Dependencies105.8
Accessibility8.27.3

Every component, mapped

fossui ships 32 components. Here is each one against its Material counterpart. Of the 32, 26 have a Material equivalent (a direct one or a near miss) and 6 have none. "Parity" is match when Material has a real equivalent, partial when it has something close but different in purpose or shape, and none when it has nothing.

fossuiMaterialParityDifference
FossButtonFilledButton / OutlinedButton / TextButton / ElevatedButtonmatchFive classes vs one widget with a variant enum, plus a built-in loading state.
FossTextFieldTextField / TextFormFieldmatchLabel, box, and caption folded into one widget with a size axis; Material splits editing from InputDecoration.
FossTextText + TextThemematchReads the token type scale under any app; Material couples to TextTheme.
FossCardCardmatchHeader/content/footer slots vs a bare surface you fill yourself.
FossCheckbox / FossCheckboxGroupCheckbox / CheckboxListTilematchStandalone, group, and item in one API; Material has no group widget.
FossRadioGroupRadio / RadioGroup / RadioListTilematchGroup plus card layout with folded label and error; Material has no card or group.
FossSwitchSwitchmatchAdds drag/flick and a press squish; Material is tap-only.
FossSliderSlidermatchSingle-thumb fossui look; Material's two-thumb RangeSlider is a separate widget.
FossTabsTabBar + TabBarView + TabControllermatchOne self-contained widget (segmented or underline); Material assembles three objects.
FossSelect / FossMultiSelectDropdownButton / DropdownMenumatchMulti-select is a real widget (value is a Set<T>); Material has none.
FossCombobox / FossAutocompleteAutocomplete / DropdownMenumatchOne filtered-popup core across three faces; Material's Autocomplete is raw builders.
FossAccordionExpansionTile / ExpansionPanelListmatchOne group with a shared open Set; Material splits a single tile from a caller-held list.
FossDialogshowDialog / Dialog / AlertDialogmatchShared surface with bare/filled footer variants; Material splits across classes.
FossAlertDialogAlertDialogmatchNon-dismissible with required actions; Material is dismissible, actions optional.
FossDrawerDrawer / showModalBottomSheetmatchFour edges in one API with drag-to-dismiss; Material splits side drawer from bottom sheet.
FossTooltipTooltipmatchHover, keyboard focus, and long-press on four sides; Material is hover/long-press, top or bottom.
FossAvatarCircleAvatarmatchAutomatic absent/loading/error fallback; Material needs the wiring by hand.
FossSeparatorDivider / VerticalDividermatchOne widget for both orientations; Material splits into two.
FossProgressLinearProgressIndicatormatchDeterminate with a label row and animated value; Material also folds in indeterminate.
FossCalendarCalendarDatePickermatchSingle, multiple, and range in one widget; Material splits the APIs.
FossDatePickershowDatePickermatchModal with a controllable open state (sheet or centered); Material returns a Future.
FossAlertMaterialBannerpartialInline five-status callout with a live region; Material's banner is top-pinned with no status tints.
FossBadgeBadge / ChippartialA status pill; Material's Badge is a notification dot and Chip is a heavier interactive control.
FossPopoverPopupMenuButton / OverlaypartialController-driven anchored panel with viewport flip and focus trap; Material has no first-class popover.
FossSpinnerCircularProgressIndicatorpartialIndeterminate-only constant spin; Material also does a determinate arc.
FossToast / FossToasterSnackBarpartialQueues up to three with status and title/description; SnackBar is one-at-a-time, bottom-pinned.
FossMeterLinearProgressIndicatornoneA bounded gauge with arbitrary min/max and a label; Material only has a 0..1 bar.
FossNumberFieldnonenoneMaterial has none; you assemble a TextField, steppers, and clamp logic each time.
FossOtpFieldnonenoneNo first-party Material OTP field; fossui ships paste, autofill, masking, and validation.
FossSkeletonnonenoneNot in Material; you reach for a shimmer package or hand-roll an animation loop.
FossTogglenonenoneMaterial has no toggle button that holds a pressed boolean.
FossToggleGroupnonenoneMaterial's SegmentedButton is a different, fixed-shape control.

Six components have no Material counterpart at all: FossMeter, FossNumberField, FossOtpField, FossSkeleton, FossToggle, and FossToggleGroup. In Material you build these by hand or add a third-party package.

Going the other way, some Material widgets are deliberately not in fossui: the two-thumb RangeSlider, the interactive Chip, SegmentedButton, Stepper, DataTable, and FloatingActionButton, plus app-shell pieces like Scaffold and AppBar. fossui is components only; the app shell stays yours.

Where Material wins

This is not a clean sweep, and pretending it is would be dishonest.

  • First-party and mature. Material ships with Flutter. Its accessibility, keyboard handling, and animation tuning have had years of real-world feedback. On the raw accessibility depth of dialogs, selects, sliders, and checkboxes, Material still leads.
  • Breadth. RangeSlider, fullscreen dialogs, the floating-label animation, a multi-line text field, richer slider and button theming: Material has them today. fossui defers many of these on purpose until someone needs them, which means it does not have them yet.
  • Ecosystem. Every Flutter tutorial, Stack Overflow answer, and package assumes Material. That gravity is real and worth something.

When to pick which

Pick Material if you want the first-party default, the widest set of built-in variants, the most hardened accessibility, and the largest ecosystem behind you. For a lot of apps that is the right call.

Pick fossui if you want a distinct, neutral look out of the box, theming that is not welded to MaterialApp, a tiny footprint, and an API that folds instead of spreads. It is young and mobile-first: web and desktop compile and should work but are less exercised, and it covers the common ground rather than everything Material has. If that fits, it stops your app from looking like every other Flutter app.