Themes
Forui themes allow you to define a consistent visual style across your application & widgets. It relies on the CLI to generate themes and styles that can be directly modified in your project.
Getting Started
Forui does not manage the theme brightness (light or dark) automatically.
You need to specify the theme explicitly in FTheme(...)
.
@override
Widget build(BuildContext context) => FTheme(
data: FThemes.zinc.light, // or FThemes.zinc.dark
child: FScaffold(...),
);
Forui includes predefined themes that can be used out of the box. They are heavily inspired by shadcn/ui .
Theme | Light Accessor | Dark Accessor |
---|---|---|
Zinc | FThemes.zinc.light | FThemes.zinc.dark |
Slate | FThemes.slate.light | FThemes.slate.dark |
Red | FThemes.red.light | FThemes.red.dark |
Rose | FThemes.rose.light | FThemes.rose.dark |
Orange | FThemes.orange.light | FThemes.orange.dark |
Green | FThemes.green.light | FThemes.green.dark |
Blue | FThemes.blue.light | FThemes.blue.dark |
Yellow | FThemes.yellow.light | FThemes.yellow.dark |
Violet | FThemes.violet.light | FThemes.violet.dark |
Theme Components
There are 5 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 individual widget styles and the following: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.
The included BuildContext
extension allows FThemeData
can be accessed via context.theme
:
@override
Widget build(BuildContext context) {
final FThemeData theme = context.theme;
final FColors colors = context.theme.colors;
final FTypography typography = context.theme.typography;
final FStyle style = context.theme.style;
return const Placeholder();
}
Colors
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)
@override
Widget build(BuildContext context) {
final colors = context.theme.colors;
return ColoredBox(
color: colors.primary,
child: Text(
'Hello World!',
style: TextStyle(color: colors.primaryForeground),
),
);
}
Hovered 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 TextStyle
s 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:
@override
Widget build(BuildContext context) {
final typography = context.theme.typography;
return Text(
'Hello World!',
style: typography.xs.copyWith(
color: context.theme.colors.primaryForeground,
fontWeight: FontWeight.bold,
),
);
}
Custom 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.
@override
Widget build(BuildContext context) => FTheme(
data: FThemeData(
colors: FThemes.zinc.light.colors,
typography: FThemes.zinc.light.typography.copyWith(
defaultFontFamily: 'Roboto',
).scale(sizeScalar: 0.8),
),
child: const FScaffold(...),
);
Style
The FStyle
class the theme’s miscellaneous styling options such as the default border radius and icon size.
@override
Widget build(BuildContext context) {
final colors = context.theme.colors;
final style = context.theme.style;
return DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
color: colors.border,
width: style.borderWidth,
),
borderRadius: style.borderRadius,
color: colors.primary,
),
child: const Placeholder(),
);
}
FWidgetStateMap
FWidgetStateMap
lets you define
different values based on WidgetState
combinations such as
hovered & pressed
and focused | disabled
.
This is useful for describing how widgets should respond to user interaction.
Each combination is also known as a constraint. Constraints are evaluated from top to bottom. The first matching will be used. In general, more specific constraints should be placed above more general ones.
In the following example, given the states {hovered, pressed}
, the 1st constraint will always match.
FWidgetStateMap({
// ❌ Don't declare more general constraints above more specific ones!
// 1st constraint: applied when the accordion is hovered.
WidgetState.hovered: typography.base.copyWith(
fontWeight: FontWeight.w500,
color: colors.foreground,
decoration: TextDecoration.underline,
),
// 2nd constraint: applied when the accordion is hovered OR pressed.
WidgetState.hovered | WidgetState.pressed: typography.base.copyWith(
fontWeight: FontWeight.w500,
color: colors.foreground,
decoration: TextDecoration.underline,
),
// 3rd constraint: This text style is applied when the accordion is NOT hovered OR pressed.
WidgetState.any: typography.base.copyWith(
fontWeight: FontWeight.w500,
color: colors.foreground,
),
});
Most FWidgetStateMap
fields only support a subset of states. For example, FButtonStyle.decoration
only supports the disabled, hovered, pressed and focused states. A field’s supported states are documented in its API docs .
Customization
Themes
The following section demonstrates how to use the CLI generate a theme and widget style that you can directly modify to fit your design needs.
We use FAccordionStyle
as an example, but the same principles apply to all Forui widgets.
Generate main.dart
Navigate to your project directory.
Run to generate a main.dart
:
dart run forui init
This generates a main.dart
file where you will add your generated theme:
import 'theme/theme.dart';
void main() {
runApp(const Application());
}
class Application extends StatelessWidget {
const Application({super.key});
@override
Widget build(BuildContext context) {
// Assign the generated theme to `theme`.
final theme = zincLight;
return MaterialApp(
localizationsDelegates: FLocalizations.localizationsDelegates,
supportedLocales: FLocalizations.supportedLocales,
builder: (_, child) => FTheme(data: theme, child: child!),
theme: theme.toApproximateMaterialTheme(),
home: const FScaffold(
// TODO: replace with your widget.
child: Placeholder(),
),
);
}
}
Generate a Theme
Run to generate a theme based on zinc’s light variant:
dart run forui theme create zinc-light
This generates a theme file which you can:
- add to your generated
main.dart
. - add the generated styles to.
import 'accordion_style.dart';
FThemeData get zincLight {
const colors = FColors(
brightness: Brightness.light,
barrier: Color(0x33000000),
background: Color(0xFFFFFFFF),
foreground: Color(0xFF09090B),
...
);
final typography = _typography(colors: colors);
final style = _style(colors: colors, typography: typography);
return FThemeData(
colors: colors,
typography: typography,
style: style,
// Add your generated styles here.
accordionStyle: accordionStyle(colors: colors, typography, typography, style: style),
);
}
FTypography _typography({
required FColors colors,
String defaultFontFamily = 'packages/forui/Inter',
}) => FTypography(
xs: TextStyle(
color: colors.foreground,
fontFamily: defaultFontFamily,
fontSize: 12,
height: 1,
),
sm: TextStyle(
color: colors.foreground,
fontFamily: defaultFontFamily,
fontSize: 14,
height: 1.25,
),
...
);
FStyle _style({required FColors colors, required FTypography typography}) => FStyle(
formFieldStyle: FFormFieldStyle.inherit(
colors: colors,
typography: typography,
),
focusedOutlineStyle: FFocusedOutlineStyle(
color: colors.primary,
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
iconStyle: IconThemeData(color: colors.primary, size: 20),
tappableStyle: FTappableStyle(),
...
);
Generate a Style
Run to generate a FAccordionStyle
:
dart run forui style create accordion
This generates a accordion style file which you can add to your theme:
FAccordionStyle accordionStyle({
required FColors colors,
required FTypography typography,
required FStyle style,
}) => FAccordionStyle(
titleTextStyle: FWidgetStateMap({
// This text style is applied when the accordion is hovered OR pressed.
WidgetState.hovered | WidgetState.pressed: typography.base.copyWith(
fontWeight: FontWeight.w500,
color: colors.foreground,
decoration: TextDecoration.underline,
),
// This text style is applied when the accordion is NOT hovered OR pressed.
WidgetState.any: typography.base.copyWith(
fontWeight: FontWeight.w500,
color: colors.foreground,
),
}),
childTextStyle: typography.sm.copyWith(color: colors.foreground),
// This decoration is ALWAYS applied.
iconStyle: FWidgetStateMap.all(
IconThemeData(color: colors.primary, size: 20),
),
focusedOutlineStyle: style.focusedOutlineStyle,
dividerStyle: FDividerStyle(color: colors.border, padding: EdgeInsets.zero),
tappableStyle: style.tappableStyle.copyWith(
animationTween: FTappableAnimations.none,
),
titlePadding: const EdgeInsets.symmetric(vertical: 15),
childPadding: const EdgeInsets.only(bottom: 15),
animationDuration: const Duration(milliseconds: 200),
);
See FWidgetStateMap
for more information on FWidgetStateMap
s.
Individual Widget Styles
The following sections demonstrate how to override an accordion’s style.
Generate the Style
Run to generate a widget style:
dart run forui style create accordion
Modify the Style
This example shows how to add underlining when the accordion title is focused, in addition to the existing hover and press states:
FAccordionStyle accordionStyle({
required FColors colors,
required FTypography typography,
required FStyle style,
}) => FAccordionStyle(
titleTextStyle: FWidgetStateMap({
// This text style is applied when the accordion is hovered OR pressed OR focused (new).
WidgetState.hovered | WidgetState.pressed | Widget.focused: typography.base.copyWith(
fontWeight: FontWeight.w500,
color: colors.foreground,
decoration: TextDecoration.underline,
),
// This text style is applied when the accordion is NOT hovered OR pressed.
WidgetState.any: typography.base.copyWith(
fontWeight: FontWeight.w500,
color: colors.foreground,
),
}),
...
);
See FWidgetStateMap
for more information on FWidgetStateMap
s.
Pass the Style
import 'lib/theme/accordion_style.dart';
@override
Widget build(BuildContext context) => FAccordion(
// Pass the modified style to the widget.
style: accordionStyle(
colors: context.theme.colors,
typography: context.theme.typography,
style: context.theme.style,
),
children: const [
FAccordionItem(
title: Text('Is it accessible?'),
child: Text('Yes. It adheres to the WAI-ARIA design pattern.'),
),
],
);
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.
toApproximateMaterialTheme()
A Forui theme can be converted to a Material theme using
toApproximateMaterialTheme()
.
The toApproximateMaterialTheme()
method is marked as experimental. This method can change without prior warning. The
mapping between Forui and Material themes is done on a best-effort basis, and may not perfectly capture all the nuances
of a Forui theme.
import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
@override
Widget build(BuildContext context) {
final fThemeData = FThemes.zinc.light;
return MaterialApp(
theme: fThemeData.toApproximateMaterialTheme(),
home: Scaffold(
body: Center(
child: FCard(
title: 'Mixed Widgets',
subtitle: 'Using both Forui and Material widgets together',
child: ElevatedButton(
onPressed: () {},
child: const Text('Material Button'),
),
),
),
),
);
}
CLI
Use the CLI to generate a copy of toApproximateMaterialTheme()
inside your project:
dart run forui snippet create material-mapping
This should be 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.