anchored_sheets 1.2.0 copy "anchored_sheets: ^1.2.0" to clipboard
anchored_sheets: ^1.2.0 copied to clipboard

A Flutter package to create anchored sheets that can be dragged and snapped to different positions on the screen.

🎯 Anchored Sheets #

Pub Version Flutter Package

A Flutter package for creating modal sheets that slide down from the top of the screen, similar to showModalBottomSheet but positioned at the top. Perfect for filter menus, notifications, dropdowns, and any content that should appear anchored to specific widgets or screen positions.

✨ Features #

  • 🎯 Anchor Positioning - Attach sheets to specific widgets using GlobalKeys
  • 🎨 Material Design - Full theming integration with Material 3 support
  • 📱 Status Bar Smart - Intelligent status bar overlap handling with background extension
  • 🖱️ Drag Support - Optional drag-to-dismiss with customizable handles
  • 🔄 Easy Dismissal - Simple context.popAnchoredSheet() method for closing sheets
  • 🚀 Provider Ready - Built-in support for state management patterns
  • Accessibility - Full screen reader and semantic support
  • 🌐 Platform Aware - Works seamlessly across iOS, Android, Web, and Desktop
  • 🛡️ Type Safe - Full type safety with generic support
  • 🔄 Smart Replacement - Automatic sheet replacement and duplicate prevention
  • 🚫 Duplicate Prevention - Prevent re-rendering when clicking same button multiple times

📦 Installation #

Add this to your package's pubspec.yaml file:

dependencies:
  anchored_sheets: ^1.2.0

Then run:

flutter pub get

🚀 Quick Start #

Basic Usage #

import 'package:anchored_sheets/anchored_sheets.dart';

// Simple sheet from top
void showBasicSheet() {
  anchoredSheet(
    context: context,
    builder: (context) => Container(
      padding: EdgeInsets.all(20),
      child: Column(
        mainAxisSize: MainAxisSize.min, // ✅ Automatically sized!
        children: [
          Icon(Icons.info, size: 48),
          SizedBox(height: 16),
          Text('Hello from top sheet!'),
          ElevatedButton(
            onPressed: () => context.popAnchoredSheet(),
            child: Text('Close'),
          ),
        ],
      ),
    ),
  );
}

Anchored to Widget #

final GlobalKey buttonKey = GlobalKey();

// In your build method
ElevatedButton(
  key: buttonKey, // 🎯 Anchor point
  onPressed: showAnchoredMenu,
  child: Text('Menu'),
)

// Show anchored sheet
void showAnchoredMenu() async {
  final result = await anchoredSheet<String>(
    context: context,
    anchorKey: buttonKey, // Sheet appears below this button
    builder: (context) => Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        ListTile(
          leading: Icon(Icons.home),
          title: Text('Home'),
          onTap: () => context.popAnchoredSheet('home'),
        ),
        ListTile(
          leading: Icon(Icons.settings),
          title: Text('Settings'),
          onTap: () => context.popAnchoredSheet('settings'),
        ),
      ],
    ),
  );
  
  if (result != null) {
    print('Selected: $result');
  }
}

📚 API Reference #

anchoredSheet<T> #

The main function for displaying anchored sheets.

Future<T?> anchoredSheet<T>({
  required BuildContext context,
  required WidgetBuilder builder,
  
  // Positioning
  GlobalKey? anchorKey,           // Anchor to specific widget
  double? topOffset,              // Manual top offset
  bool useSafeArea = false,       // Respect status bar/notch
  
  // Styling
  Color? backgroundColor,         // Sheet background color
  double? elevation,              // Material elevation
  ShapeBorder? shape,            // Custom shape
  BorderRadius? borderRadius,     // Corner radius
  Clip? clipBehavior,            // Clipping behavior
  BoxConstraints? constraints,    // Size constraints
  
  // Interaction
  bool isDismissible = true,      // Tap outside to dismiss
  bool enableDrag = false,        // Drag to dismiss
  bool? showDragHandle,          // Show drag handle
  Color? dragHandleColor,        // Handle color
  Size? dragHandleSize,          // Handle size
  bool toggleOnDuplicate = true,  // Dismiss when same anchor is used
  
  // Sheet Management (NEW in v1.2.0)
  bool replaceSheet = true,       // Auto-replace existing sheets
  bool dismissOtherModals = false, // Dismiss other modals first
  
  // Animation
  Duration animationDuration = const Duration(milliseconds: 300),
  Color overlayColor = Colors.black54,
  
  // Scroll behavior
  bool isScrollControlled = false,
  double scrollControlDisabledMaxHeightRatio = 9.0 / 16.0,
})

