Library / SDK
mcrovero/rubber avatar
mcrovero/rubber

rubber: an elastic bottom sheet for Flutter with configurable bounds

An elastic material bottom sheet implementation for Flutter.

564 stars89 forksDartBSD-2-Clause

At a glance

What is it?
rubber is a Flutter package that reimplements the elastic sheet pattern with a two-layer structure, fling velocities, a snap threshold and a choice between strict bounds and rubber overscroll. It suits a Flutter developer who wants physics-driven sheet motion rather than fixed animation durations, and it is an early preview requiring Flutter 3.44 and Dart 3.12.
Who is it for?
rubber fits a Flutter team that wants a bottom sheet whose motion is driven by physics and whose boundaries the user can configure, and it does not fit a project pinned to an older Flutter or Dart, since it requires Flutter 3.44 or later and Dart 3.12 or later.
Can I use it commercially?
Yes. BSD-2-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 40 days ago.
What is it written in?
Mainly Dart, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A bottom sheet with a physical model rather than a fixed animation

rubber is an elastic bottom sheet implementation for Flutter, and the phrase that matters is elastic.

The sheet pattern Flutter provides by default is a specific interaction: a panel anchored to the bottom of the screen that can be dragged, snapped to one of a few positions, and dismissed by a fling. The usual implementation drives that with an animation controller and fixed durations, which means the motion has the same timing whatever the gesture was. A quick flick and a slow drag end with the same curve applied to the same distance.

rubber replaces that with something that behaves like a physical sheet. The package name is the giveaway, and the option names in the readme confirm it: there is a minimum fling velocity and a complete fling velocity, and those are physical quantities rather than animation constants. A slow drag moves the sheet a little. A fast flick completes it. There is a snap threshold, expressed as a fraction, which decides the point at which a drag commits to a position rather than returning to where it started.

The requirements are stated at the top of the readme: Flutter 3.44 or later and Dart 3.12 or later. The primary language is Dart, the licence is BSD-2-Clause, the default branch is master, and the last push was on 2026-09-13. The repository has no GitHub releases, so the version is whatever the package registry gives you.

The project also states its own status plainly. This is still an early preview, some behaviors can change or being removed, and feedback, bug reports and feature requests are welcome through the issue tracker. That is a more useful sentence than a version number, because it tells you the API surface is not settled.

For getting started, the readme points at a Quick Start page in the project wiki, and the complete example is in the repository under example/ so you can preview the library on a device.

The two-layer structure, and what each layer is for

The configuration example shows the shape of the API, and the first thing it reveals is that the sheet is built from two layers.

dart
RubberBottomSheet(
  animationController: controller,
  lowerLayer: const MyBackground(),
  upperLayer: const MySheetContent(),
  draggable: true,
  snapThreshold: 0.4,
  minFlingVelocity: 700,
  completeFlingVelocity: 5000,
  scrimColor: Colors.black54,
  onScrimTap: controller.collapse,
);

The lower layer is what sits behind the sheet, and the upper layer is the sheet content itself. Separating them is what makes it possible to have the content move with the sheet while the background stays put, which is the visual difference between a bottom sheet and a dialog that happens to be anchored at the bottom.

The controller is passed in rather than created internally, and that is the standard Flutter pattern for an animation-driven widget: the caller owns the AnimationController and its TickerProvider, so the lifetime is tied to the widget that created it and the sheet can be driven from outside.

draggable: true is the switch that enables dragging, which implies it can be turned off, which in turn implies the sheet can be a purely programmatic thing with no gesture handling at all. That is the mode you want for a sheet that opens and closes on a state change rather than a gesture.

snapThreshold: 0.4 is the fraction of the travel that decides whether a drag snaps open or snaps closed, so a value of 0.4 means the sheet commits to the open position once it has travelled 40 percent of the way. minFlingVelocity: 700 and completeFlingVelocity: 5000 are the two thresholds that separate a small fling from a completing one, in the units Flutter uses for velocity, so the difference between them is the range in which a fast drag produces a partial movement rather than a full dismissal.

The scrim is the dimmed overlay behind the sheet, with scrimColor: Colors.black54 being the standard Material scrim at 54 percent black. onScrimTap is a callback rather than a boolean, and the example wires it to controller.collapse, so tapping the background collapses the sheet. Because it is a callback, you can make the scrim non-dismissible by passing a callback that does nothing, which is a small design decision with real consequences for a modal sheet.

Strict bounds versus elastic overscroll, and the listener API

The most interesting parameter in the library is boundsBehavior, because it decides whether the sheet is allowed to be dragged past its boundary at all.

dart
final controller = RubberAnimationController(
  vsync: this,
  duration: const Duration(milliseconds: 250),
  boundsBehavior: RubberAnimationBoundsBehavior.strict,
);

The readme frames the choice as using strict bounds when the elastic overscroll effect is not appropriate. So the default behaviour is the elastic one, where dragging past the end of the sheet's travel produces a resistance and a rebound, and strict bounds replace that with a hard stop.

This is the sort of option that exists because the author found a case where the default was wrong. A sheet whose content is a scrollable list is the obvious one, because an elastic overscroll on a scroll view conflicts with the platform's own scroll physics, and Material's own bottom sheet behaviour differs depending on whether the content is scrollable. A sheet that must not be dismissible by accident is another, where a rebound after a wild fling is not what you want.

The controller is a distinct class, RubberAnimationController, rather than a plain Flutter AnimationController. It takes a vsync and a duration, like the Flutter class, plus the bounds behaviour. The duration of 250 milliseconds in the example is the settle time, so even with a physical drag the sheet animates to its final position over a fixed interval once released.

