Skip to content

Theming

Themes set the default look of a Fleury app. Define shared colors, text styles, borders, and interaction feedback in ThemeData, then pass it to FleuryApp. Use a widget’s style property when that widget needs a local override.

You need to…Use
Theme the whole appFleuryApp(theme: …)
Style widgetsstyle: CellStyle(…)
Style widget interactions such as focus, hover, and selectionCellStyle.interactive(…)

ThemeData.dark() and ThemeData.light() are Fleury’s built-in starting themes. Use copyWith(...) to change the shared colors, styles, and borders your app needs:

You write · editable
ThemeData _buildCustomTheme() {
final base = ThemeData.dark();
return base.copyWith(
colorScheme: base.colorScheme.copyWith(
primary: const RgbColor(0xE8, 0xA3, 0x3D),
focus: const RgbColor(0xF2, 0xC5, 0x5C),
warning: const RgbColor(0xE8, 0xA3, 0x3D),
),
borderStyle: BorderStyle.double,
);
}
Live preview
Live

Pass the result to FleuryApp. If no theme is supplied, Fleury uses its built-in defaults, so Theme.of(context) always returns a value. Inside a widget, context.theme is shorthand for Theme.of(context), and context.colors for its colorScheme.

Every ThemeData field has a default, so a theme changes only the fields you set:

FieldDefaultUsed for
brightnessBrightness.darkWhether the theme targets a dark or a light terminal
textStyleNo styleThe base style of every Text below the theme
mutedStyleDimDe-emphasized text such as hints, separators, and disabled rows
selectionStyleInverseThe selected or current item, such as a list’s highlighted row
focusedStyleBoldFocused controls
errorStyleRed and underlinedInvalid controls and validation messages
interactiveStyleNoneApp-wide control state styles; see Interactive styles
borderStyleBorderStyle.roundedFramed surfaces such as panels and Container.framed
colorSchemeColorScheme()The color roles below
extensionsEmptyYour own theme objects

ColorScheme holds the colors an app draws from, by role:

RoleDefault in ColorScheme()Used for
foreground, backgroundnull: the terminal’s own colorsDefault text and background
surfacenull: near-black on a dark theme, near-white on a light oneOpaque fills such as Container.filled and presented dialogs
primaryColors.mintInteractive and active accents
focusColors.azureFocused regions and text entry
success, warning, error, infoANSI green, yellow, red, and cyanStatus

ThemeData.dark() and ThemeData.light() each set their own primary. extensions can hold objects of any class you define; read one by type with context.theme.extension<MyColors>(), which returns null when the theme has none.

To change the theme for part of the app, wrap that subtree in a Theme and build its data from the ambient theme:

Theme(
data: context.theme.copyWith(borderStyle: BorderStyle.double),
child: const SettingsPanel(),
)

Pass a CellStyle to a control’s style to change its base appearance. This input draws its text in cyan:

local_style.dart
You write · editable
class _StyledInputState extends State<_StyledInput> {
final _query = TextEditingController(text: 'api-gateway');
@override
void dispose() {
_query.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => SizedBox(
width: 24,
child: TextInput(
controller: _query,
style: const CellStyle(foreground: Colors.cyan),
),
);
}
Live preview
Live · interactive
click & type to interact ⓘ how this demo runs

That style becomes the control’s base appearance. It does not erase the framework’s focus, disabled, or validation cues; those still layer on when the state changes. The same CellStyle works on Text, Button, Checkbox, Select, and the rest of the control set.

A CellStyle describes terminal paint: foreground and background colors plus attributes such as bold, italic, underline, and strikethrough.

cell_style.dart
You write · editable
class _StyledText extends StatelessWidget {
const _StyledText();
@override
Widget build(BuildContext context) => const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('Default text'),
Text(
'Styled text',
style: CellStyle(
foreground: RgbColor(0x3D, 0xDC, 0x97),
bold: true,
underline: true,
),
),
],
);
}
Live preview
Live

Styles merge field by field. An unset value inherits; explicitly setting a boolean attribute to false turns that inherited attribute off.

Use CellStyle.interactive when a widget should change as users interact with it. Assign it to ThemeData.interactiveStyle for the whole app, or pass it to one widget’s ordinary style property for a local exception. Native hover feedback also needs TerminalMode(mouseMotion: true) in runApp; mouse: true alone doesn’t report plain motion, so hovered changes only when a press, drag, or wheel event arrives. An app created by fleury create starts with mouse: true; Enable mouse input in a terminal shows the change. Browser hosts already provide pointer motion. This reference renders each entry directly so the differences are easy to compare:

