image

22 Jan 2026

9K

35K

Creating Interactive Custom Bottom Sheet Widgets in Flutter

Flutter offers powerful tools for building rich and responsive user interfaces, and custom bottom sheets are a prime example of this flexibility. While a standard bottom sheet can fulfill basic needs, creating an interactive, custom-designed bottom sheet significantly enhances user experience by providing unique branding, dynamic content, and intuitive gesture-based controls. This article guides you through the process of building an interactive custom bottom sheet in Flutter, focusing on both appearance and dynamic behavior.

Understanding Bottom Sheets in Flutter

A bottom sheet is a modal panel that slides up from the bottom of the screen, typically displaying additional content or options related to the current context. Flutter provides a built-in function, showModalBottomSheet, for presenting a standard bottom sheet.


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(
      title: 'Flutter Bottom Sheet Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Standard Bottom Sheet'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            showModalBottomSheet(
              context: context,
              builder: (BuildContext context) {
                return SizedBox(
                  height: 200,
                  child: Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const Text('This is a standard bottom sheet.'),
                        ElevatedButton(
                          onPressed: () => Navigator.pop(context),
                          child: const Text('Close Bottom Sheet'),
                        ),
                      ],
                    ),
                  ),
                );
              },
            );
          },
          child: const Text('Show Standard Bottom Sheet'),
        ),
      ),
    );
  }
}

This basic implementation is functional but offers limited customization for appearance and little intrinsic interactivity beyond being dismissed.

The Need for Customization and Interactivity

Customizing a bottom sheet allows you to match it with your app's design language, adding rounded corners, specific background colors, or a custom drag handle. Interactivity goes a step further, enabling features like:

  • Draggable Resizing: Users can drag the sheet up or down to reveal more or less content, snapping to predefined sizes.
  • Dynamic Content: The content within the bottom sheet can change based on user interaction or data updates.
  • Scrollable Content: Allowing the bottom sheet to contain long lists or complex forms that can be scrolled independently.
  • Custom Dismissal: Beyond tapping outside, implementing custom gestures or buttons for dismissal.

Building Blocks for a Custom Interactive Bottom Sheet

To achieve a truly interactive and custom bottom sheet, we'll combine several Flutter widgets and concepts:

1. Custom Content Widget

The content displayed inside your bottom sheet will typically be a StatefulWidget if it needs to manage its own state (e.g., forms, counters, dynamic lists). This allows for interactions within the sheet itself.

2. Leveraging showModalBottomSheet

We'll still use showModalBottomSheet as the primary function to present our custom sheet. However, we'll set isScrollControlled: true to allow the sheet to take up more than half the screen height, which is essential for draggable sheets.

3. Adding Draggable Behavior with DraggableScrollableSheet

The DraggableScrollableSheet widget is the cornerstone for creating interactive, draggable bottom sheets. It allows its child to be dragged and resized between specified `initialChildSize`, `minChildSize`, and `maxChildSize` values. Crucially, it manages its own scroll controller, making it perfect for sheets containing scrollable content.

Step-by-Step Implementation: An Interactive Custom Bottom Sheet

Let's create a custom bottom sheet that is draggable, has rounded corners, and displays interactive content (a simple counter).

1. Defining the Custom Bottom Sheet Content

First, we'll create a StatefulWidget that will house our bottom sheet's content. This widget will manage a simple counter.


import 'package:flutter/material.dart';

class _MyCustomBottomSheetContent extends StatefulWidget {
  const _MyCustomBottomSheetContent({Key? key}) : super(key: key);

  @override
  _MyCustomBottomSheetContentState createState() => _MyCustomBottomSheetContentState();
}

