Material 3 vs FormaUI: a token-by-token comparison
Corner tokens, type scale, spacing and API surface side by side — including the Material 3 button shape that isn't themeable, and an honest list of when to use stock M3 instead.
- Material 3
- Design tokens
- Compose
I maintain FormaUI, so treat everything here with the suspicion that deserves. What I've tried to do instead of arguing is put the numbers side by side and let you decide — every value below is read from source, and the section on when to use stock Material 3 is not a formality.
Both of these are Material 3. FormaUI is a themed layer on top of androidx.compose.material3, not a replacement for it. So the comparison isn't "two design systems" — it's "Google's defaults versus a different set of defaults for the same components."
The one that isn't about taste
Start here, because it's the only difference that's a genuine API limitation rather than a preference.
Material 3's default button shape does not come from your theme. Button falls back to ButtonDefaults.shape, which reads FilledButtonTokens.ContainerShape → ShapeKeyTokens.CornerFull → CircleShape. It is hardcoded. Changing MaterialTheme.shapes does not change it.
This is a regression from Material 2, where Button used MaterialTheme.shapes.small and overriding that one value changed every button in your app. In M3 you get two options: pass shape to every button call site, or write a wrapper.
That second option is where a lot of "I'll just use Material 3" projects quietly turn into "I have a components package now."
FormaUI's equivalent reads from the theme:
object FormaButtonDefaults {
val shape: Shape
@Composable @ReadOnlyComposable
get() = FormaTheme.shapes.md
}
Change FormaShapes.md and every button follows. That's not cleverness — it's what Material 2 did, and it's a small example of the general pattern: an opinionated layer's real job is closing the gaps where the theming surface leaks.
Shapes
Stock M3 corner tokens versus FormaUI's, both from source:
| Slot | Material 3 | FormaUI |
|---|---|---|
| extraSmall | 4dp | 4dp |
| small | 8dp | 6dp |
| medium | 12dp | 8dp |
| large | 16dp | 12dp |
| extraLarge | 28dp | 16dp |
| Button default | CircleShape (pill), not themeable | 8dp, from the theme |
FormaUI is systematically tighter, and the gap widens as the shapes get bigger — 28dp versus 16dp at the top end. That single token is most of why a FormaUI card doesn't read as a Google card.
Neither is correct. M3's rounder shapes are friendlier and more playful; the tighter set reads as more editorial and more serious. If you're building a consumer app with a warm personality, M3's defaults may genuinely suit you better. If you're building a dashboard, a finance app, or anything where "playful" is wrong, 28dp corners are something you'd be overriding anyway.
FormaUI's tiers also feed M3's five slots, so raw MaterialTheme components sitting inside FormaTheme inherit the tighter corners without any wiring.
Typography
Material 3 numbers are the androidx type-scale tokens; FormaUI's are from its FormaTypography.
| Style | Material 3 | FormaUI |
|---|---|---|
| displayLarge | 57sp / 64 / 400 / −0.2sp | 64sp / 67 / 400 / −1.5sp |
| titleLarge | 22sp / 28 / 400 / 0 | 22sp / 29 / 500 / 0 |
| bodyLarge | 16sp / 24 / 400 / +0.5sp | 16sp / 25 / 400 / 0 |
| bodyMedium | 14sp / 20 / 400 / +0.2sp | 14sp / 22 / 400 / 0 |
| labelLarge | 14sp / 20 / 500 / +0.1sp | 14sp / 20 / 500 / 0 |
| labelSmall | 11sp / 16 / 500 / +0.5sp | 12sp / 17 / 500 / +1.5sp |
(size / line-height / weight / tracking)
Two patterns, and they're the whole aesthetic difference in numbers:
Tracking runs the opposite direction. M3 adds positive tracking to body and label text — letters pushed slightly apart, which reads as approachable and screen-friendly. FormaUI zeroes it and goes hard negative on the display tier. Tight headlines over neutral body is the signature of print editorial design, and −1.5sp at 64sp is a much stronger statement than −0.2sp at 57sp.
Body line-height is looser. 25 against 24 at bodyLarge, 22 against 20 at bodyMedium. Marginal per line, noticeable across a paragraph.
There's also a numeric style with fontFeatureSettings = "tnum, lnum" — real OpenType tabular figures, so digits don't jitter as values update. If you've ever watched a balance twitch as it refreshed, that's the fix. M3 has no equivalent; you'd add it yourself.
Font: stock M3 uses the platform default, which is Roboto on most devices. FormaUI bundles Public Sans. That's a genuine tradeoff and not automatically in FormaUI's favour — a bundled variable font is bytes in your APK, and Roboto is already on the device.
Spacing
Material 3 has no spacing scale. MaterialTheme takes colorScheme, typography and shapes. There is no dimension token system, no elevation semantics beyond per-component defaults, no density knob.
So most teams write one: a data class, a staticCompositionLocalOf, a provider, an accessor object. It's forty lines and everyone's is slightly different.
FormaUI ships a 4dp grid — xxs 4, xs 8, sm 12, md 16, lg 24, xl 32, xxl 48, section 96 — used internally by every component, and readable as FormaTheme.spacing.md.
Being straight about the limitation: that scale is currently a fixed contract, not a theme parameter. FormaTheme has no spacing argument. You can read the tokens; you can't swap them. If you need a 5dp or 10dp rhythm, FormaUI will fight you, and that's a real reason to write your own layer instead.
API surface
M3 gives you five top-level button composables — Button, ElevatedButton, FilledTonalButton, OutlinedButton, TextButton. FormaUI gives one with a variant:
FormaButton(
onClick = ::submit,
variant = FormaButtonVariant.Filled,
) { Text("Create account") }
That's a preference, not an improvement. A single entry point makes variants swappable by changing an argument and keeps one name to remember; five separate functions are more discoverable in autocomplete and let each carry its own parameter list. Pick whichever annoys you less.
What FormaUI adds that M3 doesn't have
Charts. androidx.compose.material3 ships none, so charting means a third-party library — MPAndroidChart, Vico, YCharts — each with its own theming that won't match your app. FormaUI includes bar, line and donut built on Compose Canvas with no extra dependency, themed with everything else, and each generates a screen-reader summary automatically: "Bar chart with 4 categories. Jan: 12. Feb: 32…" The chart libraries above give screen readers nothing.
They're presentation charts, though — no tooltips, no touch scrubbing. If you need an interactive chart, use a chart library.
A press interaction. A 0.97 scale dip layered over the ripple, applied via graphicsLayer so the measured touch target never shrinks. Exposed as a public modifier, so you can put it on your own components.
Picker sheets. Date, date-range and time pickers pre-composed into bottom sheets, which is the assembly step M3 leaves to you.
Where Material 3 straightforwardly wins
Not a formality. These are real and some of them are decisive.
It's first-party and permanent. Google maintains it, it ships with the platform, and it will outlive any third-party layer. FormaUI is one person. If I stop, you're maintaining a fork. That risk is not symmetric and no amount of test coverage changes it.
No dependency, no supply chain. M3 is already in your project.
Material 3 Expressive is coming to it, not to us. It shipped to Pixel phones in September 2025 and is still alpha-only in Compose, but when it stabilises it lands in androidx.compose.material3. A themed layer follows behind.
Dynamic colour is native. FormaUI supports it with a flag, but it's M3's feature.
Your designer may have already specified Material. If you have real designs in M3's language, adopting a different set of opinions means fighting both.
Every FormaUI API is @ExperimentalFormaUiApi. Pre-1.0, breaking changes expected. M3 is stable.
And most of FormaUI's 40 components are wrappers, not new work. 17 are a single forwarding call into an M3 composable — FormaSwitch is Switch with the arguments passed through — and 3 more are part forward. Another 11 are variant dispatch: a when over a Forma enum picking between M3 siblings, FormaButton over five M3 buttons. That leaves the three charts, the avatar, the empty state, the exposed dropdown and the three picker sheets. If you're counting components as value, count nine.
How to actually choose
Use stock Material 3 if the Google look suits your product, you have M3 designs already, you want zero dependencies, you need Expressive as soon as it stabilises, or you're building something that must outlive a solo maintainer's interest.
Use a themed layer like FormaUI if you want an app that doesn't read as a Google app, you'd rather not spend the first week writing a spacing scale and five button wrappers, tighter corners and editorial type match what you're building, and pre-1.0 APIs on a small project are an acceptable risk.
Write your own layer if you have a real design system to implement, someone else's opinions are worse than none, or you need spacing to be themeable — which, as noted, FormaUI currently doesn't allow.
Use a headless library like Compose Unstyled if you're building a design system from scratch and want behaviour without styling. That's a different, entirely legitimate bet, and it's the right tool for that job.
The honest summary is that FormaUI is a bet that most teams never actually customise their defaults, so shipping good ones beats shipping neutral ones. If that's wrong about you — if you know exactly what your app should look like — you want stock M3 and your own tokens, and you'll get there faster without me.
Values read from androidx.compose.material3 type-scale and shape tokens and from FormaUI's FormaTypography / FormaShapes / FormaSpacing. Material 3 token values track androidx main and may differ slightly from the exact stable release you're on — check yours if a number matters. Try FormaUI's components live and judge the result rather than the table.