bubbles_sheet 0.2.0 copy "bubbles_sheet: ^0.2.0" to clipboard
bubbles_sheet: ^0.2.0 copied to clipboard

Pill-shouldered, iOS-26-style modal bottom sheets for Flutter — detents, edge-flush corners, sticky CTAs and a themeable chrome, built on smooth_sheets.

bubbles_sheet #

Pill-shouldered, iOS-26-style modal bottom sheets for Flutter, built on smooth_sheets.

Content-sized Three detents Header action + live CTA Dark chrome
A content-sized sheet titled Sort by, floating above a dimmed screen with a pill Done button pinned to the bottom A half-height sheet titled Tokens showing a scrolling list A sheet titled Pick a number with a Clear action in the header and an Apply button that enables once a number is chosen The same sheet chrome rendered in the dark palette

Every sheet gets the same chrome — a grab handle, a leading close button, a centered title, an optional trailing text action, and a primary CTA that stays pinned to the bottom while the body scrolls. The sheet floats on an inset above a dimmed barrier, and as you drag it to its largest detent the inset lerps away and the bottom corners grow into the device's own screen radius, so the curve continues into the bezel instead of cutting across it.

Usage #

final picked = await showBubblesSheet<String>(
  context,
  title: 'Sort by',
  builder: (context) => const SortOptions(),
);

Detents default to [fit, large] — content-sized to start, draggable to nearly full screen. Pass your own to change that:

showBubblesSheet<void>(
  context,
  title: 'Pick a token',
  detents: const [BubblesSheetDetent.medium, BubblesSheetDetent.large],
  builder: (context) => const TokenList(),
);

A sticky CTA that depends on the body's state #

The CTA renders in the chrome, outside the body, so state both of them read belongs above the sheet. Hand it a Listenable and only the CTA rebuilds:

final pending = ValueNotifier<int>(current);
await showBubblesSheet<int>(
  context,
  title: 'Max slippage',
  primaryCtaListenable: pending,
  primaryCtaBuilder: (context) => BubblesSheetCta(
    title: 'Set ${pending.value}bps',
    enabled: pending.value != current,
    onTap: () => Navigator.of(context).pop(pending.value),
  ),
  builder: (_) => SlippagePicker(onChanged: (v) => pending.value = v),
).whenComplete(pending.dispose);

A trailing header action #

The slot opposite the close button, for a secondary action that doesn't commit the sheet:

trailingAction: BubblesSheetAction(
  label: 'Clear',
  onTap: () => draft.value = Filters.none,
),

Theming #

Every colour, measurement, icon, haptic and the CTA widget itself come from BubblesSheetThemeData, registered as a ThemeExtension. It has working defaults for everything, so the package renders correctly with no wiring at all — register it only to override:

MaterialApp(
  theme: ThemeData.light().copyWith(
    extensions: [
      BubblesSheetThemeData(
        light: BubblesSheetPalette.cream.copyWith(action: myBrandCoral),
        titleStyle: myTitleStyle,
        closeIcon: PhosphorIconsBold.x,
        ctaBuilder: (context, cta) => MyPillButton(label: cta.title, onTap: cta.onTap),
        haptics: BubblesSheetHaptics(
          onPresent: HapticFeedback.lightImpact,
          onDismiss: HapticFeedback.selectionClick,
        ),
      ),
    ],
  ),
)

Two things are worth calling out:

Haptics are hooks, not calls. The package never invokes HapticFeedback itself, so it can't fire a buzz an app's own "haptics off" setting has disabled. Wire BubblesSheetHaptics to whatever façade you already have.

The flush corners are automatic on Android 12+, and need a value elsewhere. The radius is read from the display via FlutterView.displayCornerRadii, which dart:ui populates only on Android API 31+. On iOS — and on older Android, and everywhere else — it is null, so hand the radius in yourself, e.g. from screen_corner_radius:

metrics: BubblesSheetMetrics(deviceCornerRadius: myResolvedRadius),

An explicit value always wins over the display lookup, so it is also how you force a particular curve or pin one in a test. Passing 0 squares the bottom edge off; leaving it unset on a platform that reports nothing does the same.

Paged sheets #

Multi-page flows use smooth_sheets' own PagedSheet with this package's chrome — BubblesPagedSheetHeader (which grows a back button on inner pages), BubblesPagedSheetViewport, and BubblesPagedSheetSurface. smooth_sheets is re-exported from package:bubbles_sheet/bubbles_sheet.dart, so those flows need one import rather than two.

Notes #

dark: true selects BubblesSheetThemeData.dark explicitly — it does not read the ambient Theme brightness. A sheet is often deliberately inverted against the screen behind it, so the choice stays at the call site.

1
likes
0
points
173
downloads

Publisher

verified publisherhashstudios.dev

Weekly Downloads

Pill-shouldered, iOS-26-style modal bottom sheets for Flutter — detents, edge-flush corners, sticky CTAs and a themeable chrome, built on smooth_sheets.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, smooth_sheets

More

Packages that depend on bubbles_sheet