Semantic tokens & theming
How theming works by semantic indirection — components reference roles, the theme resolves them, and swapping the theme reskins the whole app.
A theme is one FwTokens bundle: the 32 semantic colors, the radius set, the shadow scale, and
the typography (family names + tracking). Components never reference raw values — they reference
roles — and the active FwTokens resolves them. That indirection is the whole trick.
Semantic indirection
// A component references roles…
final t = context.fw;
const Text('Subscribe').tw.px(4).py(2).bg(t.colors.primary).text(t.colors.primaryForeground);Because the widget reads primary/primaryForeground (not a fixed color), swapping the active
FwTokens reskins it — along with every other role-using widget — from a single change. This is
the same model as shadcn's theming: change the theme, the
components follow.
Providing a theme
FwTheme is an InheritedWidget that publishes a bundle to everything below it; context.fw reads
it (falling back to the FwThemeExtension
bridge inside a MaterialApp).
FwTheme(tokens: FwTokens.light, child: const HomeScreen());FwTokens.light and FwTokens.dark are the built-in stock shadcn-neutral themes. Switching
between them is the host app's job — flutterwindcss just resolves whichever bundle is active.
Animating theme changes
Swap FwTheme for FwAnimatedTheme and every context.fw-styled descendant crossfades when the
tokens change — colors, radii, shadows, and typography all interpolate (FwTokens.lerp). Perfect for
a light ↔ dark toggle:
class App extends StatefulWidget {
const App({super.key});
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
bool _dark = false;
@override
Widget build(BuildContext context) {
return WidgetsApp(
color: const Color(0xFF000000),
builder: (context, _) => FwAnimatedTheme(
tokens: _dark ? FwTokens.dark : FwTokens.light,
duration: const Duration(milliseconds: 200),
child: HomeScreen(onToggleDark: () => setState(() => _dark = !_dark)),
),
);
}
}Using a custom (pasted) theme
To use any tweakcn/shadcn theme instead of the stock one, generate a theme.dart with the
Theme generator — paste the CSS, copy the output. It defines two
const FwTokens (lightTheme / darkTheme) you hand straight to FwTheme / FwAnimatedTheme:
import 'theme.dart'; // generated
FwAnimatedTheme(tokens: isDark ? darkTheme : lightTheme, child: const HomeScreen());Every color in a FwTokens is required — there are no defaults — so a generated theme always
carries all 32 roles. That's why a pasted theme round-trips with nothing dropped.
You're not forced into a theme
Semantic tokens (context.fw.colors.primary) are opt-in. The .tw utilities that take raw
values need no theme at all — so you can use flutterwindcss with the raw palette
and your own fonts, no FwTheme and no generated theme.dart:
// No FwTheme anywhere — raw palette + a literal font.
Text('Hi').tw
.px(4).py(2)
.bg(FwPalette.blue.shade600)
.text(FwPalette.slate.shade50)
.rounded(8)
.font('Inter');- Raw palette colours, spacing, sizing, radius, borders, effects, transforms — all take literal values; no theme required.
font('Family')sets a literal font family; no theme required (see Fonts).- The theme-role sugars (
fontSans/fontSerif/fontMono,roundedMd/shadowMd) prefer a theme, but fall back to the stockFwTokens.lightdefaults when none is present — they never force a theme on you. - Reading tokens where a theme is optional? Use
context.fwOrNull(returnsnullinstead of throwing). Onlycontext.fw(and semantic tokens) require a theme.
This is also why the engine works under a MaterialApp — provide tokens through an
FwThemeExtension and the same
components resolve through it; bring your own fonts via the Material textTheme.
Next steps
Colors
Two layers — the raw baked Tailwind palette, and theme-resolved semantic color roles. Components use roles so a theme swap reskins everything.
Fonts
How to wire the fonts a generated theme.dart names — bundle them or use google_fonts. flutterwindcss applies the theme's families automatically; you only register the font.