Kanban
Cross-platform extension. iOS 26 has no native kanban primitive — iPadOS Reminders ships a column-based view but iOS itself does not. The closest references are shadcn/ui and forui.dev. liqkit_ui ships this rendered in iOS 26 visual language (Liquid Glass surfaces, San Francisco type, iOS color palette) so it composes cleanly with the rest of the library, but the interaction model itself is web-native rather than iOS-canonical.
LiqKanban is a Trello-style board: N fixed-width columns laid out
horizontally, each holding an ordered list of cards. The user drags a
card from one column to another, or to a different slot in the same
column, and the parent receives an onMove(cardId, fromColumnId, toColumnId, toIndex) callback to update its own state.
The widget is fully stateless — the parent owns the card list and
column membership, LiqKanban only renders the layout and emits move
events. Drag-and-drop is implemented with Flutter's built-in
Draggable and DragTarget, so it works everywhere Flutter does (web,
desktop, mobile).
Scope is deliberately narrow: a single dragged card at a time, no inline edit, no card-creation UI, no swimlanes. Compose your own controls around the board for richer behaviour.
Default
// ignore_for_file: file_names // hyphenated name required by snippet manifest conventionimport 'package:docs_snippets/src/snippet_frame.dart';import 'package:flutter/widgets.dart';import 'package:liqkit_ui/liqkit_ui.dart';/// Snippet builder consumed by `apps/docs_snippets/lib/src/routes.g.dart`.Widget kanbanDefaultBuilder(BuildContext context) { return const SnippetFrame( maxWidth: 700, child: _KanbanDemo(), );}class _KanbanDemo extends StatefulWidget { const _KanbanDemo(); @override State<_KanbanDemo> createState() => _KanbanDemoState();}class _KanbanDemoState extends State<_KanbanDemo> { final Map<String, LiqKanbanCard> _cards = <String, LiqKanbanCard>{ 'a': const LiqKanbanCard(id: 'a', child: Text('Design system audit')), 'b': const LiqKanbanCard(id: 'b', child: Text('Add Phase 5 components')), 'c': const LiqKanbanCard(id: 'c', child: Text('Cloudflare Pages deploy')), 'd': const LiqKanbanCard(id: 'd', child: Text('Wire ⌘K search')), 'e': const LiqKanbanCard(id: 'e', child: Text('Pixel goldens')), }; List<LiqKanbanColumn> _columns = const <LiqKanbanColumn>[ LiqKanbanColumn(id: 'todo', title: 'TO DO', cardIds: <String>['a', 'c']), LiqKanbanColumn(id: 'doing', title: 'DOING', cardIds: <String>['b']), LiqKanbanColumn(id: 'done', title: 'DONE', cardIds: <String>['d', 'e']), ]; void _onMove( String cardId, String fromColumnId, String toColumnId, int toIndex, ) { setState(() { _columns = _columns.map((col) { if (col.id == fromColumnId && col.id == toColumnId) { final next = List<String>.from(col.cardIds)..remove(cardId); next.insert(toIndex.clamp(0, next.length), cardId); return LiqKanbanColumn( id: col.id, title: col.title, cardIds: next, ); } if (col.id == fromColumnId) { return LiqKanbanColumn( id: col.id, title: col.title, cardIds: List<String>.from(col.cardIds)..remove(cardId), ); } if (col.id == toColumnId) { final next = List<String>.from(col.cardIds); next.insert(toIndex.clamp(0, next.length), cardId); return LiqKanbanColumn( id: col.id, title: col.title, cardIds: next, ); } return col; }).toList(); }); } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final width = constraints.maxWidth.isFinite ? constraints.maxWidth : 700; final columnWidth = ((width - LiqKanban.defaultColumnSpacing * 2) / 3) .clamp(150.0, LiqKanban.defaultColumnWidth); return SizedBox( height: 320, child: LiqKanban( columns: _columns, cards: _cards, columnWidth: columnWidth, onMove: _onMove, ), ); }, ); }}
Dense
// ignore_for_file: file_names // hyphenated name required by snippet manifest conventionimport 'package:docs_snippets/src/snippet_frame.dart';import 'package:flutter/widgets.dart';import 'package:liqkit_ui/liqkit_ui.dart';/// Snippet builder consumed by `apps/docs_snippets/lib/src/routes.g.dart`.Widget kanbanDenseBuilder(BuildContext context) { return const SnippetFrame( maxWidth: 760, child: _KanbanDenseDemo(), );}class _KanbanDenseDemo extends StatefulWidget { const _KanbanDenseDemo(); @override State<_KanbanDenseDemo> createState() => _KanbanDenseDemoState();}class _KanbanDenseDemoState extends State<_KanbanDenseDemo> { final Map<String, LiqKanbanCard> _cards = <String, LiqKanbanCard>{ '1': const LiqKanbanCard(id: '1', child: Text('Triage inbox')), '2': const LiqKanbanCard(id: '2', child: Text('Audit color tokens')), '3': const LiqKanbanCard(id: '3', child: Text('Spec kanban v1')), '4': const LiqKanbanCard(id: '4', child: Text('Implement DragTarget gaps')), '5': const LiqKanbanCard(id: '5', child: Text('Snippet manifests')), '6': const LiqKanbanCard(id: '6', child: Text('Generate routes.g.dart')), '7': const LiqKanbanCard(id: '7', child: Text('Pixel-perfect golden')), '8': const LiqKanbanCard(id: '8', child: Text('Ship to docs site')), }; List<LiqKanbanColumn> _columns = const <LiqKanbanColumn>[ LiqKanbanColumn( id: 'backlog', title: 'BACKLOG', cardIds: <String>['1', '2'], ), LiqKanbanColumn(id: 'todo', title: 'TO DO', cardIds: <String>['3', '4']), LiqKanbanColumn(id: 'doing', title: 'DOING', cardIds: <String>['5', '6']), LiqKanbanColumn(id: 'done', title: 'DONE', cardIds: <String>['7', '8']), ]; void _onMove( String cardId, String fromColumnId, String toColumnId, int toIndex, ) { setState(() { _columns = _columns.map((col) { if (col.id == fromColumnId && col.id == toColumnId) { final next = List<String>.from(col.cardIds)..remove(cardId); next.insert(toIndex.clamp(0, next.length), cardId); return LiqKanbanColumn( id: col.id, title: col.title, cardIds: next, ); } if (col.id == fromColumnId) { return LiqKanbanColumn( id: col.id, title: col.title, cardIds: List<String>.from(col.cardIds)..remove(cardId), ); } if (col.id == toColumnId) { final next = List<String>.from(col.cardIds); next.insert(toIndex.clamp(0, next.length), cardId); return LiqKanbanColumn( id: col.id, title: col.title, cardIds: next, ); } return col; }).toList(); }); } @override Widget build(BuildContext context) { return SizedBox( height: 320, child: LiqKanban( columns: _columns, cards: _cards, onMove: _onMove, columnWidth: 180, ), ); }}