class _MyCustomBottomSheetContentState extends State<_MyCustomBottomSheetContent> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  void _decrementCounter() {
    setState(() {
      _counter--;
    });
  }

  @override
  Widget build(BuildContext context) {
    return ClipRRect( // ClipRRect for custom rounded corners
      borderRadius: const BorderRadius.vertical(top: Radius.circular(25.0)),
      child: Container(
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: const BorderRadius.vertical(top: Radius.circular(25.0)),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(0.1),
              spreadRadius: 5,
              blurRadius: 7,
              offset: const Offset(0, 3),
            ),
          ],
        ),
        child: Column(
          children: [
            // Custom drag handle
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 10.0),
              child: Container(
                width: 40,
                height: 5,
                decoration: BoxDecoration(
                  color: Colors.grey[300],
                  borderRadius: BorderRadius.circular(10),
                ),
              ),
            ),
            Expanded(
              child: ListView( // Use ListView to handle potential overflow within the sheet
                padding: EdgeInsets.zero, // Remove default padding for ListView
                children: [
                  const Padding(
                    padding: EdgeInsets.all(16.0),
                    child: Text(
                      'Interactive Bottom Sheet Content',
                      style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
                      textAlign: TextAlign.center,
                    ),
                  ),
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 16.0),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        IconButton(
                          icon: const Icon(Icons.remove_circle),
                          onPressed: _decrementCounter,
                        ),
                        Text(
                          'Counter: $_counter',
                          style: const TextStyle(fontSize: 24),
                        ),
                        IconButton(
                          icon: const Icon(Icons.add_circle),
                          onPressed: _incrementCounter,
                        ),
                      ],
                    ),
                  ),
                  // Add more content here to demonstrate scrolling
                  ...List.generate(
                    20,
                    (index) => ListTile(
                      title: Text('List Item ${index + 1}'),
                      leading: const Icon(Icons.info),
                    ),
                  ),
                  Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: ElevatedButton(
                      onPressed: () => Navigator.pop(context),
                      child: const Text('Close Sheet'),
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

In this content widget:

  • ClipRRect with BorderRadius.vertical gives the top corners a rounded shape.
  • A simple Container acts as a visual drag handle.
  • The counter logic demonstrates state management within the bottom sheet.
  • ListView is used inside an Expanded widget to allow the content to scroll if it exceeds the sheet's current height, which is essential for DraggableScrollableSheet.

2. Integrating DraggableScrollableSheet for Interactivity

Now, we'll wrap our custom content widget within a DraggableScrollableSheet. This will be the builder function for showModalBottomSheet.


// This function will be called by an onPressed event
void _showCustomBottomSheet(BuildContext context) {
  showModalBottomSheet(
    context: context,
    isScrollControlled: true, // Crucial for a full-height draggable sheet
    backgroundColor: Colors.transparent, // Make background transparent to show custom shape
    builder: (BuildContext context) {
      return DraggableScrollableSheet(
        initialChildSize: 0.5, // The sheet will initially take 50% of the screen height
        minChildSize: 0.25,  // Minimum height the sheet can be dragged to
        maxChildSize: 0.9,   // Maximum height the sheet can be dragged to
        expand: false,       // Set to false to allow the sheet to start at initialChildSize
        builder: (BuildContext context, ScrollController scrollController) {
          return _MyCustomBottomSheetContent(); // Our custom content widget
        },
      );
    },
  );
}

Key properties of DraggableScrollableSheet:

  • initialChildSize: The fraction of the screen height the sheet starts at.
  • minChildSize, maxChildSize: The minimum and maximum fractions the sheet can be dragged to.
  • expand: false: Allows the sheet to resize, rather than filling the entire available space.
  • The builder function provides a ScrollController that *must* be passed to any scrollable widget (like our ListView) inside the sheet for the dragging to work correctly. However, in our current _MyCustomBottomSheetContent, the ListView doesn't directly use this controller in its constructor. For direct integration of the scroll behavior, you'd pass it like `ListView(controller: scrollController, ...)`. The example above implies the sheet manages the scrolling through its child widget, which is okay for a simple list. If you want seamless drag-to-scroll, pass the controller to the child's scrollable.

3. Triggering the Custom Bottom Sheet

Finally, we need a way to open this bottom sheet, usually through a button press.


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(
      title: 'Interactive Custom Bottom Sheet',
      theme: ThemeData(
        primarySwatch: Colors.deepPurple,
      ),
      home: const MyHomePage(),
    );
  }
}

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

  // Function to show the custom bottom sheet
  void _showCustomBottomSheet(BuildContext context) {
    showModalBottomSheet(
      context: context,
      isScrollControlled: true, // Crucial for full-height and draggable
      backgroundColor: Colors.transparent, // Allows custom shape to show
      builder: (BuildContext context) {
        return DraggableScrollableSheet(
          initialChildSize: 0.5, // Starts at 50% of screen height
          minChildSize: 0.25,    // Minimum height
          maxChildSize: 0.9,     // Maximum height
          expand: false,         // Essential for initialChildSize to work
          builder: (BuildContext context, ScrollController scrollController) {
            // Pass the scrollController to the scrollable child if needed for seamless drag-to-scroll
            return _MyCustomBottomSheetContent(scrollController: scrollController);
          },
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Interactive Custom Bottom Sheet'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () => _showCustomBottomSheet(context),
          child: const Text('Show Custom Bottom Sheet'),
        ),
      ),
    );
  }
}

