📚 Documentation · 🧩 Playground · 🤓 API Reference · 🗺️ Roadmap
Flow UI is an open-source Flutter UI library to build production-grade Chat & AI assistant interfaces.
Important
flow_ui is pre-1.0. The API is still settling, and minor releases may carry breaking changes — pin a minor version and read the changelog when upgrading.
| Component | What it does |
|---|---|
FlowChatScreen |
The full chat surface: bounded thread over a composer, centred at a readable width, with a zero state (greeting, lifted composer, starters) and a jump-to-latest button |
FlowThread |
Scrollable conversation anchored to the newest message |
FlowMessage |
One turn — ink-wash user bubble, plain assistant, error bubble, typed content parts |
FlowStreamingText |
Animated text reveal while a reply arrives |
FlowThinkingIndicator |
Turning, breathing asterisk with a shimmering label |
FlowShimmerText |
Sweeping text highlight, static once settled |
FlowMessageActions |
Copy / regenerate / edit / feedback row under a message |
FlowComposer |
Multiline input with send/stop, attachments strip, and leading/trailing action slots |
FlowMenu |
Icon-triggered menu with groups, submenus, and toggles — anchored card on desktop, bottom sheet on phones |
FlowModelSelector |
Model picker with effort and overflow submenus, sheet on phones |
FlowAttachmentGroup |
Image and file tiles with a type pill |
FlowAttachmentPreview |
Full-screen image viewer with zoom and paging |
FlowSuggestion / FlowSuggestionGroup |
Prompt starters — plain or outlined; scroll, wrap, or column layouts |
FlowGreeting |
Zero-state headline |
FlowTheme |
Design tokens (colors and typography) as a ThemeExtension, with light and dark presets |
dependencies:
flow_ui: ^0.1.0Install the theme once (optional — without it, components fall back to a preset matching the ambient brightness):
MaterialApp(
theme: ThemeData(extensions: [FlowTheme.light()]),
darkTheme: ThemeData(
brightness: Brightness.dark,
extensions: [FlowTheme.dark()],
),
)The default typography ships with the package — Figtree, bundled under the SIL Open Font License — so the theme renders as designed with no font setup.
Messages are pure view models. Your app maps its own transport into
FlowMessageData, and streaming is data, not streams: while a reply arrives,
rebuild with copyWith carrying the grown text.
class ChatPage extends StatefulWidget {
const ChatPage({super.key});
@override
State<ChatPage> createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage> {
final ScrollController _scroll = ScrollController();
List<FlowMessageData> _messages = const [];
bool _generating = false;
void _send(String text) async {
final id = DateTime.now().microsecondsSinceEpoch.toString();
setState(() {
_messages = [
..._messages,
FlowMessageData.text(id: id, role: FlowMessageRole.user, text: text),
// An empty pending reply renders the thinking indicator.
FlowMessageData(
id: '$id-reply',
role: FlowMessageRole.assistant,
status: FlowMessageStatus.pending,
),
];
_generating = true;
});
// Feed chunks from your backend as they arrive.
var streamed = '';
await for (final chunk in myBackend.reply(text)) {
streamed += chunk;
setState(() {
_messages = [
..._messages.sublist(0, _messages.length - 1),
_messages.last.copyWith(
parts: [FlowTextPart(streamed)],
status: FlowMessageStatus.streaming,
),
];
});
}
setState(() {
_messages = [
..._messages.sublist(0, _messages.length - 1),
_messages.last.copyWith(status: FlowMessageStatus.complete),
];
_generating = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: FlowChatScreen(
empty: _messages.isEmpty,
greeting: const FlowGreeting(
icon: Icons.wb_twilight,
text: 'Good afternoon',
),
suggestions: FlowSuggestionGroup(
layout: FlowSuggestionLayout.column,
suggestions: [
FlowSuggestion(
label: 'Write an essay about life and enjoyment',
icon: Icons.edit_note,
onTap: () => _send('Write an essay about life and enjoyment'),
),
FlowSuggestion(
label: 'Create a Monday briefing from my tasks',
icon: Icons.event_available,
onTap: () => _send('Create a Monday briefing from my tasks'),
),
],
),
thread: FlowThread(
messages: _messages,
controller: _scroll,
thinkingLabel: 'Thinking…',
),
threadController: _scroll,
jumpToLatestTooltip: 'Jump to latest',
composer: FlowComposer(
placeholder: 'How can I help you today?',
isStreaming: _generating,
onSend: _send,
onStop: myBackend.stop,
),
),
);
}
}FlowChatScreen is body-only — it builds no Scaffold and no app bar, so
your app keeps the chrome, the background, and the keyboard inset. See
example/lib/main.dart for a complete runnable
version of this page, and the live playground
for a demo of every component with variants and code snippets.
The composer takes leading and trailing action slots. Drop in a FlowMenu
(attachments, toggles) and a FlowModelSelector — both render an anchored
card on wide layouts and a bottom sheet on phones:
FlowComposer(
onSend: _send,
leadingActions: [
FlowMenu(
icon: Icons.add,
tooltip: 'Add to chat',
entries: const [
FlowMenuOption(id: 'files', icon: Icons.attach_file, label: 'Add files'),
FlowMenuDivider(),
FlowMenuOption(id: 'web', icon: Icons.public, label: 'Web search', selected: true),
],
onSelected: _handleMenu,
),
],
trailingActions: [
FlowModelSelector(
models: const [
FlowModelOption(id: 'fast', label: 'Fast', description: 'Quick answers'),
FlowModelOption(id: 'smart', label: 'Smart', description: 'Hard problems'),
],
selectedId: _modelId,
onSelected: (id) => setState(() => _modelId = id),
),
],
)A message holds an ordered list of sealed FlowMessageParts —
FlowTextPart, FlowAttachmentPart, and FlowCustomPart for anything the
package doesn't know about. Custom parts render through a builder you supply,
so hosts can inject arbitrary widgets (tool cards, citations, charts) without
forking the message renderer:
FlowThread(
messages: _messages,
customPartBuilder: (context, message, part) {
return switch (part.type) {
'order-card' => OrderCard(order: part.data as Order),
_ => null, // unknown parts are skipped
};
},
)Attachments carry an ImageProvider, so network, file, memory, and asset
images all work — the package never loads anything itself:
FlowMessageData(
id: 'm1',
role: FlowMessageRole.user,
parts: [
FlowAttachmentPart([
FlowAttachment(id: 'a1', thumbnail: NetworkImage(url), kind: 'JPG', label: 'sunset.jpg'),
]),
FlowTextPart('What do you think of this shot?'),
],
)FlowTheme carries two token sets — colors and typography. Role names follow
Material 3's ColorScheme, so an existing scheme maps across, with one
addition: the design draws content at three ink levels (onSurface,
onSurfaceVariant, onSurfaceMuted) where M3 names two. Start from a preset
and override what your brand needs:
FlowTheme(
colors: FlowColors.dark.copyWith(primary: const Color(0xFF6C5CE7)),
typography: FlowTypography.standard,
)Spacing and corner radii are deliberately not tokens. Following Material's
structure, each component bakes its own metrics from the Flow UI design file
and exposes per-widget overrides (padding:, borderRadius:) where hosts
retheme. Strings shown to the user (tooltips, placeholders, labels) are
host-supplied, so localization stays in your app — the one exception, the
model selector's effortLabel and moreModelsLabel English defaults, is
overridable the same way.
Full documentation lives at flowui.stac.dev, and every component has a stage in the live playground — variant pills and code snippets included. The playground is also in the repo to run locally:
cd playground && flutter run -d chromeCode is released under the MIT License. The bundled Figtree font is licensed separately under the SIL Open Font License.
