super_tree_field
A generic, recursive, keyboard-first tree widget for Flutter.
super_tree_field provides a reusable hierarchy engine built around
TreeNode<T>, SuperTreeController<T>, and SuperTree<T>. It supports
search, expansion, keyboard navigation, single or multi-selection, inline
editing, drag-and-drop reordering, contextual actions, bilingual node labels,
and light/dark themes through super_core.
The package also includes AccountTree, a ready-made chart-of-accounts tree
with account types, debit/credit nature, roll-up balances, KPI cards, filters,
and bundled sample data.
Features
- Generic tree nodes with typed payloads.
- Recursive rendering for hierarchies of any depth.
- Ancestor-preserving search with inline highlighting.
- Keyboard navigation with LTR and RTL-aware arrow behavior.
- Readable and editable modes.
- Inline rename, add, delete, and drag-and-drop operations.
- Single and multi-selection with tristate group checkboxes.
- Expand all, collapse all, and expand-subtree actions.
- Custom leading and trailing cells.
- Optional Arabic labels on every node.
- Responsive layouts using
super_coredevice modes. - A chart-of-accounts implementation with financial summaries.
Requirements
| Dependency | Minimum version |
|---|---|
| Dart | 3.8.0 |
| Flutter | 3.32.0 |
super_core |
3.3.0 |
Installation
Add the package to pubspec.yaml:
dependencies:
super_tree_field: ^0.5.2
Then install dependencies:
flutter pub get
Import the public barrel:
import 'package:super_tree_field/super_tree.dart';
The barrel also re-exports the public super_core API used by this package.
Theme setup
super_tree_field reads its colors, spacing, typography, radii, and motion
from SuperMaterialThemeData. Starting with super_core 3.3.0, both
textTheme and primaryTextTheme are required SuperTextTheme values.
Typography is read with context.superTextTheme; it is no longer exposed by
SuperThemeData. Configure the theme at the application root:
import 'package:flutter/material.dart';
import 'package:super_tree_field/super_tree.dart';
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
final textTheme = SuperTextTheme(isDesktop: true);
return MaterialApp(
debugShowCheckedModeBanner: false,
themeMode: ThemeMode.system,
theme: SuperMaterialThemeData.light(
palette: SuperPalette.bluePalette,
mode: SuperDeviceMode.desktop,
textTheme: textTheme,
primaryTextTheme: textTheme,
),
darkTheme: SuperMaterialThemeData.dark(
palette: SuperPalette.bluePalette,
mode: SuperDeviceMode.desktop,
textTheme: textTheme,
primaryTextTheme: textTheme,
),
home: const FileTreePage(),
);
}
}
Choose the device mode that matches the current layout:
final mode = SuperDeviceMode.forWidth(MediaQuery.sizeOf(context).width);
When the application supports multiple form factors, rebuild the root theme when the active width crosses the mobile, tablet, or desktop breakpoint.
For locale changes, rebuild SuperTextTheme as well. For example, use
SuperTextTheme(isArabic: true) for an Arabic typography ramp. Do not use
context.superTheme.textTheme or SuperThemeData.of(context).textTheme in
super_core 3.3.0.
super_core 3.3.0 also removed the internal _familyOf inference. A font
family configured in SuperTextTheme is no longer copied implicitly into
SuperTokensData; set token-level font metadata explicitly only when needed.
Quick start
The following example creates a reusable file tree. The controller is created
once in initState and disposed with the widget, following Flutter controller
lifecycle conventions.
import 'package:flutter/material.dart';
import 'package:super_tree_field/super_tree.dart';
@immutable
class FileItem {
const FileItem.folder() : isFolder = true, size = null;
const FileItem.file(this.size) : isFolder = false;
final bool isFolder;
final String? size;
}
const files = <TreeNode<FileItem>>[
TreeNode<FileItem>(
code: 'lib',
name: 'lib',
value: FileItem.folder(),
children: [
TreeNode<FileItem>(
code: 'lib/main.dart',
name: 'main.dart',
value: FileItem.file('2.4 KB'),
),
TreeNode<FileItem>(
code: 'lib/features',
name: 'features',
value: FileItem.folder(),
children: [
TreeNode<FileItem>(
code: 'lib/features/home.dart',
name: 'home.dart',
value: FileItem.file('5.1 KB'),
),
],
),
],
),
TreeNode<FileItem>(
code: 'README.md',
name: 'README.md',
value: FileItem.file('8.7 KB'),
),
];
class FileTreePage extends StatefulWidget {
const FileTreePage({super.key});
@override
State<FileTreePage> createState() => _FileTreePageState();
}
class _FileTreePageState extends State<FileTreePage> {
late final SuperTreeController<FileItem> _controller;
@override
void initState() {
super.initState();
_controller = SuperTreeController<FileItem>(
roots: files,
defaultExpandDepth: 0,
searchText: (node) => '${node.code} ${node.name}',
onOpenLeaf: (node) {
debugPrint('Open ${node.code}');
},
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: SuperTree<FileItem>(
controller: _controller,
title: 'Project files',
subtitle: 'Browse the current Flutter project',
titleIcon: Icons.folder_open_outlined,
nameColumnLabel: 'Name',
trailingColumnLabel: 'Size',
placeholder: 'Search files',
samples: const ['dart', 'features', 'README'],
unit: 'files',
showArabic: false,
leadingBuilder: (context, node, info) {
final isFolder = node.value?.isFolder ?? node.hasChildren;
return Icon(
isFolder
? (info.open ? Icons.folder_open : Icons.folder)
: Icons.description_outlined,
size: 18,
);
},
trailingBuilder: (context, node, info) {
final size = node.value?.size;
return size == null ? null : Text(size);
},
),
),
),
);
}
}
Tree data model
Every hierarchy is represented by TreeNode<T>:
const node = TreeNode<String>(
code: 'departments.engineering.mobile',
name: 'Mobile',
ar: 'تطبيقات الجوال',
value: 'mobile-team',
children: [
TreeNode<String>(
code: 'employees.42',
name: 'Anwar',
ar: 'أنور',
value: 'employee-42',
),
],
);
| Property | Description |
|---|---|
code |
Stable unique identifier used for expansion, focus, selection, editing, and drag-and-drop. |
name |
Primary node label. |
ar |
Optional Arabic label. |
value |
Optional domain payload of type T. |
children |
Optional child nodes. A null or empty list creates a leaf. |
Useful node APIs:
node.hasChildren;
node.isLeaf;
node.withChildren(updatedChildren);
node.copyWith(name: 'Updated name');
node.renamed('Updated name', ar: 'اسم محدّث');
Codes must be unique across the complete tree. Duplicate codes cause expansion, focus, selection, and editing state to target the wrong node.
Controller
SuperTreeController<T> is the state owner for the tree. It extends
ChangeNotifier, so the widget reacts automatically when controller state
changes.
final controller = SuperTreeController<MyItem>(
roots: roots,
searchText: (node) => '${node.code} ${node.name} ${node.ar ?? ''}',
defaultExpandDepth: 1,
query: '',
mode: SuperTreeMode.readable,
selectionMode: SuperTreeSelectionMode.none,
onOpenLeaf: openItem,
onTreeChanged: saveTree,
onSelectionChanged: saveSelection,
);
The host that creates the controller owns it and must call dispose().
Reading state
controller.roots;
controller.visible;
controller.query;
controller.searching;
controller.matchCount;
controller.totalLeaves;
controller.visibleLeaves;
controller.focusId;
controller.selected;
controller.mode;
controller.isEditable;
controller.editingId;
controller.checked;
controller.checkedCount;
controller.checkedNodes;
Updating state
controller.setRoots(updatedRoots);
controller.setQuery('invoice');
controller.clearQuery();
controller.setMode(SuperTreeMode.editable);
controller.toggleMode();
controller.expandAll();
controller.collapseAll();
controller.expandSubtree('root-code');
controller.clearSelection();
Search
Supply a searchText callback when creating the controller. The callback should
return every field that must participate in search:
searchText: (node) => [
node.code,
node.name,
node.ar,
node.value?.category,
].whereType<String>().join(' '),
Search is case-insensitive and ancestor-preserving:
- A matching node keeps its complete subtree.
- A matching descendant keeps the path from the root to that descendant.
- All filtered branches are rendered expanded while the query is active.
- Matching text is highlighted by
HighlightText.
Drive search programmatically when needed:
controller.setQuery('bank');
final matches = controller.matchCount;
controller.clearQuery();
Selection
Selection is configured when the controller is created.
Single selection
final controller = SuperTreeController<Permission>(
roots: permissionTree,
searchText: (node) => node.name,
selectionMode: SuperTreeSelectionMode.single,
initialChecked: const {'permission.read'},
onSelectionChanged: (codes) {
debugPrint('Selected: $codes');
},
);
Single selection keeps at most one node code.
Multi-selection
final controller = SuperTreeController<Permission>(
roots: permissionTree,
searchText: (node) => node.name,
selectionMode: SuperTreeSelectionMode.multi,
initialChecked: const {
'permission.read',
'permission.create',
},
onSelectionChanged: persistPermissions,
);
In multi-selection mode:
- Selecting a group selects all descendant leaves.
- Group checkboxes derive a checked, partial, or unchecked state from leaves.
- The table header provides a select-all checkbox.
checkedcontains checked leaf codes.
Selection APIs:
controller.checkState('permission.manage');
controller.isChecked('permission.read');
controller.rootCheckState;
controller.toggleChecked(node);
controller.toggleCheckedFocused();
controller.checkAll();
controller.clearChecked();
controller.toggleCheckAll();
controller.setChecked({'permission.read'});
The available states are:
TreeCheckState.unchecked
TreeCheckState.partial
TreeCheckState.checked
Editing
Editing requires two related settings:
- The controller mode controls whether mutations are active.
SuperTree.enableEditingdisplays the built-in Read/Edit switch and Add node action.
late final controller = SuperTreeController<Category>(
roots: categories,
searchText: (node) => node.name,
mode: SuperTreeMode.readable,
newNodeBuilder: (code) => TreeNode<Category>(
code: code,
name: 'New category',
value: const Category(),
),
onTreeChanged: (updatedRoots) {
repository.save(updatedRoots);
},
);
SuperTree<Category>(
controller: controller,
enableEditing: true,
leadingBuilder: buildCategoryIcon,
);
Editable mode provides:
- Inline rename.
- Add root, child, sibling before, or sibling after.
- Delete a node and its subtree.
- Drag-and-drop before, inside, or after another node.
- A contextual menu through right-click, long-press, or the row menu button.
Programmatic editing APIs:
controller.beginRename(code);
controller.commitRename(code, 'New name', ar: 'اسم جديد');
controller.cancelRename();
controller.addRoot();
controller.addChild(parentCode);
controller.addSiblingBefore(code);
controller.addSiblingAfter(code);
controller.deleteNode(code);
controller.moveNode(dragCode, targetCode, DropPosition.inside);
controller.canDrop(dragCode, targetCode);
Structural changes are emitted through onTreeChanged. Persist the returned
roots in the application repository or state-management layer.
Drag-and-drop and row editing are paused while search is active. Clear the query before restructuring the tree.
Custom cells and layout slots
SuperTree<T> requires a leading-cell builder and accepts an optional trailing
builder. Both receive TreeRowInfo:
leadingBuilder: (context, node, info) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(info.hasChildren ? Icons.folder : Icons.description),
const SizedBox(width: 8),
Text(node.code),
],
);
},
trailingBuilder: (context, node, info) {
return Text(node.value?.statusLabel ?? '');
},
TreeRowInfo exposes:
| Property | Description |
|---|---|
depth |
Zero-based depth of the node. |
open |
Whether the branch is currently rendered expanded. |
hasChildren |
Whether the node is a branch. |
Use above for content above the search toolbar and toolbarExtra for an
additional row beneath it:
SuperTree<Project>(
controller: controller,
leadingBuilder: buildLeading,
above: const ProjectSummary(),
toolbarExtra: const ProjectFilters(),
);
SuperTree configuration
| Parameter | Purpose |
|---|---|
controller |
Required state controller. |
leadingBuilder |
Required leading-cell builder. |
trailingBuilder |
Optional trailing-cell builder. |
accent |
Overrides the theme accent for tree interactions. |
title |
Tree-card title. |
subtitle |
Optional descriptive subtitle. |
titleIcon |
Optional title icon. |
nameColumnLabel |
Main-column heading. |
trailingColumnLabel |
Trailing-column heading. |
placeholder |
Search-field hint. |
samples |
Search suggestion chips. |
unit |
Noun used in leaf-count summaries. |
showArabic |
Shows or hides TreeNode.ar. |
showLeafCount |
Shows or hides branch leaf counts. |
selectionLabel |
Label used in the selected/opened summary. |
enableEditing |
Displays built-in editing controls. |
above |
Widget rendered above the toolbar. |
toolbarExtra |
Additional toolbar content. |
Keyboard navigation
Focus the tree, then use:
| Key | Action |
|---|---|
Arrow Up / Arrow Down |
Move between visible rows. |
Arrow Right |
Expand or move toward children in LTR. Mirrored in RTL. |
Arrow Left |
Collapse or move toward the parent in LTR. Mirrored in RTL. |
Home / End |
Move to the first or last visible row. |
Enter |
Open a leaf or toggle a branch. |
Space |
Toggle the focused checkbox when selection is enabled; otherwise activate the row. |
/ |
Focus the search field. |
Escape |
Clear the query while the search field is focused. |
* |
Expand all branches. |
\ |
Collapse all branches. |
? |
Open the keyboard-shortcuts dialog. |
| Right-click / long-press | Open the node context menu. |
The horizontal arrow behavior is resolved through Directionality, so tree
navigation remains visually correct in RTL layouts.
Chart of accounts
AccountTree is a specialized SuperTree<AccountData> implementation:
AccountTree(
roots: myAccounts,
onOpenAccount: (account) {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => AccountLedgerPage(accountCode: account.code),
),
);
},
);
Omit roots to use AccountTreeData.tree:
const AccountTree();
The built-in account tree includes:
- Five account types: asset, liability, equity, income, and expense.
- Debit and credit account nature.
- English and Arabic account names.
- Leaf balances and recursive group roll-ups.
- Total assets, liabilities, equity, and net-income KPI cards.
- An assets-versus-liabilities-plus-equity balance indicator.
- Account-type filters.
- A balance share bar for each node.
- Read and edit modes.
AccountTree labels monetary values as SAR. Build a custom
SuperTree<AccountData> when the product requires another currency, formatter,
column layout, or persistence workflow.
A ready-to-route page is also exported:
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const AccountTreeDemo(),
),
);
Account domain types
const account = AccountData(
type: AccountType.asset,
balance: 125000,
);
final nature = AccountType.asset.nature; // AccountNature.debit
final code = nature.code; // DR
AccountType.ordered returns the stable display order used by the built-in
filters and KPI layout.
Tree algorithms
TreeLogic contains stateless hierarchy operations and can be used without the
SuperTree widget:
final leafCount = TreeLogic.leafCount(root);
final leafCodes = TreeLogic.leafCodes(root);
final total = TreeLogic.rollup(root, (node) => node.value?.amount ?? 0);
final groups = TreeLogic.groupCodes(roots, maxDepth: 2);
final filtered = TreeLogic.filter(roots, 'sales', searchText);
final matches = TreeLogic.countMatches(roots, 'sales', searchText);
final visible = TreeLogic.flattenVisible(roots, expandedCodes, false);
final parent = TreeLogic.parentOf(roots, nodeCode);
Immutable edit operations:
final renamed = TreeLogic.mapNode(
roots,
nodeCode,
(node) => node.renamed('Updated'),
);
final withoutNode = TreeLogic.removeNode(roots, nodeCode);
final withChild = TreeLogic.insertChild(roots, parentCode, child);
final withSibling = TreeLogic.insertSibling(
roots,
targetCode,
sibling,
after: true,
);
final moved = TreeLogic.moveNode(
roots,
dragCode,
targetCode,
DropPosition.inside,
);
These operations return new tree lists rather than mutating the supplied hierarchy.
RTL and bilingual labels
Place the tree below a Directionality or a localized MaterialApp:
Directionality(
textDirection: TextDirection.rtl,
child: SuperTree<MyItem>(
controller: controller,
showArabic: true,
leadingBuilder: buildLeading,
),
);
TreeNode.ar is a secondary Arabic label. Setting showArabic: false hides it.
The package mirrors directional indentation and horizontal keyboard navigation,
but its built-in toolbar and action labels are English strings. Application-wide
localization of those labels requires a custom wrapper or package-level string
customization.
Responsive usage
The toolbar uses wrapping layouts and the account KPI cards adjust their column
count based on available width. For predictable component density, generate the
super_core theme with the matching SuperDeviceMode:
final mode = SuperDeviceMode.forWidth(width);
final textTheme = SuperTextTheme(
isDesktop: mode == SuperDeviceMode.desktop,
);
final lightTheme = SuperMaterialThemeData.light(
palette: SuperPalette.purplePalette,
mode: mode,
textTheme: textTheme,
primaryTextTheme: textTheme,
);
For narrow screens, place the tree in a vertically scrollable page and provide
sufficient horizontal space for custom trailing columns. Keep custom cells
compact and prefer TextOverflow.ellipsis for unbounded labels.
Public API overview
Domain
| API | Purpose |
|---|---|
TreeNode<T> |
Generic immutable hierarchy node. |
AccountData |
Account payload containing type and balance. |
AccountType |
Asset, liability, equity, income, and expense metadata. |
AccountNature |
Debit or credit nature. |
DropPosition |
Before, inside, or after drag-and-drop position. |
SearchText<T> |
Search-text callback type. |
LeafValue<T> |
Numeric leaf-value callback type. |
TreeLogic |
Pure hierarchy query and edit operations. |
State
| API | Purpose |
|---|---|
SuperTreeController<T> |
Expansion, focus, search, selection, and editing state. |
SuperTreeMode |
Readable or editable interaction mode. |
SuperTreeSelectionMode |
None, single, or multi-selection. |
TreeCheckState |
Unchecked, partial, or checked state. |
Widgets and helpers
| API | Purpose |
|---|---|
SuperTree<T> |
Generic tree shell and interaction view. |
TreeRow<T> |
Low-level recursive node-row widget. |
TreeRowInfo |
Row depth, expansion, and branch metadata. |
TreeSlotBuilder<T> |
Leading-cell builder type. |
TreeTrailingBuilder<T> |
Trailing-cell builder type. |
TreeCheckbox |
Themed tristate checkbox. |
HighlightText |
Highlights a matching search substring. |
showTreeContextMenu<T> |
Opens the mode-aware node context menu. |
showShortcutsHelp |
Opens the keyboard-shortcuts dialog. |
AccountTree |
Ready-made chart-of-accounts tree. |
AccountTreeDemo |
Scaffolded chart-of-accounts page. |
KpiCard |
Financial summary card used by AccountTree. |
NaturePill |
Debit/credit indicator. |
AccountTreeData |
Bundled sample account hierarchy. |
Most applications should compose SuperTree<T> instead of constructing
TreeRow<T> directly.
Flutter usage guidelines
- Create long-lived controllers in
initState, not insidebuild. - Dispose every controller created by the host widget.
- Keep every
TreeNode.codeunique and stable across rebuilds and persistence. - Treat
TreeNodevalues and root lists as immutable application state. - Persist
onTreeChangedoutput instead of reading private widget state. - Include codes, translated labels, and domain keywords in
searchText. - Keep expensive calculations outside cell builders when the tree is large.
- Use
constconstructors for static nodes and widgets where possible. - Keep leading and trailing builders small to reduce rebuild cost.
- Test keyboard focus, RTL navigation, selection cascades, and edit persistence.
- Provide meaningful tooltips or semantics for custom icon-only controls.
Testing
A widget test can create a controller, pump SuperTree, then interact with
labels or controller methods:
testWidgets('filters nodes through the controller', (tester) async {
final controller = SuperTreeController<FileItem>(
roots: files,
searchText: (node) => node.name,
);
addTearDown(controller.dispose);
final textTheme = SuperTextTheme();
await tester.pumpWidget(
MaterialApp(
theme: SuperMaterialThemeData.light(
textTheme: textTheme,
primaryTextTheme: textTheme,
),
home: Scaffold(
body: SuperTree<FileItem>(
controller: controller,
showArabic: false,
leadingBuilder: (context, node, info) {
return const Icon(Icons.description_outlined);
},
),
),
),
);
controller.setQuery('README');
await tester.pump();
expect(find.text('README.md'), findsOneWidget);
expect(find.text('main.dart'), findsNothing);
});
Prefer domain tests for TreeLogic and widget tests for focus, keyboard,
selection, editing controls, and rendered labels.
License
See LICENSE.
Libraries
- super_tree
- Super Tree — a GeniusLink design-system Flutter package providing SuperTree, a recursive, generic, keyboard-first hierarchy component.