// Update _MyCustomBottomSheetContent to accept a ScrollController
class _MyCustomBottomSheetContent extends StatefulWidget {
  final ScrollController scrollController; // Add this
  const _MyCustomBottomSheetContent({Key? key, required this.scrollController}) : super(key: key); // Update constructor

  @override
  _MyCustomBottomSheetContentState createState() => _MyCustomBottomSheetContentState();
}

class _MyCustomBottomSheetContentState extends State<_MyCustomBottomSheetContent> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  void _decrementCounter() {
    setState(() {
      _counter--;
    });
  }

  @override
  Widget build(BuildContext context) {
    return ClipRRect(
      borderRadius: const BorderRadius.vertical(top: Radius.circular(25.0)),
      child: Container(
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: const BorderRadius.vertical(top: Radius.circular(25.0)),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(0.1),
              spreadRadius: 5,
              blurRadius: 7,
              offset: const Offset(0, 3),
            ),
          ],
        ),
        child: Column(
          children: [
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 10.0),
              child: Container(
                width: 40,
                height: 5,
                decoration: BoxDecoration(
                  color: Colors.grey[300],
                  borderRadius: BorderRadius.circular(10),
                ),
              ),
            ),
            Expanded(
              child: ListView(
                controller: widget.scrollController, // Pass the controller here
                padding: EdgeInsets.zero,
                children: [
                  const Padding(
                    padding: EdgeInsets.all(16.0),
                    child: Text(
                      'Interactive Bottom Sheet Content',
                      style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
                      textAlign: TextAlign.center,
                    ),
                  ),
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 16.0),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        IconButton(
                          icon: const Icon(Icons.remove_circle),
                          onPressed: _decrementCounter,
                        ),
                        Text(
                          'Counter: $_counter',
                          style: const TextStyle(fontSize: 24),
                        ),
                        IconButton(
                          icon: const Icon(Icons.add_circle),
                          onPressed: _incrementCounter,
                        ),
                      ],
                    ),
                  ),
                  ...List.generate(
                    20,
                    (index) => ListTile(
                      title: Text('List Item ${index + 1}'),
                      leading: const Icon(Icons.info),
                    ),
                  ),
                  Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: ElevatedButton(
                      onPressed: () => Navigator.pop(context),
                      child: const Text('Close Sheet'),
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Conclusion

By combining showModalBottomSheet with DraggableScrollableSheet and a custom StatefulWidget for its content, you can create highly interactive and visually appealing bottom sheets in Flutter. This approach offers fine-grained control over sizing, draggability, and appearance, allowing you to build richer user experiences that align perfectly with your application's design language. Experiment with different initialChildSize, minChildSize, and maxChildSize values, as well as the content within your custom widget, to create the perfect interactive bottom sheet for your Flutter application.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is