overlay_layers

A flexible overlay system for Flutter with support for popups, modals, toasts, and dialogs.

Built on Flutter's native Overlay system with type-safe data passing and lifecycle management.

Features

  • Native Flutter integration: Uses Flutter's Overlay API under the hood
  • No wrapper required: Works immediately without wrapping your app
  • Type-safe: Full type safety with generics
  • Future-extensible: Designed to support popups, modals, toasts, and dialogs
  • Simple API: Easy-to-use controllers and data contexts
  • Lifecycle callbacks: onDataChange and onClose hooks
  • Animated widgets: Built-in animation support
  • Positioned popups: Popup positioning relative to target widgets

Installation

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

dependencies:
  overlay_layers: ^3.0.0

Quick Start

No setup required! Just import and use:

import 'package:overlay_layers/overlay_layers.dart';

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {
        PopupController.of(context).open(
          builder: (context) => MyPopupWidget(),
        );
      },
      child: Text('Open Popup'),
    );
  }
}

That's it! The package uses a global singleton manager automatically.

Usage

1. Open a popup

import 'package:overlay_layers/overlay_layers.dart';

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final popupController = PopupController.of(context);

    return ElevatedButton(
      onPressed: () {
        popupController.open(
          builder: (context) => MyPopupWidget(),
          options: OverlayCreateOptions(
            initialData: {'message': 'Hello!'},
            onClose: (data) => print('Closed with: $data'),
          ),
        );
      },
      child: Text('Open Popup'),
    );
  }
}

3. Access popup data from within a popup

class MyPopupWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final popup = PopupDataContext.of<Map<String, dynamic>>(context);

    return OverlayScaffold(
      onBackdropTap: () => popup.close(),
      child: AnimatedOverlay(
        child: Container(
          padding: EdgeInsets.all(24),
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(16),
          ),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text(popup.data['message']),
              SizedBox(height: 16),
              ElevatedButton(
                onPressed: () {
                  popup.updatePopupData({'message': 'Updated!'});
                },
                child: Text('Update'),
              ),
              ElevatedButton(
                onPressed: () => popup.close(),
                child: Text('Close'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

2. Work with modals

Modals are similar to popups but with one key difference: only one modal can be displayed at a time. Opening a new modal automatically closes the previous one (interchangeable behavior).

import 'package:overlay_layers/overlay_layers.dart';

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final modalController = ModalController.of(context);

    return ElevatedButton(
      onPressed: () {
        // Opening this modal closes any existing modal
        modalController.open(
          builder: (context) => MyModalWidget(),
          options: OverlayCreateOptions(
            initialData: {'title': 'Welcome'},
            onClose: (data) => print('Modal closed'),
          ),
        );
      },
      child: Text('Open Modal'),
    );
  }
}

3. Access modal data from within

class MyModalWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final modal = ModalDataContext.of<Map<String, dynamic>>(context);

    return OverlayScaffold(
      onBackdropTap: () => modal.close(),
      child: AnimatedOverlay(
        child: Container(
          padding: EdgeInsets.all(32),
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(16),
          ),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text(modal.data['title']),
              SizedBox(height: 16),
              ElevatedButton(
                onPressed: () {
                  modal.updateModalData({'title': 'Updated!'});
                },
                child: Text('Update'),
              ),
              ElevatedButton(
                onPressed: () => modal.close(),
                child: Text('Close'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

API Reference

PopupController

Controller for managing popups.

final controller = PopupController.of(context);

// Open a popup
final id = controller.open(
  builder: (context) => MyPopupWidget(),
  options: OverlayCreateOptions(
    initialData: {},
    onDataChange: (data) => print(data),
    onClose: (data) => print('Closed'),
  ),
);

// Close specific popup
controller.close(id);

// Close topmost popup
controller.close();

// Close all popups
controller.closeAll();

// Update popup data from outside
controller.updatePopupData(id, {'key': 'value'});

// Get all active popups
final popups = controller.popups;

PopupDataContext

Access and update popup data from within a popup widget.

final popup = PopupDataContext.of<MyDataType>(context);

// Access data
final data = popup.data;

// Update data (merges with existing)
popup.updatePopupData({'key': 'value'});

// Close popup
popup.close();

// Close with final data
popup.close({'finalKey': 'finalValue'});

ModalController

Controller for managing modals. Interchangeable behavior: Only one modal can be displayed at a time.

final controller = ModalController.of(context);

// Open a modal (closes any existing modal automatically)
controller.open(
  builder: (context) => MyModalWidget(),
  options: OverlayCreateOptions(
    initialData: {},
    onDataChange: (data) => print(data),
    onClose: (data) => print('Closed'),
  ),
);

// Close current modal
controller.close();

// Update current modal data
controller.updateModalData({'key': 'value'});

// Get current modal (if any)
final currentModal = controller.modal;

ModalDataContext

Access and update modal data from within a modal widget.

final modal = ModalDataContext.of<MyDataType>(context);

// Access data
final data = modal.data;

// Update data (merges with existing)
modal.updateModalData({'key': 'value'});

// Close modal
modal.close();

// Close with final data
modal.close({'finalKey': 'finalValue'});

OverlayManager

Access the global singleton manager for advanced use cases:

// Access all active overlays
final overlays = OverlayManager.instance.overlays;

// Get overlays by type
final popups = OverlayManager.instance.getOverlaysByType(OverlayType.popup);

// Or use a custom manager instance (advanced)
final customManager = OverlayManager.custom();
PopupController.withManager(context, customManager).open(...);

Helper Widgets

OverlayScaffold

Generic overlay scaffold with backdrop and positioning. Works with all overlay types.

OverlayScaffold(
  backdropColor: Colors.black.withOpacity(0.5),
  onBackdropTap: () => context.close(),
  alignment: Alignment.center,
  child: MyContent(),
)

AnimatedOverlay

Flexible animated wrapper with configurable fade, scale, and slide animations.

// Default: fade + scale
AnimatedOverlay(
  duration: Duration(milliseconds: 250),
  curve: Curves.easeOut,
  child: MyContent(),
)

// Slide from bottom (like bottom sheet)
AnimatedOverlay(
  slideFrom: Offset(0, 1),
  child: MyContent(),
)

// Slide from left with fade, no scale
AnimatedOverlay(
  slideFrom: Offset(-1, 0),
  scale: false,
  child: MyContent(),
)

// Custom scale range
AnimatedOverlay(
  scaleBegin: 0.8,
  scaleEnd: 1.0,
  child: MyContent(),
)

PositionedOverlay

Position overlay relative to a target widget. Perfect for tooltips, dropdowns, and context menus.

PositionedOverlay(
  targetRect: targetRect,
  position: OverlayPosition.bottom,
  spacing: 8.0,
  child: MyContent(),
)

Overlay Types

The package currently supports:

  • Popups: Multiple popups can be displayed simultaneously
  • Modals: Interchangeable - only one modal at a time (opening a new modal closes the previous)

Future Features

Additional overlay types planned:

  • Tooltip: Positioned tooltips using PositionedOverlay
  • Toast: Temporary notifications
  • Dialog: Alert-style overlays

Architecture

The package is built on Flutter's native Overlay system with a centralized OverlayManager for state management. Each overlay type (popup, toast, modal, dialog) has its own controller but shares the same underlying infrastructure.

How it works:

  1. Controllers access Flutter's native Overlay.of(context)
  2. OverlayManager creates and manages OverlayEntry instances
  3. Each entry wraps your widget with type-safe data context
  4. Updates trigger markNeedsBuild() on specific entries

Benefits:

  • No wrapper widget required (uses existing Overlay from MaterialApp/CupertinoApp)
  • Native Flutter integration and performance
  • Consistent API across overlay types
  • Proper z-index management
  • Type-safe data passing with generics
  • Unified lifecycle management
  • Tree-shakable widgets (v3.0.0+): unused widgets don't bloat your bundle

License

MIT

Libraries

overlay_layers