AppShell
Compose responsive header, sidebar, rail, and mobile navigation regions.
Compose typed header, sidebar, main, and outline parts with responsive rail or modal-drawer navigation, route progress, and scroll restoration.
Contract
| Axis | Contract |
|---|---|
| Header height | In application chrome TRAppShellHeader rests at TRMeasurements.headerHeight, and one TRSpacing.large step taller under comfortable density, so it agrees with TRPaneHeader. That is a resting height rather than a cap: taller content, such as a title that wraps at an enlarged text scale, grows the bar instead of being clipped. Pass height to fix the bar to an exact height. |
Install
Add the package, then import its public library.
flutter pub add tinyrack_uiimport 'package:tinyrack_ui/tinyrack_ui.dart';Playground
Usage
import 'package:material_ui/material_ui.dart';
import 'package:tinyrack_ui/tinyrack_ui.dart';
TRAppShell(
breakpoint: TRAppShellBreakpoint.sm,
layout: TRAppShellLayout.sidebarFirst,
controller: controller,
header: TRAppShellHeader(children: [brand, actions]),
sidebar: TRAppShellSidebar(child: navigation),
main: const TRAppShellMain(child: Workspace()),
)Parts and state
TRAppShell accepts TRAppShellHeader, TRAppShellSidebar, TRAppShellMain, and an optional TRAppShellOutline. Brand and Actions align header content, while SidebarLabel keeps its accessible name when visually hidden in rail mode. Use one TRAppShellController when mobileOpen and sidebarMode must be externally controlled.
final controller = TRAppShellController(
mobileOpen: false,
sidebarMode: TRAppShellSidebarMode.expanded,
);
TRAppShell(
controller: controller,
onMobileOpenChanged: handleOpen,
onSidebarModeChanged: handleSidebarMode,
header: TRAppShellHeader(children: [
TRAppShellTrigger(
icon: const Icon(Icons.menu),
label: 'Open navigation',
),
TRAppShellBrand(child: const Text('Orbit Ops')),
]),
sidebar: TRAppShellSidebar(child: navigation),
main: const TRAppShellMain(child: Workspace()),
)Responsive layout and focus
sm and lg resolve at 768px and 1024px viewport boundaries. headerFirst spans the header across the shell; sidebarFirst spans the sidebar from top to bottom. The mobile drawer is a Navigator route from the logical start or end edge, so backdrop taps, Escape, and system Back dismiss it, focus stays inside, and focus returns to the trigger. Set mobileSidebar to rail to keep a 64px navigation strip visible.
Adaptive pane layouts
Compose TRAdaptiveNavigationLayout and TRAdaptiveListDetailLayout to show content alone below 600px, navigation with content from 600–1199px, and navigation, collection, and detail at 1200px and wider. Neither layout owns navigation state. Let Navigator and Page handle history, system Back, Android predictive Back, and interrupted transitions. Pass the same keyed content Navigator as singlePane and detailPane to preserve its state through breakpoint changes. Descendant panes read the complete viewport width class from TRAdaptiveLayoutScope.
final contentNavigator = Navigator(
key: contentNavigatorKey,
pages: contentPages,
onDidRemovePage: handleRemovedPage,
);
TRAdaptiveNavigationLayout(
navigationPane: TRNavigationPane(children: navigationSections),
contentPane: TRAdaptiveListDetailLayout(
singlePane: contentNavigator,
collectionPane: const ProjectList(),
detailPane: contentNavigator,
),
)Software keyboard
TRAppShell keeps the header, sidebar, and main above the software keyboard by default while its background continues to paint to the viewport edge. Set resizeToAvoidBottomInset to false only when interactive content must deliberately render behind the keyboard.
TRAppShell(
resizeToAvoidBottomInset: false,
main: const TRAppShellMain(child: ImmersiveCanvas()),
)Sidebar width and collapse
TRAppShellSidebar owns its own width. It defaults to the shell width for the current mode and accepts width as an override. Every width change — expanding, collapsing, or switching to the rail — animates over TRMotion.normal while the content stays laid out at its target width and is clipped. Setting collapsed drops the surface out of focus, pointers, and semantics as the collapse starts, then removes it from the tree once the animation ends. Do not wrap it in a fixed-width SizedBox. Disabling animations in the platform accessibility settings makes the change instant.
TRAppShell(
sidebar: TRAppShellSidebar(
collapsed: navigationCollapsed,
child: navigation,
),
main: const TRAppShellMain(child: Workspace()),
)Docs chrome and scrolling
docs provides a 48px header, optional outline, and route progress. Main becomes busy while pendingPath differs from currentPath. container uses Main’s Tinyrack scroll area; primary uses the current Flutter route’s primary scroller. PUSH and REPLACE move to the top, POP restores the locationKey offset, and hash moves to a GlobalKey registered in anchorTargets.
TRAppShell(
chrome: TRAppShellChrome.docs,
currentPath: route.path,
pendingPath: navigation.pendingPath,
locationKey: route.key,
navigationKind: TRAppShellNavigationKind.pop,
hash: route.hash,
anchorTargets: {'install': installHeadingKey},
pageScroll: TRAppShellPageScroll.container,
header: TRAppShellHeader(children: [brand, actions]),
sidebar: TRAppShellSidebar(child: docsNavigation),
outline: TRAppShellOutline(child: tableOfContents),
main: TRAppShellMain(scroll: true, child: article),
)Examples
Control appearances permalink
Trigger, Close, and SidebarToggle use 32px small controls and accept the shared solid, outline, and ghost appearances.
Docs chrome and scroll restoration permalink
Docs chrome adds a 48px header and route progress while Main owns a named container scroller.
Shared pane chrome permalink
PaneHeader aligns leading navigation, density-aware headings, supporting text, wrapping actions, and the body divider.
API
AppShell preserves native Flutter state and callbacks while applying Tinyrack token defaults.