Skip to content

CupertinoSheetRoute with scrolling and dragging#177337

Merged
auto-submit[bot] merged 31 commits intoflutter:masterfrom
MitchellGoodwin:dragging-sheet
Jan 23, 2026
Merged

CupertinoSheetRoute with scrolling and dragging#177337
auto-submit[bot] merged 31 commits intoflutter:masterfrom
MitchellGoodwin:dragging-sheet

Conversation

@MitchellGoodwin
Copy link
Contributor

@MitchellGoodwin MitchellGoodwin commented Oct 21, 2025

Fixes #161687

Enables scrollable content within the sheet to work together with the dragging animation.

Flutter:

Screen.Recording.2025-11-25.at.2.14.02.PM.mov

Native:

Screen.Recording.2025-10-16.at.1.42.10.PM.mov

Fling when scrolling to the top vs fling when the scrollable content is already at the top

Screen.Recording.2025-11-25.at.2.16.13.PM.mov

When scrolling is enabled, then the sheet will no longer have a drag gesture recognizer over the sheet content, and will instead rely on the scrollable content to trigger the drag. A non-scrolling area can be wrapped with CupertinoSheetDragArea to put a drag gesture recognizer only on that area, convenient for navbars. See cupertino_sheet.3.dart for a full example.

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

@github-actions github-actions bot added framework flutter/packages/flutter repository. See also f: labels. f: cupertino flutter/packages/flutter/cupertino repository labels Oct 21, 2025
@lukepighetti
Copy link
Contributor

missing one minor bit of fidelity. if you are scrolled down and fling to the top it should spring at the top instead of slam to a halt

@MitchellGoodwin
Copy link
Contributor Author

The physics when scrolling to the top is definitely not there yet. It either stops suddenly or bounces when it shouldn't when the drag to dismiss happens. I opened this draft PR to get some feedback on the overall approach.

return Navigator.of(context, rootNavigator: true).push<T>(route);
} else {
widgetBuilder = (BuildContext context) {
Widget nestedNavigationContent(Widget child) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is changed to define an inline function, is the else necessary?

context,
rootNavigator: true,
).push<T>(CupertinoSheetRoute<T>(builder: widgetBuilder, enableDrag: enableDrag));
final PageRoute<T> route = effectiveBuilder != null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you simplify the logic to de-dupe and call on your new wrapper method based on useNestedNavigation?

}
}