context.popAnchoredSheet<T> #

Context-based dismissal function (preferred).

// Dismiss with result
context.popAnchoredSheet('result_value');

// Dismiss without result
context.popAnchoredSheet();

// From BuildContext extension
void someFunction(BuildContext context) {
  context.popAnchoredSheet('closed_from_context');
}

🎨 Examples #

Styled Sheet #

anchoredSheet(
  context: context,
  backgroundColor: Colors.purple.shade50,
  elevation: 10,
  borderRadius: BorderRadius.only(
    bottomLeft: Radius.circular(24),
    bottomRight: Radius.circular(24),
  ),
  builder: (context) => Container(
    padding: EdgeInsets.all(20),
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Icon(Icons.palette, size: 48, color: Colors.purple),
        SizedBox(height: 16),
        Text('Custom Styled Sheet'),
      ],
    ),
  ),
);

Draggable Sheet #

anchoredSheet(
  context: context,
  enableDrag: true,           // 🖱️ Enable drag to dismiss
  showDragHandle: true,       // Show drag handle
  dragHandleColor: Colors.grey,
  builder: (context) => Container(
    padding: EdgeInsets.all(20),
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Text('Drag me up to dismiss!'),
        SizedBox(height: 20),
        // Your content here
      ],
    ),
  ),
);

Form Sheet with Return Value #

void showFormSheet() async {
  final Map<String, dynamic>? result = await anchoredSheet<Map<String, dynamic>>(
    context: context,
    isScrollControlled: true,
    builder: (context) => FormSheetWidget(),
  );
  
  if (result != null) {
    print('Form data: ${result['name']}, ${result['email']}');
  }
}

class FormSheetWidget extends StatefulWidget {
  @override
  _FormSheetWidgetState createState() => _FormSheetWidgetState();
}

class _FormSheetWidgetState extends State<FormSheetWidget> {
  final _nameController = TextEditingController();
  final _emailController = TextEditingController();
  
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.all(20),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          TextField(
            controller: _nameController,
            decoration: InputDecoration(labelText: 'Name'),
          ),
          TextField(
            controller: _emailController,
            decoration: InputDecoration(labelText: 'Email'),
          ),
          SizedBox(height: 20),
          ElevatedButton(
            onPressed: () {
              context.popAnchoredSheet({
                'name': _nameController.text,
                'email': _emailController.text,
              });
            },
            child: Text('Submit'),
          ),
        ],
      ),
    );
  }
}

Smart Sheet Management (NEW!) #

class SmartSheetDemo extends StatelessWidget {
  final GlobalKey menuKey = GlobalKey();
  final GlobalKey filterKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        actions: [
          // Menu button with smart toggle
          IconButton(
            key: menuKey,
            icon: Icon(Icons.menu),
            onPressed: () => showMenuSheet(context),
          ),
          // Filter button with smart replacement
          IconButton(
            key: filterKey,
            icon: Icon(Icons.filter_list),
            onPressed: () => showFilterSheet(context),
          ),
        ],
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () => showRegularSheet(context),
          child: Text('Show Regular Sheet'),
        ),
      ),
    );
  }

  void showMenuSheet(BuildContext context) {
    // Smart behavior:
    // 1st click: Shows menu
    // 2nd click: Dismisses menu (same anchor key)
    // After filter is open, clicking this replaces filter with menu
    anchoredSheet(
      context: context,
      anchorKey: menuKey, // Smart anchor-based detection
      builder: (context) => MenuContent(),
    );
  }

  void showFilterSheet(BuildContext context) {
    // Smart behavior:
    // Replaces any existing sheet (different anchor key)
    // Automatically dismisses other modals if needed
    anchoredSheet(
      context: context,
      anchorKey: filterKey,
      dismissOtherModals: true, // Clean slate approach
      builder: (context) => FilterContent(),
    );
  }

  void showRegularSheet(BuildContext context) {
    // Smart behavior:
    // Always replaces existing sheets (no anchor key)
    anchoredSheet(
      context: context,
      builder: (context) => RegularContent(),
    );
  }
}