For observing state, the readme says to use controller.addListener(...) to observe the position continuously, and to note that status listeners report direction and boundary transitions with the values forward, reverse, completed and dismissed.

That is the useful part of the API for anyone wiring the sheet to a navigation flow. The continuous listener gives you the position, so you can drive a progress indicator or a blur from it. The status listener gives you the discrete events, and the four named values tell you which transition you are in: forward means opening, reverse means closing, completed means it reached the open end, and dismissed means it went past the boundary into dismissal. Distinguishing completed from dismissed matters, because they look similar in a position listener but mean different things to the caller.

And there is one detail that saves a class of bug. Pixel bounds are automatically recalculated after size and orientation changes. Without that, a sheet configured against one screen size would have wrong bounds after a rotation, and the drag would feel wrong until the widget was rebuilt with new constraints.

What the repository actually contains

The top-level entries are those of a standard Flutter package, and there is nothing surprising in them.

There is lib/ for the package source, test/ for the unit tests, example/ with a full application, assets/ holding the logo and the two demonstration gifs, a pubspec.yaml and pubspec.lock, an analysis_options.yaml, a CHANGELOG.md and a LICENSE. The .metadata file is the Flutter tool's own marker, and .github/ holds the issue templates and workflows.

The example application is where the two animations in the readme come from. There are two gifs side by side, one showing the basic interaction and one showing the sheet with scrolling content, which is the case where the bounds behaviour and the scroll physics have to cooperate. Inside the example directory there are the platform folders android/ and ios/, an integration_test/ directory, a lib/ directory with the example code, and its own pubspec.yaml, .metadata, analysis_options.yaml and README.md.

The presence of an integration_test directory is worth a note, because it says the package's own example is tested as an application rather than only at the widget level. For a package whose entire value is a gesture interaction, that is the test that actually matters, and it is also the one that is hardest to write for a library whose behaviour depends on real pointer velocity.

The readme carries an Awesome Flutter badge linking to a community list, which is the standard way a package signals that it is part of the wider Flutter ecosystem.

What is absent from the repository is as informative as what is present. There is no documentation directory with a generated API site, and no examples beyond the example application itself. The documentation is the readme plus the wiki, and the wiki page is the Quick Start that the readme points to. For a package at the early preview stage, that is a reasonable amount of documentation, and it is also a signal that the API will move.

Where this fits against the built-in bottom sheet

The alternative that matters is not another third-party package, it is Flutter's own showModalBottomSheet and the DraggableScrollableSheet widget that ships with the framework.

The built-in modal bottom sheet gives you the standard behaviour with no dependency: a drag handle, a snap at a threshold, an optional scrollable body, and a scrim. It is the right answer for most applications, because it is the thing the platform designer chose, it is documented, and it is covered by the framework's own tests and accessibility behaviour.

The difference rubber offers is the motion model and the control over the boundaries. The built-in sheet animates with a fixed duration curve; a rubber sheet responds to how the gesture was performed, which means a quick flick and a slow drag feel different. It also exposes the tuning points directly, snapThreshold, minFlingVelocity and completeFlingVelocity, and the strict bounds mode, so you can decide what happens at the end of the drag rather than inheriting the platform's choice.

The cost is the usual one. You are taking on a dependency that reimplements a platform behaviour, and in exchange you get a package that declares itself an early preview where some behaviors can change or be removed. The framework's bottom sheet will change with Flutter; this one changes when its author decides to.

There is a third option worth naming, which is to use the framework's DraggableScrollableSheet with a custom controller and drive it yourself. That gives you full control over the physics without a dependency, at the cost of writing the fling and snap logic that rubber already has. That is a reasonable trade for an application with unusual requirements and a poor one for a team that wants the behaviour and not the code.

The practical fit, then, is an application that has decided the platform's standard bottom sheet does not feel right, that has a gesture-heavy design where the motion is part of the brand, and that is on a recent enough Flutter to satisfy the 3.44 requirement. If any of those three is not true, the built-in widget is the cheaper answer.

Editorial conclusion

rubber fits a Flutter team that wants a bottom sheet whose motion is driven by physics and whose boundaries the user can configure, and it does not fit a project pinned to an older Flutter or Dart, since it requires Flutter 3.44 or later and Dart 3.12 or later. Before adopting it, read the Quick Start page in the project wiki, and decide between RubberAnimationBoundsBehavior.strict and the default elastic behaviour, because that choice changes whether an overscroll past the boundary is allowed at all.

Frequently asked questions

What does the rubber Flutter package provide?

It provides an elastic bottom sheet where a lower layer sits behind the sheet content and the whole thing is driven by an animation controller, with a draggable option, a snap threshold, minimum and complete fling velocities, a scrim colour and a scrim tap callback.

How do I make the rubber bottom sheet stop bouncing past its bounds?

Pass boundsBehavior: RubberAnimationBoundsBehavior.strict to RubberAnimationController, which the readme describes as the option to use when the elastic overscroll effect is not appropriate. The default behaviour is the elastic one.

What Flutter and Dart versions does rubber require?

The readme states that rubber requires Flutter 3.44 or later and Dart 3.12 or later. The package is licensed BSD-2-Clause and is still an early preview where some behaviors can change or be removed.

How do I observe the state of a rubber bottom sheet?

Use controller.addListener(...) to observe the position continuously. Status listeners report direction and boundary transitions with the values forward, reverse, completed and dismissed, and pixel bounds are automatically recalculated after size and orientation changes.

Official sources

  1. Issues
  2. License: BSD-2-Clause
  3. mcrovero/rubber on GitHub
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/mcrovero-rubber.svg)](https://hysenlabs.com/projects/mcrovero-rubber)

Was this useful?