grabber_sheet 1.2.0
grabber_sheet: ^1.2.0 copied to clipboard
A customizable draggable bottom sheet inspired by modal sheets in popular apps. Solves gesture conflicts with an independent grabber and smooth snapping. The ultimate DraggableScrollableSheet alternative.
grabber_sheet #
Table of Contents #
- Features
- Getting started
- Compatibility
- Basic Usage
- Advanced Customization
- Properties
- Additional information
A reusable and customizable draggable bottom sheet with a grabber handle, inspired by the modal sheets in popular apps like Google Maps.
It solves the common frustrations with Flutter's built-in DraggableScrollableSheet:
- Independent Grabber Area: The grabber and header are separate from the scrollable content. Dragging the header moves the sheet; scrolling the content scrolls the list. No more gesture conflicts.
- Predictable Snapping: Easily snap to specific heights without complex controller logic.
- Seamless Integration: Works perfectly with
ListView,SingleChildScrollView, and other scrollable widgets.
Why use GrabberSheet? #
If you've ever struggled with:
- The grabber disappearing when you scroll down.
- The sheet not moving when you try to drag the header.
- Janky snapping animations.
Then GrabberSheet is the solution you've been looking for. It provides a robust, production-ready implementation of the "modal bottom sheet" pattern found in top-tier mobile apps.
Features #
- Draggable bottom sheet with a customizable grabber handle.
- Stable and predictable behavior, fixing common scroll controller conflicts.
- Use any widget as the content of the sheet via a
builder. - Insert a custom widget into the draggable grabber area.
- Customize sheet sizes (initial, min, max).
- Optional snapping behavior with custom snap points via
snapandsnapSizes. - Customize grabber style (color, size, shape).
- Grabber is automatically hidden on desktop and web platforms for a native feel.
- Sheet has rounded top corners by default (customizable via
borderRadius). - Programmatic Control: Use
GrabberSheetControllertomaximize(),minimize(), oranimateTo()specific sizes. - State Callbacks: Receive notifications on
onSizeChangedduring dragging/resizing andonSnapwhen the sheet settles at a snap point.
Getting started #
Add this to your package's pubspec.yaml file. Check the latest version on pub.dev.
dependencies:
grabber_sheet: ^1.2.0
Then, install it by running flutter pub get in your terminal.
Compatibility #
This package is compatible with the following SDK versions:
- Flutter:
>=3.0.0 - Dart:
>=3.0.0 <4.0.0
Basic Usage #
Here's a simple example of how to use GrabberSheet:
import 'package:flutter/material.dart';
import 'package:grabber_sheet/grabber_sheet.dart';
class ExampleHomePage extends StatelessWidget {
const ExampleHomePage({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final sheetColor = Colors.blue.shade100;
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('GrabberSheet Example'),
backgroundColor: sheetColor,
),
body: Stack(
children: [
Center(
child: Text(
'Background Content',
style: theme.textTheme.headlineMedium,
),
),
GrabberSheet(
initialChildSize: 0.5,
minChildSize: 0.2,
maxChildSize: 0.8,
snap: true,
snapSizes: const [.5],
backgroundColor: sheetColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)), // Custom border radius
grabberStyle: GrabberStyle(color: Colors.grey.shade400),
bottom: Row(
children: [
const Text('sheet title'),
const Spacer(),
IconButton(onPressed: () {}, icon: const Icon(Icons.close)),
],
),
bottomAreaPadding: const EdgeInsets.symmetric(horizontal: 16),
builder: (BuildContext context, ScrollController scrollController) {
return ListView.builder(
controller: scrollController,
itemCount: 30,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
'Item $index',
style: TextStyle(
color: theme.colorScheme.onSurface,
),
),
);
},
);
},
),
],
),
);
}
}
Advanced Customization #
Controlling Snap Behavior #
By setting snap to true, the sheet will automatically animate to the nearest defined snap point when a drag gesture ends. This creates a clean, predictable motion for the user.
The snapping logic behaves differently based on the drag gesture:
- Slow Drag: If the user slowly drags the sheet and releases, it will snap to the closest snap point.
- Fling: If the user "flings" the sheet with a quick gesture, it will animate to the nearest boundary (either
minChildSizeormaxChildSize) in the direction of the fling.
You can define the snap points using minChildSize, maxChildSize, and the optional snapSizes for any intermediate points.
Here is an example with multiple intermediate snap points:
GrabberSheet(
// Enable snapping
snap: true,
// Define snap points: 0.2 (min), 0.5, 0.8 (intermediate), and 1.0 (max)
minChildSize: 0.2,
maxChildSize: 1.0,
initialChildSize: 0.5,
snapSizes: const [0.5, 0.8],
builder: (context, scrollController) {
// ... your content
},
),
Customizing the Grabber #
The appearance of the grabber handle can be fully customized using the grabberStyle property.
GrabberSheet(
grabberStyle: GrabberStyle(
width: 60,
height: 6,
margin: const EdgeInsets.symmetric(vertical: 10),
color: Colors.grey.shade300,
radius: const Radius.circular(12),
),
builder: (context, scrollController) {
// ... your content
},
),
You can also hide the grabber completely by setting showGrabber: false.
Adding a Custom Widget to the Grabber Area #
You can insert a custom widget into the draggable area below the grabber handle using the bottom property. This entire area (handle and custom widget) is draggable. This is useful for adding a title, action buttons, or any other information that should remain visible and separate from the scrollable content.
The bottomAreaPadding property can be used to add padding around this custom widget.
GrabberSheet(
bottom: Row(
children: [
const Text('sheet title'),
const Spacer(),
IconButton(onPressed: () {}, icon: const Icon(Icons.close)),
],
),
bottomAreaPadding: const EdgeInsets.symmetric(horizontal: 16),
builder: (context, scrollController) {
// ... your list of locations
},
),
Programmatic Control & State Listeners #
You can use GrabberSheetController to control the sheet's position programmatically and listen for state changes to update the UI.
Key Methods:
maximize(): Smoothly animates the sheet to itsmaxChildSize.minimize(): Smoothly animates the sheet to itsminChildSize.animateTo(double size): Animates the sheet to a specificsize(fraction).
State Callbacks:
onSizeChanged: Triggered whenever the sheet's size changes (during dragging or animation). Useful for responsive UI updates based on drag progress.onSnap: Triggered when the sheet settles at a specific snap point.
The complete implementation can be found in example/lib/main.dart.
class _ExampleHomePageState extends State<ExampleHomePage> {
final GrabberSheetController _grabberSheetController = GrabberSheetController();
String _currentSheetStatus = 'Idle';
double _currentSize = 0.5;
@override
void initState() {
super.initState();
// 2. Listen to state changes (optional, for controller-based updates)
_grabberSheetController.addListener(() {
if (_grabberSheetController.isAttached && mounted) {
setState(() {
_currentSize = _grabberSheetController.size;
});
}
});
}
...
@override
Widget build(BuildContext context) {
...
return Scaffold(
...
floatingActionButton: Column(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton.small(
heroTag: 'maximize',
// Control: Maximize the sheet
onPressed: () => _grabberSheetController.maximize(),
tooltip: 'Maximize',
child: const Icon(Icons.keyboard_arrow_up),
),
...
FloatingActionButton.small(
heroTag: 'minimize',
// Control: Minimize the sheet
onPressed: () => _grabberSheetController.minimize(),
tooltip: 'Minimize',
child: const Icon(Icons.keyboard_arrow_down),
),
...
FloatingActionButton.small(
heroTag: 'animate',
// Control: Animate to a specific size (0.7)
onPressed: () => _grabberSheetController.animateTo(
0.7,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
),
tooltip: 'Animate to 0.7',
child: const Icon(Icons.height),
),
],
),
body: Stack(
children: [
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Background Content',
style: theme.textTheme.headlineMedium,
),
const SizedBox(height: 20),
Text('Sheet Size: ${_currentSize.toStringAsFixed(2)}'),
Text('Sheet Status: $_currentSheetStatus'),
],
),
),
GrabberSheet(
controller: _grabberSheetController, // Attach the controller
initialChildSize: 0.5,
minChildSize: 0.2,
maxChildSize: 0.8
...
onSizeChanged: (size) {
if (mounted) {
setState(() {
_currentSheetStatus = 'Dragging/Resizing';
_currentSize = size;
});
}
},
// Callback: Triggered when snapped
onSnap: (size) {
if (mounted) {
setState(() {
_currentSheetStatus = 'Snapped to ${size.toStringAsFixed(2)}';
_currentSize = size;
});
}
},
builder: (BuildContext context, ScrollController scrollController) {
return ListView.builder(
controller: scrollController,
itemCount: 30,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
'Item $index',
style: TextStyle(color: theme.colorScheme.onSurface),
),
);
},
);
},
),
],
),
);
}
}
Showing the Grabber on Desktop and Web #
By default, the grabber handle is only visible on mobile platforms (iOS, Android). However, you can force it to always be visible on desktop (Windows, macOS, Linux) and web platforms by setting the showGrabberOnNonMobile property to true. This is useful when you want to provide a consistent UI across all platforms.
GrabberSheet(
showGrabberOnNonMobile: true,
builder: (context, scrollController) {
// ... Your content
},
),
Properties #
GrabberSheet #
| Property | Type | Description | Default Value |
|---|---|---|---|
builder |
Widget Function(BuildContext, ScrollController) |
Required. Builds the scrollable content of the sheet. Provides a ScrollController to be used by the content. |
- |
initialChildSize |
double |
The initial fractional size of the sheet. | 0.5 |
minChildSize |
double |
The minimum fractional size of the sheet. | 0.25 |
maxChildSize |
double |
The maximum fractional size of the sheet. | 1.0 |
snap |
bool |
If true, the sheet will snap to the nearest snap point after dragging. | false |
snapSizes |
List<double>? |
A list of intermediate fractional sizes to snap to. | null |
showGrabber |
bool |
Whether to show the grabber handle. It is automatically hidden on desktop and web platforms regardless of this value. | true |
grabberStyle |
GrabberStyle |
The visual style of the grabber handle. | const GrabberStyle() |
bottom |
Widget? |
A custom widget to display below the grabber and above the main content. | null |
bottomAreaPadding |
EdgeInsetsGeometry? |
The padding for the bottom widget area. |
null |
backgroundColor |
Color? |
The background color of the sheet container. If null, it uses the theme's colorScheme.surface. |
Theme.of(context).colorScheme.surface |
borderRadius |
BorderRadiusGeometry? |
The border radius of the sheet. If null, defaults to a top-left and top-right radius of 16.0. | const BorderRadius.vertical(top: Radius.circular(16.0)) |
controller |
GrabberSheetController? |
An optional controller to programmatically control the sheet's size and state. Provides maximize(), minimize(), and all DraggableScrollableController methods. |
null |
onSizeChanged |
ValueChanged<double>? |
Callback that is called whenever the sheet's fractional size changes (during dragging or animation). | null |
onSnap |
ValueChanged<double>? |
Callback that is called when the sheet completes a snap animation and settles at a specific fractional size. | null |
GrabberStyle #
| Property | Type | Description | Default Value |
|---|---|---|---|
color |
Color |
The background color of the grabber handle. | Colors.grey |
width |
double |
The width of the grabber handle. | 48.0 |
height |
double |
The height of the grabber handle. | 5.0 |
radius |
Radius |
The border radius of the grabber handle's corners. | const Radius.circular(8.0) |
margin |
EdgeInsetsGeometry |
The margin surrounding the grabber handle. | const EdgeInsets.symmetric(vertical: 10.0) |
Additional information #
To file issues, request features, or contribute, please visit the GitHub repository.