Filter Menu #

final GlobalKey filterButtonKey = GlobalKey();
String selectedFilter = 'All';

Widget buildFilterButton() {
  return ElevatedButton.icon(
    key: filterButtonKey,
    onPressed: showFilterMenu,
    icon: Icon(Icons.filter_list),
    label: Text('Filter: $selectedFilter'),
  );
}

void showFilterMenu() async {
  final String? result = await anchoredSheet<String>(
    context: context,
    anchorKey: filterButtonKey,
    builder: (context) => Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        'All', 'Recent', 'Favorites', 'Archived'
      ].map((filter) => ListTile(
        title: Text(filter),
        trailing: selectedFilter == filter ? Icon(Icons.check) : null,
        onTap: () => context.popAnchoredSheet(filter),
      )).toList(),
    ),
  );
  
  if (result != null) {
    setState(() => selectedFilter = result);
  }
}

🎯 Advanced Features #

⚡ Performance Optimizations (NEW in v1.2.0) #

Anchored sheets now provide ultra-smooth animations with optimized timing:

// All animations now run at optimal 60fps performance
anchoredSheet(
  context: context,
  // Automatically optimized:
  // - 16ms frame-perfect timing
  // - Unified status bar rendering
  // - Synchronized content animation
  // - Reduced transition delays
  builder: (context) => YourContent(),
);

Performance Improvements:

  • Unified Rendering: Status bar and content render as single Material widget
  • Smoother Animations: Reduced enter/exit timing for more responsive feel
  • No Visual Lag: Eliminated desynchronization between status bar and content

🚫 Smart Duplicate Prevention (NEW in v1.2.0) #

Anchored sheets now automatically prevent duplicate rendering when the same button is clicked multiple times:

final GlobalKey menuButtonKey = GlobalKey();

ElevatedButton(
  key: menuButtonKey,
  onPressed: () {
    // First click: shows the sheet
    // Second click: dismisses the sheet (prevents duplicate)
    // Third click: shows the sheet again
    anchoredSheet(
      context: context,
      anchorKey: menuButtonKey, // Same anchor = smart toggle behavior
      builder: (context) => MenuContent(),
    );
  },
  child: Text('Menu'),
)

How it works:

  • Same anchor key → Dismisses current sheet (prevents duplicate)
  • Different anchor key → Replaces current sheet smoothly
  • No anchor key → Uses default replacement behavior

🔄 Auto Sheet Replacement (NEW in v1.2.0) #

When showing a new sheet while another is open, it automatically replaces the current one:

// This will replace any currently open sheet
anchoredSheet(
  context: context,
  replaceSheet: true, // Default behavior
  builder: (context) => NewContent(),
);

// Disable replacement to stack sheets (not recommended)
anchoredSheet(
  context: context,
  replaceSheet: false,
  builder: (context) => StackedContent(),
);

🎛️ Modal Management (NEW in v1.2.0) #

Automatically dismiss other types of modals before showing an anchored sheet:

// Dismiss bottom sheets, dialogs, etc. first
anchoredSheet(
  context: context,
  dismissOtherModals: true,
  builder: (context) => CleanSlateContent(),
);

Status Bar Handling #

The package automatically handles status bar overlap:

anchoredSheet(
  context: context,
  useSafeArea: true, // ✅ Respects status bar and notch
  builder: (context) => YourContent(),
);

When a sheet would overlap with the status bar, it automatically:

  • Extends the background color to cover the status bar
  • Maintains proper content positioning
  • Preserves smooth animations

MainAxisSize.min Support #

Unlike many modal implementations, anchored_sheets naturally supports MainAxisSize.min:

