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 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@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 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@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 defines 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

Variants

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 hovered
6 [.not(.hovered)]: const BoxDecoration(color: Colors.red),
7 // hovered OR pressed
8 [.hovered, .pressed]: const BoxDecoration(color: Colors.grey),
9 // disabled AND pressed
10 [.disabled.and(.pressed)]: const BoxDecoration(color: Colors.black26),
11 },
12);
13

Variants 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 radius
9 [.not(.hovered)]: const .delta(color: Colors.red),
10 // hovered OR pressed - keeps border radius
11 [.hovered, .pressed]: const .delta(color: Colors.grey),
12 // disabled AND pressed - keeps border radius
13 [.disabled.and(.pressed)]: const .delta(color: Colors.black26),
14 },
15);
16

Resolution uses a tiered most-specific-wins strategy which is deterministic and order-independent.

Each variant belongs to one of three tiers:

TierCategoryExamples
2Semanticdisabled, selected, error
1Interactionhovered, focused, pressed
0Platformandroid, 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';
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

Alternatively, you can generate a copy of toApproximateMaterialTheme() inside your project using the CLI:

dart run forui snippet create material-mapping

This 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.

On this page