Core Concepts

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 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 FAnimatedTheme(...).

1@override
2Widget build(BuildContext context) => FTheme(
3 data: FThemes.neutral.light, // or FThemes.neutral.dark
4 child: const FScaffold(child: Placeholder()),
5);
6

Forui includes predefined themes that can be used out of the box. They are heavily inspired by shadcn/ui.

ThemeLight AccessorDark Accessor

Neutral

FThemes.neutral.lightFThemes.neutral.dark

Zinc

FThemes.zinc.lightFThemes.zinc.dark

Slate

FThemes.slate.lightFThemes.slate.dark

Blue

FThemes.blue.lightFThemes.blue.dark

Green

FThemes.green.lightFThemes.green.dark

Orange

FThemes.orange.lightFThemes.orange.dark

Red

FThemes.red.lightFThemes.red.dark

Rose

FThemes.rose.lightFThemes.rose.dark

Violet

FThemes.violet.lightFThemes.violet.dark

Yellow

FThemes.yellow.lightFThemes.yellow.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:

1@override
2Widget 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;
7
8 return const Placeholder();
9}
10

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)
1@override
2Widget 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}
12

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:

1@override
2Widget build(BuildContext context) {
3 final typography = context.theme.typography;
4
5 return Text(
6 'Hello World!',
7 style: typography.xs.copyWith(
8 color: context.theme.colors.primaryForeground,
9 fontWeight: .bold,
10 ),
11 );
12}
13

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.

1@override
2Widget build(BuildContext context) => FTheme(
3 data: FThemeData(
4 colors: FThemes.neutral.light.colors,
5 typography: FThemes.neutral.light.typography
6 .copyWith(xs: const TextStyle(fontSize: 12, height: 1))
7 .scale(sizeScalar: 0.8),
8 ),
9 child: const FScaffold(child: Placeholder()),
10);
11

Style

The FStyle class the theme's miscellaneous styling options such as the default border radius and icon size.

1@override
2Widget build(BuildContext context) {
3 final colors = context.theme.colors;
4 final style = context.theme.style;
5
6 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}
15

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.

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 init

This generates a main.dart file where you will add your generated theme:

1
2import 'package:flutter/material.dart';
3
4import 'package:forui/forui.dart';
5
6import 'theme/theme.dart';
7
8
9void main() {
10 runApp(const Application());
11}
12
13class Application extends StatelessWidget {
14 const Application({super.key});
15
16 @override
17 Widget build(BuildContext context) {
18 // Assign the generated theme to `theme`.
19 final theme = neutralLight;
20
21 return MaterialApp(
22 localizationsDelegates: FLocalizations.localizationsDelegates,
23 supportedLocales: FLocalizations.supportedLocales,
24 builder: (_, child) => FTheme(data: theme, child: child!),
25 theme: theme.toApproximateMaterialTheme(),
26 home: const FScaffold(
27 // TODO: replace with your widget.
28 child: Placeholder(),
29 ),
30 );
31 }
32}
33

Generate a Theme

Run to generate a theme based on neutral's light variant:

dart run forui theme create neutral-light

Tip: 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.
1
2import 'package:flutter/material.dart';
3
4import 'package:forui/forui.dart';
5
6import 'accordion_style.dart';
7
8
9FThemeData get neutralLight {
10 final colors = FThemes.neutral.light.colors;
11
12 final typography = _typography(colors: colors);
13 final style = _style(colors: colors, typography: typography);
14
15 return FThemeData(
16 colors: colors,
17 typography: typography,
18 style: style,
19 // Add your generated styles here.
20 accordionStyle: accordionStyle(
21 colors: colors,
22 typography: typography,
23 style: style,
24 ),
25 );
26}
27
28FTypography _typography({
29 required FColors colors,
30 String defaultFontFamily = 'packages/forui/Inter',
31}) => FTypography(
32 xs: TextStyle(
33 color: colors.foreground,
34 fontFamily: defaultFontFamily,
35 fontSize: 12,
36 height: 1,
37 ),
38 sm: TextStyle(
39 color: colors.foreground,
40 fontFamily: defaultFontFamily,
41 fontSize: 14,
42 height: 1.25,
43 ),
44);
45
46FStyle _style({required FColors colors, required FTypography typography}) =>
47 FStyle(
48 formFieldStyle: .inherit(colors: colors, typography: typography),
49 focusedOutlineStyle: FFocusedOutlineStyle(
50 color: colors.primary,
51 borderRadius: const .all(.circular(8)),
52 ),
53 iconStyle: IconThemeData(color: colors.primary, size: 20),
54 tappableStyle: FTappableStyle(),
55 );
56

