Flutter Motion Kit

Previewable Flutter animations + pitfall guide · one-click reuse from Claude Code

← Back

Draggable bottom sheet

explicit · difficulty 3/5 · verified Flutter 3.32 / 2026-06

A bottom sheet the user can drag between heights and then scroll its contents. DraggableScrollableSheet hands the builder a ScrollController that composes the drag and the inner scroll so the hand-off feels seamless. Adds snap points.

▶︎ Running for real (Flutter 3.32 · self-hosted, not a recording)

Code

// ✅ Recommended: a DraggableScrollableSheet whose inner ListView uses the builder's
// ScrollController, so dragging the sheet and scrolling its content compose cleanly.
// Snap points give it discrete resting heights. Scroll-driven — no AnimationController.
// Paste straight into DartPad (https://dartpad.dev) to run.
import 'package:flutter/material.dart';

void main() => runApp(const _App());

class _App extends StatelessWidget {
  const _App();
  @override
  Widget build(BuildContext context) => MaterialApp(
    debugShowCheckedModeBanner: false,
    theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.teal),
    home: const _Demo(),
  );
}

class _Demo extends StatelessWidget {
  const _Demo();

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Scaffold(
      appBar: AppBar(title: const Text('Draggable bottom sheet')),
      // A simple page behind the sheet.
      body: Stack(
        children: [
          const Center(child: Text('Drag the sheet up, then scroll it')),
          // ✅ expand:true makes the sheet fill the Stack and align to the bottom.
          DraggableScrollableSheet(
            // ✅ min <= initial <= max, otherwise it asserts.
            initialChildSize: 0.4,
            minChildSize: 0.25,
            maxChildSize: 0.9,
            expand: true,
            // ✅ snap to discrete heights instead of resting wherever the finger lifts.
            snap: true,
            snapSizes: const [0.25, 0.4, 0.9],
            builder: (context, scrollController) {
              return DecoratedBox(
                decoration: BoxDecoration(
                  color: theme.colorScheme.surfaceContainerHighest,
                  borderRadius: const BorderRadius.vertical(
                    top: Radius.circular(20),
                  ),
                ),
                // ✅ Hand the provided controller to the inner scrollable so the
                // drag-to-expand then scroll-content hand-off works seamlessly.
                child: ListView.builder(
                  controller: scrollController,
                  padding: const EdgeInsets.only(top: 8),
                  itemCount: 31,
                  itemBuilder: (context, i) {
                    if (i == 0) {
                      // A drag handle at the top of the sheet.
                      return Center(
                        child: Container(
                          width: 40,
                          height: 4,
                          margin: const EdgeInsets.only(bottom: 12),
                          decoration: BoxDecoration(
                            color: theme.colorScheme.onSurfaceVariant,
                            borderRadius: BorderRadius.circular(2),
                          ),
                        ),
                      );
                    }
                    return ListTile(
                      leading: CircleAvatar(child: Text('$i')),
                      title: Text('Item $i'),
                    );
                  },
                ),
              );
            },
          ),
        ],
      ),
    );
  }
}

⚠️ Pitfalls (4)

The inner scrollable must use the ScrollController the builder hands you; using your own controller breaks the drag-to-expand-then-scroll hand-off.
✅ Pass the builder's scrollController straight into the ListView/CustomScrollView controller parameter.
official-docs · source
initialChildSize must lie within [minChildSize, maxChildSize] or the widget fails an assertion at build time.
✅ Keep the three fractions ordered, e.g. min 0.25 <= initial 0.4 <= max 0.9.
official-docs · source
Without snap the sheet rests at whatever height the finger leaves it, which feels imprecise for a few discrete stops.
✅ Set snap:true and list snapSizes (each must be within [min,max]) to snap between defined heights.
official-docs · source
With expand:true the sheet sizes to its parent, so it needs a bounded ancestor such as a Stack to overlay the page from the bottom.
✅ Put the DraggableScrollableSheet inside a Stack (or the Scaffold body) so it fills and aligns to the bottom.
official-docs · source

Official docs