Themes
Define consistent visual styles across your Flutter application with Forui's theming system.
Forui themes allow you to define a consistent visual style across your application & widgets. It optionally relies on the CLI to generate themes and styles that can be directly modified in your project.
Getting Started
Theme Brightness
Forui does not manage the theme brightness (light or dark) automatically.
You need to specify the theme explicitly in FTheme(...).
1@override2Widget build(BuildContext context) => FTheme(3 data: FThemes.neutral.light, // or FThemes.neutral.dark4 child: const FScaffold(child: Placeholder()),5);6Forui includes predefined themes that can be used out of the box. They are heavily inspired by shadcn/ui.
| Theme | Light Accessor | Dark Accessor |
|---|---|---|
Neutral | FThemes.neutral.light | FThemes.neutral.dark |
Zinc | FThemes.zinc.light | FThemes.zinc.dark |
Slate | FThemes.slate.light | FThemes.slate.dark |
Blue | FThemes.blue.light | FThemes.blue.dark |
Green | FThemes.green.light | FThemes.green.dark |
Orange | FThemes.orange.light | FThemes.orange.dark |
Red | FThemes.red.light | FThemes.red.dark |
Rose | FThemes.rose.light | FThemes.rose.dark |
Violet | FThemes.violet.light | FThemes.violet.dark |
Yellow | FThemes.yellow.light | FThemes.yellow.dark |
Theme Components
There are 7 core components in Forui's theming system.
FTheme: The root widget that provides the theme data to all widgets in the subtree.FThemeData: Main class that holds:FColors: Color scheme including primary, foreground, and background colors.FTypography: Typography settings including font family and text styles.FStyle: Misc. options such as border radius and icon size.FVariants: Maps variant constraints, e.g. hovered and pressed, to values.- Individual widget styles.
- Individual widget motions.
A BuildContext extension allows FThemeData can be accessed via context.theme:
1@override2Widget build(BuildContext context) {3 final FThemeData theme = context.theme;4 final FColors colors = context.theme.colors;5 final FTypography typography = context.theme.typography;6 final FStyle style = context.theme.style;78 return const Placeholder();9}10Colors
The FColors class contains the theme's color scheme. Colors come in pairs - a main color and its corresponding
foreground color for text and icons.
For example:
primary(background) +primaryForeground(text/icons)secondary(background) +secondaryForeground(text/icons)destructive(background) +destructiveForeground(text/icons)
1@override2Widget build(BuildContext context) {3 final colors = context.theme.colors;4 return ColoredBox(5 color: colors.primary,6 child: Text(7 'Hello World!',8 style: TextStyle(color: colors.primaryForeground),9 ),10 );11}12Hovered and Disabled Colors
To create hovered and disabled color variants, use the FColors.hover
and FColors.disable methods.
Typography
The FTypography class contains the theme's typography settings, including the default font family and various text
styles.
The TextStyles in FTypography are based on Tailwind CSS Font Size.
For example, FTypography.sm is the equivalent of text-sm in Tailwind CSS.
FTypography's text styles only specify fontSize and height. Use copyWith() to add colors and other properties:
1@override2Widget build(BuildContext context) {3 final typography = context.theme.typography;45 return Text(6 'Hello World!',7 style: typography.xs.copyWith(8 color: context.theme.colors.primaryForeground,9 fontWeight: .bold,10 ),11 );12}13Custom Font Family
Use the copyWith() method to change the default font family. As some fonts may have different sizes, the scale()
method is provided to quickly scale all the font sizes.
1@override2Widget build(BuildContext context) => FTheme(3 data: FThemeData(4 colors: FThemes.neutral.light.colors,5 typography: FThemes.neutral.light.typography6 .copyWith(xs: const TextStyle(fontSize: 12, height: 1))7 .scale(sizeScalar: 0.8),8 ),9 child: const FScaffold(child: Placeholder()),10);11Style
The FStyle class defines the theme's miscellaneous styling options such as the default border radius and icon size.
1@override2Widget build(BuildContext context) {3 final colors = context.theme.colors;4 final style = context.theme.style;56 return DecoratedBox(7 decoration: BoxDecoration(8 border: .all(color: colors.border, width: style.borderWidth),9 borderRadius: style.borderRadius,10 color: colors.primary,11 ),12 child: const Placeholder(),13 );14}15Variants
FVariants lets you define a base value with optional overrides for specific variant constraints.
This is useful for expressing a wide range of styling concepts:
- User interaction states, e.g. hovered, pressed.
- Semantic states, e.g. disabled, error.
- Stylistic variants, e.g. destructive and outlined buttons.
- Platform differences, e.g. touch vs desktop.
Each widget defines its own variant type, e.g. FTappableVariant and FCalendarVariant, ensuring only valid variants
can be used. Constraints are composed using .and(...) and .not(...):
1FVariants(2 // base (default if no variants match)3 const BoxDecoration(color: Colors.white),4 variants: {5 // NOT hovered6 [.not(.hovered)]: const BoxDecoration(color: Colors.red),7 // hovered OR pressed8 [.hovered, .pressed]: const BoxDecoration(color: Colors.grey),9 // disabled AND pressed10 [.disabled.and(.pressed)]: const BoxDecoration(color: Colors.black26),11 },12);13Variants can also be expressed as deltas (modifications) applied to a base value:
1FVariants.from(2 // base (default if no variants match)3 const BoxDecoration(4 color: Colors.white,5 borderRadius: .all(.circular(8)),6 ),7 variants: {8 // NOT hovered - keeps border radius9 [.not(.hovered)]: const .delta(color: Colors.red),10 // hovered OR pressed - keeps border radius11 [.hovered, .pressed]: const .delta(color: Colors.grey),12 // disabled AND pressed - keeps border radius13 [.disabled.and(.pressed)]: const .delta(color: Colors.black26),14 },15);16Resolution uses a tiered most-specific-wins strategy which is deterministic and order-independent.
Each variant belongs to one of three tiers:
| Tier | Category | Examples |
|---|---|---|
| 2 | Semantic | disabled, selected, error |
| 1 | Interaction | hovered, focused, pressed |
| 0 | Platform | android, iOS, web |
Higher tiers always take precedence.
For example, given the states {.disabled, .pressed}, .disabled.and(.pressed) wins over .pressed because disabled
is a tier 2 (semantic) state which outranks tier 1 (interaction) states.
To learn how to customize FVariants, see the customizing widget styles
guide.
Material Interoperability
Forui provides 2 ways to convert FThemeData
to Material's ThemeData.
This is useful when:
- Using Material widgets within a Forui application.
- Maintaining consistent theming across both Forui and Material components.
- Gradually migrating from Material to Forui.
A Forui theme can be converted to a Material theme using
toApproximateMaterialTheme().
The mapping is done on a best-effort basis, may not capture all nuances, and can change without prior warning.
1import 'package:flutter/material.dart';23import 'package:forui/forui.dart';45@override6Widget build(BuildContext context) => MaterialApp(7 theme: FThemes.neutral.light.toApproximateMaterialTheme(),8 home: Scaffold(9 body: Center(10 child: FCard(11 title: const Text('Mixed Widgets'),12 subtitle: const Text('Using both Forui and Material widgets together'),13 child: ElevatedButton(14 onPressed: () {},15 child: const Text('Material Button'),16 ),17 ),18 ),19 ),20);21Alternatively, you can generate a copy of toApproximateMaterialTheme() inside your project using the CLI:
dart run forui snippet create material-mappingThis is preferred when you want to fine-tune the mapping between Forui and Material themes, as it allows you to modify the generated mapping directly to fit your design needs.