// ✅ This works perfectly!
Column(
  mainAxisSize: MainAxisSize.min, // Automatically sizes to content
  children: [
    Text('Dynamic content'),
    if (showExtraContent) 
      Text('This appears conditionally'),
    ElevatedButton(
      onPressed: () => context.popAnchoredSheet(),
      child: Text('Close'),
    ),
  ],
)

Context-Free Dismissal #

Dismiss sheets from anywhere in your app:

// In a utility class
class NotificationService {
  static void showNotification(String message) {
    anchoredSheet(
      context: navigatorKey.currentContext!,
      builder: (context) => NotificationWidget(message),
    );
    
    // Auto-dismiss after 3 seconds
    Timer(Duration(seconds: 3), () {
      context.popAnchoredSheet(); // No context needed! 🎉
    });
  }
}

// In a service class
class ApiService {
  static Future<void> logout() async {
    await _performLogout();
    
    // Dismiss any open sheets
    context.popAnchoredSheet();
    
    // Navigate to login
    navigatorKey.currentState?.pushReplacementNamed('/login');
  }
}

🎨 Theming #

Material 3 Integration #

// In your app theme
ThemeData(
  useMaterial3: true,
  bottomSheetTheme: BottomSheetThemeData(
    backgroundColor: Colors.white,
    elevation: 8,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
    ),
  ),
)

// Sheets automatically inherit theme
anchoredSheet(
  context: context,
  // backgroundColor, elevation, shape inherited from theme
  builder: (context) => YourContent(),
);

Custom Theming #

anchoredSheet(
  context: context,
  backgroundColor: Theme.of(context).colorScheme.surface,
  elevation: 12,
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(20),
    side: BorderSide(
      color: Theme.of(context).colorScheme.outline,
      width: 1,
    ),
  ),
  builder: (context) => YourContent(),
);

🔧 Migration Guide #

From showModalBottomSheet #

// Before (showModalBottomSheet)
showModalBottomSheet(
  context: context,
  builder: (context) => YourContent(),
);

// After (anchored_sheets)
anchoredSheet(
  context: context,
  builder: (context) => YourContent(),
);

The API is intentionally similar to showModalBottomSheet for easy migration.

From other top sheet packages #

Most parameters map directly:

// Other packages
showTopSheet(
  context: context,
  child: YourContent(),
);

// anchored_sheets
anchoredSheet(
  context: context,
  builder: (context) => YourContent(),
);

🐛 Troubleshooting #

Sheet not sizing correctly #

Problem: Sheet takes full height instead of sizing to content.

Solution: Use MainAxisSize.min in your Column:

// ❌ Don't do this
Column(
  children: [...], // Takes full height
)

// ✅ Do this instead
Column(
  mainAxisSize: MainAxisSize.min, // Sizes to content
  children: [...],
)

Content getting cut off #

Problem: Content appears truncated or overlaps status bar.

Solution: Use useSafeArea: true:

anchoredSheet(
  context: context,
  useSafeArea: true, // ✅ Respects status bar
  builder: (context) => YourContent(),
);

Animations feel slow #

Problem: Default animation duration is too long.

Solution: Customize animation duration:

anchoredSheet(
  context: context,
  animationDuration: Duration(milliseconds: 200), // Faster
  builder: (context) => YourContent(),
);

🆕 What's New in v1.2.0 #

⚡ Status Bar Animation Performance #

  • Eliminated Delay: Fixed visual delay between status bar background and sheet content
  • Unified Rendering: Single Material container for synchronized rendering
  • 16ms Timing: Optimized all delays to single frame timing at 60fps
  • Smoother Feel: Enhanced animation responsiveness (220ms/180ms timing)
  • Zero Lag: Removed rendering desynchronization issues

🚫 Smart Duplicate Prevention #

  • Anchor-based Intelligence: Uses existing anchorKey to detect duplicate sheet requests
  • No Manual IDs: No need to manage custom sheet identifiers
  • Toggle Behavior: Same button click dismisses sheet, different source replaces it
  • Zero Configuration: Works automatically with existing code

