📚 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 |
|---|---|
FlowChatView | 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 — reads from the top, anchoring to the newest message once it outgrows the viewport |
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 |
FlowCodeBlock | Fenced code with built-in synchronous highlighting, a header label, and a copy affordance — languages host-extensible |
FlowMarkdown | Assistant prose typeset from a built-in parser — headings, emphasis, lists, quotes, tables, links, and fences composing the code block; assistant turns render it by default and it streams gracefully |
FlowErrorState | Failure card with a host-written message and retry pill — failed turns render it automatically |
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 |
FlowPill | Removable pill for an enabled tool or mode in the composer's action row — label auto-drops 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.2.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 — Google Sans and Google Sans Code — arrives through
google_fonts: each cut is fetched
on first use and cached on the device, so there is no font to bundle. The
fetch needs network access: android.permission.INTERNET in an Android
app's main manifest (Flutter's template grants it only to debug and profile)
and the com.apple.security.network.client entitlement in a sandboxed macOS
app; without it text falls back to the platform face. To render offline on
first launch, ship the files under a google_fonts/ asset folder.
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.
classChatPageextendsStatefulWidget {
constChatPage({super.key});
@overrideState<ChatPage> createState() =>_ChatPageState();
}
class_ChatPageStateextendsState<ChatPage> {
finalScrollController _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 ='';
awaitfor (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;
});
}
@overrideWidgetbuild(BuildContext context) {
returnScaffold(
body:FlowChatView(
empty: _messages.isEmpty,
greeting:constFlowGreeting(
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,
),
),
);
}
}FlowChatView 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) {
returnswitch (part.type) {
'order-card'=>OrderCard(order: part.data asOrder),
_ =>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:constColor(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. Google Sans and Google Sans Code are fetched from Google Fonts under the SIL Open Font License, not bundled.
