Themes
Forui themes allows you to customize the look and feel of your Flutter application. Our theming solution is designed to help you get started quickly while offering powerful and flexible customization options.
Predefined Themes
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 provides a set of predefined themes that you can use out of the box. The color schemes are heavily inspired by shadcn/ui themes .
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 |
Usage
FTheme
uses inherited widgets to provide the
FThemeData
to all widgets in the subtree.
Forui provides an extension on BuildContext
which allows for direct access to FThemeData
through context.theme
.
@override
Widget build(BuildContext context) {
final theme = context.theme; // FThemeData
final colors = context.theme.colors; // FColors
final typography = context.theme.typography; // FTypography
final style = context.theme.style; // FStyle
return const Placeholder();
}
A high level overview of the theme data structure is as follows:
FThemeData
containsFColors
,FTypography
,FStyle
, and widget styles (eg.FCardStyle
).FColors
contains the color scheme (eg.background
,foreground
, andprimary
).FTypography
contains thedefaultFontFamily
and variousTextStyle
s.FStyle
contains other miscellaneous styling options (eg.borderRadius
).
A more detailed explanation of each class can be found in the Class Diagram section.
Customize Themes
CLI
It’s recommended to use the CLI to generate themes and styles in your project. These themes and styles can be directly modified to fit your design needs.
This example demonstrates how to generate and use both a theme and FButtonStyles
in your project.
Navigate to your project directory.
Run the following command to generate a main.dart
:
dart run forui init
Run the following command to generate a theme based on zinc:
dart run forui theme create zinc
Run the following command to generate a FButtonStyles
:
dart run forui style create button
The first command, dart run forui init
generates a main file which you can add the generated theme to:
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(),
// You can replace FScaffold with Material's Scaffold.
home: const FScaffold(
// TODO: replace with your widget.
child: Example(),
),
);
}
}
class Example extends StatefulWidget {
const Example({super.key});
@override
State<Example> createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
int _count = 0;
@override
Widget build(BuildContext context) => Center(
child: ...
);
}
The second command, dart run forui theme create zinc
generates a theme file which you can add the generated button
styles to:
import 'button_styles.dart';
FThemeData get zincLight {
const colors = FColors(
brightness: Brightness.light,
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 previously generated button styles and other generated styles here.
buttonStyles: buttonStyles(colors: colors, typography, typography, style: style),
);
}
FThemeData get zincDark {
const colors = FColors(
brightness: Brightness.dark,
background: Color(0xFF09090B),
foreground: Color(0xFFFAFAFA),
...
);
final typography = _typography(colors: colors);
final style = _style(colors: colors, typography: typography);
return FThemeData(
colors: colors,
typography: typography,
style: style,
// Add your previously generated button styles and other generated styles here.
buttonStyles: buttonStyles(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(),
);
The last command, dart run forui style create button
generates a button styles file which you can add to your theme.
FWidgetStateMap
allows you to define different styles for different combinations of states for a widget. This
combination of states is known as a “constraint”.
To add a decoration for a disabled button when it is hovered or pressed:
FButtonStyles buttonStyles({
required FColors colors,
required FTypography typography,
required FStyle style,
}) => FButtonStyles(
primary: _buttonStyle(
colors: colors,
style: style,
typography: typography,
color: colors.primary,
foregroundColor: colors.primaryForeground,
),
...
);
FButtonStyle _buttonStyle({
required FColors colors,
required FTypography typography,
required FStyle style,
required Color color,
required Color foregroundColor,
}) => FButtonStyle(
decoration: FWidgetStateMap({
WidgetState.disabled & (WidgetState.hovered | WidgetState.pressed): BoxDecoration(
borderRadius: style.borderRadius,
color: colors.hover(colors.disable(color)),
),
WidgetState.disabled: BoxDecoration(
borderRadius: style.borderRadius,
color: colors.disable(color),
),
WidgetState.hovered | WidgetState.pressed: BoxDecoration(
borderRadius: style.borderRadius,
color: colors.hover(color),
),
WidgetState.any: BoxDecoration(
borderRadius: style.borderRadius,
color: color,
),
}),
...
);
Constraints are applied in the order they are defined (top to bottom). The first matching constraint will be used. This means that more specific constraints should be defined above more general ones.
Most FWidgetStateMap
properties only support a subset of WidgetState
s. For example, FButtonStyle.decoration
only
supports WidgetState.disabled
, WidgetState.hovered
, WidgetState.pressed
and WidgetState.focused
. A property’s
supported states are documented in its API docs.
Class Diagram
Colors
The Colors
class contains the color scheme for the theme.
This usually consists of a set of colors with its corresponding foreground color (eg. primary
and primaryForeground
).
In most cases, the color will be used as the background color, and the foreground color will be used as the text/icon color.
@override
Widget build(BuildContext context) {
final colors = context.theme.colors;
final typography = context.theme.typography;
return ColoredBox(
color: colors.primary,
child: Text(
'Hello World!',
style: typography.xs.copyWith(
color: colors.primaryForeground
),
),
);
}
Hovered and Disabled Colors
Hovered and disabled colors are derived by adjusting the opacity. To derive these colors, use the FColors.hover
and FColors.disable
methods. The opacity can be adjusted with FColors.enabledHoveredOpacity
and
FColors.disabledOpacity
.
Typography
The FTypography
class contains the typography settings for the theme.
This includes the default font family and various TextStyle
s for different use cases.
The TextStyle
s stored in FTypography
are based on Tailwind CSS Font Size .
For instance, FTypography.sm
is closely related to text-sm
in Tailwind CSS.
It is recommended to use the copyWith(...)
to apply colors and other styles as the TextStyle
s stored only contain the fontSize
and height
properties.
@override
Widget build(BuildContext context) {
final colors = context.theme.colors;
final typography = context.theme.typography;
return Text(
'Hello World!',
style: typography.xs.copyWith(
color: colors.primaryForeground,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic,
),
);
}
Custom Font Family
To change the default font family, use the copyWith(...)
method to apply the new 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 contains other miscellaneous styling options for the theme.
@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(),
);
}
Override Individual Widget Styles
In certain cases, you may want to override the theme for a specific widget. This can be easily achieved by using the
copyWith(...)
function.
In the example below, styles are only overridden for this specific FCard
widget.
FCard
widgets that are used elsewhere will still inherit from theme data.
@override
Widget build(BuildContext context) {
final theme = context.theme;
return FCard(
title: 'Notification',
subtitle: 'You have 3 new messages',
style: theme.cardStyle.copyWith(
decoration: theme.cardStyle.decoration.copyWith(
borderRadius: BorderRadius.zero, // Remove border radius.
),
),
);
}
Override FWidgetStateMap
Properties
FWidgetStateMap
allows you to define different styles for different combinations of states for a widget. This
combination of states is known as a “constraint”.
Constraints are applied in the order they are defined (top to bottom). The first matching constraint will be used. This means that more specific constraints should be defined above more general ones.
@override
Widget build(BuildContext context) {
final style = context.theme.buttonStyles.primary;
return Column(
children: [
FButton(
style: style.copyWith(
// Override the decoration for the constraints that are hovered.
decoration: style.decoration.replaceAllWhere({WidgetState.hovered}, BoxDecoration()),
),
onPress: () {},
child: const Text('Button'),
),
FButton(
style: style.copyWith(
// Override the decoration for the first constraint that is hovered.
decoration: style.decoration.replaceFirstWhere({WidgetState.hovered}, BoxDecoration()),
),
onPress: () {},
child: const Text('Button'),
),
FButton(
style: style.copyWith(
// Override the decoration for the last constraint that is hovered.
decoration: style.decoration.replaceLastWhere({WidgetState.hovered}, BoxDecoration()),
),
onPress: () {},
child: const Text('Button'),
),
],
);
}
Most FWidgetStateMap
properties only support a subset of WidgetState
s. For example, FButtonStyle.decoration
only
supports WidgetState.disabled
, WidgetState.hovered
, WidgetState.pressed
and WidgetState.focused
. A property’s
supported states are documented in its API docs.
Material Design Interoperability
Forui provides a way to convert its themes to Material Design’s ThemeData
for interoperability between Forui and
Material widgets. This is useful when:
- Using Material widgets within a Forui application
- Maintaining consistent theming across both Forui and Material components
- Gradually migrating from Material Design to Forui
toApproximateMaterialTheme()
A Forui theme can be converted to a Material theme using the
toApproximateMaterialTheme()
method.
The toApproximateMaterialTheme()
method is marked as experimental. As an experimental feature, 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
For more fine-grained control, you can use the CLI to generate a copy of the toApproximateMaterialTheme()
inside your
project. The generated copy can then be directly modified to fit your design needs.
To generate a copy of toApproximateMaterialTheme()
, run:
dart run forui snippet create material-mapping