� Automatic Sheet Replacement #

  • Default Replacement: replaceSheet = true by default for seamless UX
  • Smooth Transitions: 50ms optimized delay for perfect timing
  • Context Safety: Automatic context.mounted checks prevent errors
  • Backward Compatible: Existing code works without changes

🎛️ Enhanced Modal Management #

  • Multi-Modal Support: dismissOtherModals parameter for clean slate behavior
  • Bottom Sheet Integration: Seamlessly handles existing bottom sheets
  • Dialog Compatibility: Works with alert dialogs and custom dialogs
  • SnackBar Coexistence: Smart handling of persistent UI elements

�️ Architecture Improvements #

  • Anchor Key Tracking: Intelligent storage and comparison of anchor keys
  • Controller Enhancement: Better generic type handling and safety
  • Performance Optimization: Reduced animation times and smoother transitions
  • Memory Management: Automatic cleanup of tracking variables

� Developer Experience #

  • Cleaner API: Simplified sheet management without manual configuration
  • Better Debugging: Enhanced error messages and development warnings
  • Example Updates: Comprehensive demos showing all new features
  • Documentation: Updated guides and best practices

🏆 Best Practices #

✅ Do's #

// ✅ Use context.popAnchoredSheet() for dismissal
ElevatedButton(
  onPressed: () => context.popAnchoredSheet('result'),
  child: Text('Close'),
)

// ✅ Use Provider for state management
Consumer<AppState>(
  builder: (context, state, child) => YourWidget(),
)

// ✅ Set useSafeArea for proper status bar handling
anchoredSheet(
  context: context,
  useSafeArea: true,
  builder: (context) => YourContent(),
)

// ✅ Use MainAxisSize.min for auto-sizing
Column(
  mainAxisSize: MainAxisSize.min,
  children: [...],
)

❌ Don'ts #

// ❌ Don't use Navigator.pop() directly
Navigator.of(context).pop(); // Can cause issues

// ❌ Don't manage state manually when using Provider
setState(() {
  // Let Provider handle state updates
});

// ❌ Don't forget to handle async gaps
// Use if (mounted) checks when needed

🔄 Migration Guide #

From v1.1.x to v1.2.0 #

Good News: No breaking changes! All existing code continues to work.

New Defaults (automatically enabled):

// Before v1.2.0
anchoredSheet(
  context: context,
  replaceSheet: false, // Was default
  builder: (context) => YourContent(),
);

// After v1.2.0 (automatic improvement)
anchoredSheet(
  context: context,
  replaceSheet: true, // Now default - better UX
  builder: (context) => YourContent(),
);

Smart Features (work automatically):

final buttonKey = GlobalKey();

// This now has smart duplicate prevention built-in
ElevatedButton(
  key: buttonKey,
  onPressed: () {
    anchoredSheet(
      context: context,
      anchorKey: buttonKey, // Automatic smart behavior
      builder: (context) => YourContent(),
    );
  },
  child: Text('Smart Button'),
)

Optional Enhancements:

// Dismiss other modals first (opt-in)
anchoredSheet(
  context: context,
  dismissOtherModals: true, // NEW: Clean slate behavior
  builder: (context) => YourContent(),
);

From v1.0.x to v1.2.0 #

The API is mostly backwards compatible, but we recommend these updates:

// Old (still works)
dismissAnchoredSheet('result');

// New (recommended)
context.popAnchoredSheet('result');

Adding Provider Support #

// 1. Add provider dependency
dependencies:
  provider: ^6.1.2

// 2. Update your main app
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => AppState(),
      child: MyApp(),
    ),
  );
}

// 3. Use Consumer in your sheets
Consumer<AppState>(
  builder: (context, appState, child) {
    return YourSheetContent(
      onChanged: (value) => appState.updateValue(value),
    );
  },
)

🙏 Acknowledgments #

  • Inspired by Material Design guidelines
  • Built on Flutter's robust animation and layout systems
  • Thanks to the Flutter community for feedback and suggestions

📧 Support #


2
likes
0
points
89
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter package to create anchored sheets that can be dragged and snapped to different positions on the screen.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter

More

Packages that depend on anchored_sheets