interactive_styles.dart
You write · editable
const CellStyle _interactiveStyle = CellStyle.interactive(
focused: CellStyle(inverse: true, bold: true),
hovered: CellStyle(underline: true),
selected: CellStyle(foreground: Colors.green, bold: true),
invalid: CellStyle(foreground: Colors.red, underline: true),
disabled: CellStyle(dim: true),
);
Live preview
Live

When assigned to ThemeData.interactiveStyle, controls apply the relevant entry automatically. If states overlap, Fleury layers selected, hovered, focused, pressed, and invalid paint in that order; disabled paint stands alone. pressed: CellStyle(…) styles a held primary pointer press on buttons and other activatable controls; Input & gestures explains when it applies. Pair color with an attribute such as bold, underline, inverse, or dim so feedback does not depend on hue alone.

The same style property accepts either kind of value. Here a Theme stands in for the app theme and gives every control an inverse, bold focus cue; Theme focus inherits it, and Local focus overrides it:

local_interactive_style.dart
You write · editable
class _LocalFocusStyle extends StatelessWidget {
const _LocalFocusStyle();
@override
Widget build(BuildContext context) => Theme(
// Overrides the app theme below this point: every control's inherited
// focus cue becomes inverse and bold.
data: Theme.of(context).copyWith(
interactiveStyle: const CellStyle.interactive(
focused: CellStyle(inverse: true, bold: true),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text('LOCAL INTERACTION STYLE', style: CellStyle(bold: true)),
const Text('Tab or click to compare focus cues'),
Row(
children: <Widget>[
Button(text: 'Theme focus', autofocus: true, onPressed: () {}),
const SizedBox(width: 2),
Button(
text: 'Local focus',
style: const CellStyle.interactive(
focused: CellStyle(foreground: Colors.cyan, underline: true),
),
onPressed: () {},
),
],
),
],
),
);
}
Live preview
Live · interactive
click & type to interact ⓘ how this demo runs

A plain CellStyle changes the base paint while inherited interaction cues remain. CellStyle.interactive is the local escape hatch when one control’s state should differ. A local entry replaces the theme’s entry for that state, then combines with any other active states.

Choose the mode when you construct the app theme. ThemeData.dark() and ThemeData.light() set brightness for you, or pass it explicitly to ThemeData(...):

FleuryApp(title: 'Dashboard', theme: ThemeData.dark(), home: const Dashboard());

Fleury does not guess the terminal’s background. When another value needs a light and dark variant, pick it with the ambient theme’s adaptive:

final shadow = context.theme.adaptive(
light: Colors.gray,
dark: Colors.black,
);

For code outside a widget build method, use theme.brightness.pick(light: ..., dark: ...).

Use CellStyle.none for a state when one widget should deliberately suppress an inherited visual cue. The validation message and semantics remain intact; only the input’s invalid paint is neutral:

neutral_invalid.dart
You write · editable
class _NeutralInvalidFieldState extends State<_NeutralInvalidField> {
final _form = FormController();
final _query = TextEditingController();
@override
void dispose() {
_form.dispose();
_query.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Form(
controller: _form,
onSubmit: () {},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text('NEUTRAL INVALID CHROME', style: CellStyle(bold: true)),
const Text('Submit empty: the message stays visible'),
FormField(
validator: () => _query.text.isEmpty ? 'Enter a query.' : null,
child: SizedBox(
width: 30,
child: TextInput(
controller: _query,
autofocus: true,
semanticLabel: 'Query',
placeholder: 'Query',
style: const CellStyle.interactive(invalid: CellStyle.none),
),
),
),
Button(text: 'Submit', onPressed: _form.submit),
],
),
);
}
Live preview
Live · interactive
click & type to interact ⓘ how this demo runs

Authors of reusable interactive widgets can call CellStyle.resolve(...) to apply the same theme, local-style, and state cascade as Fleury’s controls. Ordinary application code should not need to call it.

package:fleury/themes.dart is Fleury’s opt-in community theme collection, included in the core package. It currently includes Nord, Dracula, Gruvbox Dark, Tokyo Night, Catppuccin Mocha, One Dark, and Solarized in dark and light variants, and contributions can add more:

import 'package:fleury/fleury.dart';
import 'package:fleury/themes.dart';
FleuryApp(title: 'Dashboard', theme: tokyoNight, home: const Dashboard());

Arrow through the live picker to compare the same UI across every palette:

Live · interactive
click & type to interact ⓘ how this demo runs

Every preset is a plain const ThemeData. The package also exports fleuryThemes, a named list ready for a theme picker.

For a broader comparison—and a custom editor for primary, focus, and status colors, brightness, and borders—open the Theme studio showcase.