flutter_tray

GitHub Repo stars Pub Version Pub Likes Pub Points

Flutter Tray is an opinionated bottom-sheet flow for Flutter. Give it a widget. When that widget changes, the tray changes with it—content, height, action, and all.

There are already plenty of bottom sheet packages. Here comes another one. This one is for compact flows that should feel connected instead of becoming a pile of modal pages.

Install

flutter pub add flutter_tray

Usage

import 'package:flutter_tray/flutter_tray.dart';

await showFlutterTray<void>(
  context: context,
  presentation: FlutterTrayPresentation.floating,
  page: FlutterTrayPageSpec(
    builder: (_) => const FlutterTrayPage(
      title: Text('Choose a vehicle'),
      child: VehiclePicker(),
    ),
    primaryActionBuilder: (trayContext) => MyBrandButton(
      label: 'Continue',
      onPressed: () => Navigator.of(trayContext).pop(),
    ),
  ),
  bottomActionWrapperBuilder: (_, action) =>
      FlutterTrayActionBar(child: action),
);

MyBrandButton can be anything. Material, Cupertino, a painted gesture surface, your design system's primary control—Flutter Tray only asks for a widget.

Features

  • Content-sized transitions – The tray grows and shrinks with its child
  • Tray pages – Push keyed pages without mounting another Navigator
  • Stationary actions – Change the page while its primary action stays put
  • Your buttons – Use arbitrary widgets for one action or an entire action bar
  • Floating mode – Fully rounded, inset, keyboard-aware presentation
  • Detents – Content, pixel, and viewport offsets with snap grids
  • Dismissal policy – Gate barrier, swipe, and back dismissal attempts
  • Keyboard policy – Dismiss on upward, downward, or any tray drag
  • Scaffold slots – Optional top bar, body, and page-local bottom bar
  • Reduced motion – Respects MediaQuery.disableAnimations
  • No runtime dependencies – Just Flutter

Gradual revelation

Full-screen navigation is useful when someone is going somewhere new. A tray is useful when they are still doing the thing in front of them.

Flutter Tray keeps that context visible. A confirmation can unfold above the screen that created it. A warning can become a choice. A short choice can become a form. Each step appears inside the same physical surface, so the user can see the flow changing instead of feeling transported somewhere else.

The height change is part of the navigation. A taller next page makes progress visible. A smaller page brings the flow back down. Content, action, and shape follow a few shared rules, which is what lets a multi-step tray feel like one continuous object.

The default structure is deliberately small:

  • one focused page at a time;
  • one optional stationary primary action;
  • one keyed transition between page states;
  • one surface that adapts to its content.

Those are opinions, not restrictions. Place any widget inside when the ready-made page is useful. Own the entire child when it is not.

Change pages

Every page can own its content, motion, action, and action visibility.

FlutterTray.of(context).pushPage(
  FlutterTrayPageSpec(
    key: const ValueKey('confirmation'),
    builder: (_) => const FlutterTrayPage(
      title: Text('Confirm transfer'),
      child: TransferSummary(),
    ),
    primaryActionBuilder: (context) => TransferButton(
      onPressed: () => confirmTransfer(),
    ),
    actionVisibility: const FlutterTrayActionVisibility.keyboardHidden(),
  ),
);

Use FlutterTray.of(context).pop() to return to the previous page. The outgoing content, incoming content, height, and stationary action use the same transition system.

Bring your own layout

The package does not need to own the title, spacing, or button.

await showFlutterTray<void>(
  context: context,
  presentation: FlutterTrayPresentation.floating,
  builder: (_) => const MyEntireTray(),
);

FlutterTrayPage, FlutterTrayActionBar, and FlutterTrayScaffold are conveniences. The animation works without them.

Floating trays

Floating mode is fully rounded and inset by default.

await showFlutterTray<void>(
  context: context,
  presentation: FlutterTrayPresentation.floating,
  floatingMargin: const EdgeInsets.all(16),
  floatingKeyboardMargin: const EdgeInsets.fromLTRB(16, 0, 16, 32),
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(36),
  ),
  builder: (_) => const MyTrayContent(),
);

Surface color, elevation, clip behavior, margins, shape, and drag handle are all replaceable independently.

Detents

Use typed detents when the tray should also be user-resizable.

final controller = FlutterTrayController();

await showFlutterTray<void>(
  context: context,
  controller: controller,
  presentation: FlutterTrayPresentation.floating,
  detents: const FlutterTrayDetentConfiguration(
    initialDetent: FlutterTrayDetent.content(),
    snapGrid: FlutterTraySnapGrid.detents(
      detents: [
        FlutterTrayDetent.pixels(240),
        FlutterTrayDetent.viewport(fraction: 0.75),
      ],
    ),
  ),
  builder: (_) => const ResultsList(),
);

The floating handle owns detent dragging, so scrollable content keeps its own gesture space. Use animateToDetent or jumpToDetent for programmatic control. FlutterTraySnapGrid.stepless keeps the final dragged height instead of snapping to a fixed list.

Dismissal and keyboard

await showFlutterTray<void>(
  context: context,
  dismissPolicy: FlutterTrayDismissPolicy(
    onDismissAttempt: (context) async => confirmDiscard(context),
  ),
  keyboardDismissBehavior:
      const FlutterTrayKeyboardDismissBehavior.onDragDown(
    isContentScrollAware: true,
  ),
  builder: (_) => const EditProfileForm(),
);

Dismissal attempts can be allowed synchronously or asynchronously. Use FlutterTrayDismissPolicy.locked() for a tray that must close through an explicit app-owned action.

Requirements

  • Dart 3.12.2+
  • Flutter 3.44.6+

Acknowledgements

Flutter Tray's language of trays and gradual revelation was inspired by Family's dynamic tray system.

Thanks to smooth_sheets for its thoughtful work on Flutter sheet primitives. Flutter Tray's detents, snap grids, dismissal policy, and keyboard behavior are original implementations informed by those ideas.

License

© 2026 Eric Noel

Licensed under MIT

Libraries

flutter_tray
An opinionated Flutter tray that follows animated, changing content.