class _NeverUserScrollableScrollPhysics extends ScrollPhysics {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed over using NeverScrollableScrollPhysics?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want to set allowImplicitScrolling to false.

@github-actions github-actions bot added f: scrolling Viewports, list views, slivers, etc. d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos and removed f: scrolling Viewports, list views, slivers, etc. labels Nov 21, 2025
child: _CupertinoSheetScope(child: builder(context)),
child: Builder(
builder: (BuildContext context) {
return _CupertinoSheetScope(sheetContext: context, child: _effectiveBuilder(context));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to pass around context? That's an anti-pattern.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultimately it's so that _CupertinoDragGestureDetector is able to figure out the height of the sheet for knowing how much to adjust the page transition in response to a user drag.

It gets there through CupertinoSheetDragArea looking up the _CupertinoSheetScope above it and passing along the context to _CupertinoDragGestureDetector.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's really not something we should do. Passing around build context is not safe. DraggableScrollableSheet keeps track of the sheet's extent. Are we able to do that here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I can change it so that _CupertinoSheetScope tracks the height/extent rather than the context and that'll work pretty smoothly.

@chunhtai chunhtai self-requested a review November 25, 2025 23:07
Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Accessibility, we would need to focus on two things

  1. dismissable

in talkback this is fine as user can do gesture down and left to issue a android back.

For voiceOver though, it is two fingers Z gesture and it calls Semantics(onDismiss). Looking at the semantics tree, I didnt see the semantics action is set.

  1. the draghandle

In talkback, tap should be able to dismiss the sheet similar to

onSemanticsTap: widget.onClosing,

For voicerOver, we may need to check the iOS behavior, do you know whether there is builtin app that uses the native draggable sheet?

bool get enableDrag;

/// Determines whether the content can be scrolled.
bool get enableScroll;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be private?

@chunhtai
Copy link
Contributor

also, while not yet wired up in the engine, can you also apply SemanticsRole.dragHandle to the handle so that we won't forget about this in the future when it is wired up

@MitchellGoodwin
Copy link
Contributor Author

For voicerOver, we may need to check the iOS behavior, do you know whether there is builtin app that uses the native draggable sheet?

You can find the native sheet in the Contacts app, when you add a new contact. You can also find another one in the Settings app through General -> Language & Region -> Add Language

@MitchellGoodwin
Copy link
Contributor Author

also, while not yet wired up in the engine, can you also apply SemanticsRole.dragHandle to the handle so that we won't forget about this in the future when it is wired up

CupertinoSheet currently does not add a drag handle. For the native sheet widget, the drag handle is normally added when the sheet is resizable, which CupertinoSheet does not do by default, nor is this PR adding that.

However, CupertinoSheetDragArea could be wrapped around a drag handle type widget manually added by a dev.

So should we either

  1. Not do anything with the drag handle role for this PR.
  2. Add either a isDragHandle or a more generic SemanticsRole property to CupertinoSheetDragArea.
  3. Add an other SemanticsRole somewhere else that I'm missing.

@MitchellGoodwin
Copy link
Contributor Author

@chunhtai

I tested on a device both the native behavior and this code.

On native the two finger Z-scrub gesture does not dismiss the sheet, it seems. Double tap, then drag does dismiss it however.

For CupertinoSheet, currently the Z-scrub gesture does dismiss it, the same with double tap and drag.

Should we block the two finger Z gesture?

@Piinks
Copy link
Contributor

Piinks commented Dec 2, 2025

CupertinoSheet currently does not add a drag handle. For the native sheet widget, the drag handle is normally added when the sheet is resizable

@MitchellGoodwin have you checked the native behavior with accessibility controls enabled? Does SwiftUI expose the ability to add drag handles?

@MitchellGoodwin
Copy link
Contributor Author

MitchellGoodwin commented Dec 2, 2025

@MitchellGoodwin have you checked the native behavior with accessibility controls enabled? Does SwiftUI expose the ability to add drag handles?

@Piinks I have checked the native behavior with accessibility controls, using a physical device.

For the drag handle on native, they do expose a way to add it through prefersGrabberVisibile. They refer to the drag handle as a "grabber".

From the HIGs documentation on sheets, they say:

"Include a grabber in a resizable sheet. A grabber shows people that they can drag the sheet to resize it; they can also tap it to cycle through the detents. In addition to providing a visual indicator of resizability, a grabber also works with VoiceOver so people can resize the sheet without seeing the screen."

I filled an issue for the drag handle #179358. I think that can be done in a separate PR from this one.

@MitchellGoodwin MitchellGoodwin marked this pull request as ready for review December 5, 2025 23:01
Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just trying to think through an alternative approach... Rather than a new constructor for CupertinoSheetRoute that passes a ScrollController, could you use PrimaryScrollController.of inside of CupertinoSheetRoute and ask users to do the same and pass that to their relevant scroll view? I'm no expert in this area but it seems kind of aligned with the intention of PrimaryScrollController.

If not PrimaryScrollController, then would a new similar InheritedWidget make sense? ...Or would it just make this more complicated and less discoverable?

Otherwise, do you know how this kind of thing works in SwiftUI? Is it always the entire CupertinoSheet that is scrollable?

Assuming none of these drive-by ideas is viable, and that this covers the main use cases from SwiftUI, then this approach looks good to me!

Comment on lines +99 to +106
/// This is a convenience method for displaying [CupertinoSheetRoute] for common,
/// straightforward use cases. The Widget returned from `pageBuilder` will be
/// used to display the content on the [CupertinoSheetRoute].
/// straightforward use cases. There are two different build methods that are provided
/// by the method for building the content of the [CupertinoSheetRoute]. Use `builder`
/// if there is no content within the sheet that needs to scroll. This is good
/// for sheets with a simple display, however scrolling gestures will conflict
/// with the drag to dismiss gesture. `scrollableBuilder` will enable scrollable
/// content within the sheet. See [CupertinoSheetRoute.scrollable] for more
/// information.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Should you mention that this is for vertical scrolling?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good callout

@MitchellGoodwin
Copy link
Contributor Author

Just trying to think through an alternative approach... Rather than a new constructor for CupertinoSheetRoute that passes a ScrollController, could you use PrimaryScrollController.of inside of CupertinoSheetRoute and ask users to do the same and pass that to their relevant scroll view? I'm no expert in this area but it seems kind of aligned with the intention of PrimaryScrollController.

@justinmc The only issue with that is that we would need to know that the sheet content is intended to be scrollable. So, we could provide the controller with PrimaryScrollController but I think we'd need to have at least a scrollable boolean property in the constructor. When set to true it doesn't apply the drag gesture detector over the whole sheet.

If not PrimaryScrollController, then would a new similar InheritedWidget make sense? ...Or would it just make this more complicated and less discoverable?

I don't think a new widget would be needed. As far the user of this API would be concerned, the scroll controller isn't unique in how they implement it. This would make this API further different from DraggableScrollableSheet, so there's a difference in consistency there.

Otherwise, do you know how this kind of thing works in SwiftUI? Is it always the entire CupertinoSheet that is scrollable?

You can just put a scroll view within the sheet and it works as expected. Scrollable areas in Swift know when they are the child of a resizable area and handle things accordingly. Similarly they are able to have priority over the drag gesture that might be on the rest of the sheet.

The most common use case is that the majority of the sheet, except for the nav bar is scrollable. So the situation in the example.

@github-actions github-actions bot added the f: scrolling Viewports, list views, slivers, etc. label Jan 22, 2026
@Piinks Piinks added the autosubmit Merge PR when tree becomes green via auto submit App label Jan 23, 2026
@auto-submit auto-submit bot added this pull request to the merge queue Jan 23, 2026
Merged via the queue into flutter:master with commit 7411f3f Jan 23, 2026
71 checks passed
@flutter-dashboard flutter-dashboard bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Jan 23, 2026
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jan 24, 2026
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jan 24, 2026
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jan 25, 2026
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jan 25, 2026
auto-submit bot pushed a commit to flutter/packages that referenced this pull request Jan 25, 2026
flutter/flutter@bfc9041...def9ca9

2026-01-25 [email protected] Roll Skia from f1433eb44a50 to 2830fbe8bafe (1 revision) (flutter/flutter#181464)
2026-01-25 [email protected] Roll Fuchsia Linux SDK from 6xoKGIry6Y8T8x5Sa... to T4qTEa3T5CCSCIoJY... (flutter/flutter#181458)
2026-01-24 [email protected] Roll Skia from b6d396a151bc to f1433eb44a50 (1 revision) (flutter/flutter#181449)
2026-01-24 [email protected] Roll Dart SDK from 29918a54dd5c to 60553fc4c04f (1 revision) (flutter/flutter#181437)
2026-01-24 [email protected] Roll Fuchsia Linux SDK from n7NohL9DPpEuPjNt9... to 6xoKGIry6Y8T8x5Sa... (flutter/flutter#181438)
2026-01-24 [email protected] [Impeller] Fix perspective clips with a large perspective bias (flutter/flutter#181434)
2026-01-24 [email protected] Roll Dart SDK from e82d7ad1855e to 29918a54dd5c (4 revisions) (flutter/flutter#181435)
2026-01-24 [email protected] Roll Skia from 32b52343e757 to b6d396a151bc (4 revisions) (flutter/flutter#181431)
2026-01-24 [email protected] [Impeller] Fix interpolation error in Rect::TransformAndClipBounds (flutter/flutter#181420)
2026-01-23 [email protected] Roll Skia from 6d438894c2a8 to 32b52343e757 (2 revisions) (flutter/flutter#181419)
2026-01-23 [email protected] [Material] modernize Typography._withPlatform with Dart 3 switch expression (flutter/flutter#181398)
2026-01-23 [email protected] CupertinoSheetRoute with scrolling and dragging (flutter/flutter#177337)
2026-01-23 [email protected] Adds contents of keys file when a skia gold error occurs. (flutter/flutter#181401)
2026-01-23 [email protected] Roll Skia from e4bd0a355e68 to 6d438894c2a8 (3 revisions) (flutter/flutter#181405)
2026-01-23 [email protected] bump KGP and AGP max known versions (flutter/flutter#181325)
2026-01-23 [email protected] Roll Skia from db10db8bd55f to e4bd0a355e68 (3 revisions) (flutter/flutter#181391)
2026-01-23 [email protected] Roll Packages from 9010299 to 5af5f50 (4 revisions) (flutter/flutter#181388)
2026-01-23 [email protected] Look for project root for FeatureFlags manifest (flutter/flutter#180689)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC [email protected],[email protected] on the revert to ensure that a human
is aware of the problem.

To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
flutter-zl pushed a commit to flutter-zl/flutter that referenced this pull request Feb 10, 2026
Fixes flutter#161687

Enables scrollable content within the sheet to work together with the
dragging animation.

Flutter:


https://github.com/user-attachments/assets/3a9994e3-078a-410e-8288-95592713ab39



Native:


https://github.com/user-attachments/assets/c0bd46fb-26f5-41a6-bf42-6c78c23ddf34

Fling when scrolling to the top vs fling when the scrollable content is
already at the top


https://github.com/user-attachments/assets/e934facc-36d2-4e81-9ccd-a3e9dd7b6201



When scrolling is enabled, then the sheet will no longer have a drag
gesture recognizer over the sheet content, and will instead rely on the
scrollable content to trigger the drag. A non-scrolling area can be
wrapped with `CupertinoSheetDragArea` to put a drag gesture recognizer
only on that area, convenient for navbars. See
[cupertino_sheet.3.dart](https://github.com/flutter/flutter/pull/177337/files#diff-3df71aa5e6ddb6184007a85e847a9a4883515697b7014764e1978d240d72e0e1)
for a full example.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
@eshquvvatov
Copy link

Steps to reproduce
Create a CupertinoSheetRoute with a ListView.separated inside
Open the sheet
Scroll the ListView to the very top
Try to drag down from the list area to dismiss the sheet
The sheet does not dismiss
Expected: When the ListView is at the top and user drags down, the sheet should dismiss (like native iOS behavior)
Actual: The sheet stays open and does not respond to the downward drag gesture

Expected results
When the ListView is scrolled to the top and the user continues to drag downward, the CupertinoSheetRoute should begin its dismiss animation and close, similar to native iOS sheet behavior.

Actual results
The sheet does not dismiss when dragging down on the ListView area. The drag gesture is consumed by the ListView and never passed to the sheet's drag-to-dismiss handler.

Note: PR #177337 (merged Jan 23, 2026) was supposed to fix this issue (#161687), but the problem persists on stable 3.41.2.

Uploading FILE 2026-02-25 16:14:58.mp4…

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
const MyApp({super.key});

@OverRide
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false, home: HomePage());
}
}

class HomePage extends StatelessWidget {
const HomePage({super.key});

@OverRide
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(middle: Text('Home')),
child: SafeArea(
child: Center(
child: CupertinoButton.filled(
child: const Text('Open Cupertino Sheet'),
onPressed: () {
showCupertinoSheet(
context: context,
builder: (BuildContext sheetContext) {
return const _MySheetPage();
},
enableDrag: true,
showDragHandle: false,
useNestedNavigation: true,
);
},
),
),
),
);
}
}

class _MySheetPage extends StatefulWidget {
const _MySheetPage();

@OverRide
State<_MySheetPage> createState() => _MySheetPageState();
}

class _MySheetPageState extends State<_MySheetPage> {
@OverRide
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: const Text('Sheet'),
trailing: CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () => Navigator.of(context).maybePop(),
child: const Text('Close'),
),
),
child: SafeArea(
top: false,
child: Column(
children: [
const SizedBox(height: 8),
const _GrabHandle(),
const SizedBox(height: 8),

        Expanded(
          child: ListView.separated(
            physics: const AlwaysScrollableScrollPhysics(
              parent: BouncingScrollPhysics(),
            ),
            padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
            itemCount: 60,
            separatorBuilder: (_, _) => const SizedBox(height: 10),
            itemBuilder: (context, index) {
              return _CupertinoCard(
                title: 'Item #$index',
                subtitle: 'info',
              );
            },
          ),
        ),
      ],
    ),
  ),
);

}
}

class _GrabHandle extends StatelessWidget {
const _GrabHandle();

@OverRide
Widget build(BuildContext context) {
return Center(
child: Container(
width: 44,
height: 5,
decoration: BoxDecoration(
color: CupertinoColors.systemGrey4,
borderRadius: BorderRadius.circular(99),
),
),
);
}
}

class _CupertinoCard extends StatelessWidget {
final String title;
final String subtitle;

const _CupertinoCard({required this.title, required this.subtitle});

@OverRide
Widget build(BuildContext context) {
return Material(
child: DecoratedBox(
decoration: BoxDecoration(
color: CupertinoColors.secondarySystemBackground,
borderRadius: BorderRadius.circular(14),
),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
Text(
subtitle,
style: const TextStyle(
fontSize: 14,
color: CupertinoColors.systemGrey,
),
),
const SizedBox(height: 10),
CupertinoButton(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
onPressed: () {},
child: const Text('Action'),
),
],
),
),
),
);
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c: tech-debt Technical debt, code quality, testing, etc. d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos f: cupertino flutter/packages/flutter/cupertino repository f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cupertino Sheet should have drag to dismiss and nested scrolling work together

6 participants