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 FAnimatedTheme(...).
@override
Widget build(BuildContext context) => FAnimatedTheme(
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 6 core components in Forui’s theming system.
FAnimatedTheme: 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.- Individual widget styles.
- Individual widget motions.
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 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:
@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) => FAnimatedTheme(
data: FThemeData(
colors: FThemes.zinc.light.colors,
typography: FThemes.zinc.light.typography.copyWith(
xs: const TextStyle(fontSize: 12, height: 1),
).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
Forui provides 2 ways to customize themes and widget styles.
- CLI - major and reusable style changes, such as creating your own design system.
copyWith(...)method - minor and one-off adjustments to existing styles.
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 initThis 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) => FAnimatedTheme(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-lightTip: Run dart run forui theme ls to see all available themes.
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 accordionTip: Run dart run forui style ls to see all available styles.
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),
motion: const FAccordionMotion(),
);See FWidgetStateMap for more information on FWidgetStateMaps.
Individual Widget Styles
You can customize a widget’s style in 2 ways:
- Use
copyWith()- Best for minor adjustments to existing styles. - Generate via CLI - Best for major style overhauls.
copyWith(...)
All widgets and style copyWith(...)s accept style builder functions.
To change an accordion’s focused outline color:
FAccordion(
style: (style) => style.copyWith(
focusedOutlineStyle: (style) => style.copyWith(
color: context.theme.colors.background,
),
),
);All styles implement the call function. This allows a style object to be
passed directly to a widget or style’s copyWith(...) as a shortcut.
Both are equivalent:
// Long-form
FAccordion(
style: (style) => FAccordionStyle(...),
);
// Short-form
FAccordion(
style: FAccordionStyle(...),
);CLI
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 accordionTip: Run dart run forui style ls to see all available styles.
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 FWidgetStateMaps.
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.'),
),
],
);Custom Properties
Forui themes can be extended with your own application-specific properties using Flutter’s ThemeExtension
system.
Create a Theme Extension
Theme extensions must extend ThemeExtension and implement copyWith() and lerp().
import 'package:flutter/material.dart';
class BrandColor extends ThemeExtension<BrandColor> {
final Color color;
const BrandColor({
required this.color,
});
@override
BrandColor copyWith({Color? color}) => BrandColor(color: color ?? this.color);
@override
BrandColor lerp(BrandColor? other, double t) {
if (other is! BrandColor) {
return this;
}
return BrandColor(color: Color.lerp(color, other.color, t)!);
}
}Add the extension to FThemeData(...) via its extensions parameter:
import 'brand_theme.dart';
FThemeData(
// ... other theme properties
extensions: [
BrandColor(color: const Color(0xFF6366F1)),
],
);You can also add extensions to existing themes using copyWith(...):
final theme = FThemes.zinc.light.copyWith(
extensions: [
BrandColor(color: const Color(0xFF6366F1)),
],
);Accessing the Properties
Retrieve your custom theme extension via extension<T>():
@override
Widget build(BuildContext context) {
final brand = context.theme.extension<BrandColor>();
return ColoredBox(color: brand.color);
}Optionally, we recommend creating a getter on FThemeData:
extension BrandColorExtension on FThemeData {
BrandColor get brand => extension<BrandColor>();
}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-mappingThis 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.