Generate a Style

Run to generate a FAccordionStyle:

dart run forui style create accordion

Tip: Run dart run forui style ls to see all available styles.

This generates a accordion style file which you can add to your theme:

1FAccordionStyle accordionStyle({
2 required FColors colors,
3 required FTypography typography,
4 required FStyle style,
5}) => FAccordionStyle(
6 titleTextStyle: .delta(
7 // This text style is applied when the accordion is NOT hovered OR pressed.
8 typography.base.copyWith(fontWeight: .w500, color: colors.foreground),
9 variants: {
10 // This text style is applied when the accordion is hovered OR pressed.
11 [.hovered, .pressed]: const .delta(decoration: .underline),
12 },
13 ),
14

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:

1FAccordion(
2 style: .delta(
3 focusedOutlineStyle: .delta(color: context.theme.colors.background),
4 ),
5 children: const [],
6);
7

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:

1// Complete replacement by passing a style directly
2FAccordion(
3 style: FAccordionStyle.inherit(
4 colors: colors,
5 typography: typography,
6 style: style,
7 ),
8 children: const [],
9);
10
11// Short-form
12FAccordion(
13 style: .delta(titlePadding: .symmetric(vertical: 20)),
14 children: [],
15);
16

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 accordion

Tip: 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:

1FAccordionStyle accordionStyle({
2 required FColors colors,
3 required FTypography typography,
4 required FStyle style,
5}) => FAccordionStyle(
6 titleTextStyle: .delta(
7 // This text style is applied when the accordion is NOT hovered OR pressed.
8 typography.base.copyWith(fontWeight: .w500, color: colors.foreground),
9 variants: {
10 // This text style is applied when the accordion is hovered OR pressed OR focused (new).
11 [.hovered, .pressed, .focused]: const .delta(decoration: .underline),
12 },
13 ),
14);
15

See FWidgetStateMap for more information on FWidgetStateMaps.

Pass the Style
1
2@override
3Widget build(BuildContext context) => FAccordion(
4 // Pass the modified style to the widget.
5 style: accordionStyle(
6 colors: context.theme.colors,
7 typography: context.theme.typography,
8 style: context.theme.style,
9 ),
10 children: const [
11 FAccordionItem(
12 title: Text('Is it accessible?'),
13 child: Text('Yes. It adheres to the WAI-ARIA design pattern.'),
14 ),
15 ],
16);
17

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().

1import 'package:flutter/material.dart';
2
3class BrandColor extends ThemeExtension<BrandColor> {
4 final Color color;
5
6 const BrandColor({required this.color});
7
8 @override
9 BrandColor copyWith({Color? color}) => BrandColor(color: color ?? this.color);
10
11 @override
12 BrandColor lerp(BrandColor? other, double t) {
13 if (other is! BrandColor) {
14 return this;
15 }
16
17 return BrandColor(color: Color.lerp(color, other.color, t)!);
18 }
19}
20

Add the extension to FThemeData(...) via its extensions parameter:

1
2FThemeData(
3 colors: FThemes.neutral.light.colors,
4 // ... other theme properties
5 extensions: [const BrandColor(color: Color(0xFF6366F1))],
6);
7

You can also add extensions to existing themes using copyWith(...):

1final theme = FThemes.neutral.light.copyWith(
2 extensions: [const BrandColor(color: Color(0xFF6366F1))],
3);
4

Accessing the Properties

Retrieve your custom theme extension via extension<T>():

1@override
2Widget build(BuildContext context) {
3 final brand = context.theme.extension<BrandColor>();
4 return ColoredBox(color: brand.color);
5}
6

Optionally, we recommend creating a getter on FThemeData:

1extension BrandColorExtension on FThemeData {
2 BrandColor get brand => extension<BrandColor>();
3}
4

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.

1import 'package:flutter/material.dart';
2
3import 'package:forui/forui.dart';
4
5@override
6Widget 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);
21

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.

On this page