Skip to main content

Overlays

BookWish uses bottom sheet overlays for contextual content and actions, providing a non-intrusive way to display detailed information without full page navigation.

Overlay Pattern

All overlays use the same consistent pattern with DraggableScrollableSheet inside showModalBottomSheet.

Base Implementation

void showExampleOverlay(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.9,
minChildSize: 0.5,
maxChildSize: 0.95,
builder: (context, scrollController) => ExampleOverlay(
scrollController: scrollController,
),
),
);
}

class ExampleOverlay extends ConsumerWidget {
final ScrollController? scrollController;

const ExampleOverlay({super.key, this.scrollController});


Widget build(BuildContext context, WidgetRef ref) {
return Container(
decoration: BoxDecoration(
color: AppColors.parchment,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: ListView(
controller: scrollController,
padding: const EdgeInsets.all(16),
children: [
// Overlay content
],
),
);
}
}

Available Overlays

OverlayFilePurposeKey Features
Book Infobook_info_overlay.dartDisplay book details, add to cart/wishlistCover image, metadata, availability check, purchase/wishlist actions
Settingssettings_overlay.dartApp settings and preferencesTabbed interface (Account/Preferences), profile editing, addresses, notifications
Profileprofile_overlay.dartUser profile informationUser details, stats, follow button, activity feed
Scribblescribble_overlay.dartNote editing interfaceRich text editor, book association
Store Searchstore_search_overlay.dartSearch and select bookstoresLocation-based search, store selection

Detailed Implementations

Book Info Overlay

Located in /lib/ui/overlays/book_info_overlay.dart

Features:

  • Displays complete book information from Book model
  • Shows cover image, title, subtitle, authors, description
  • Metadata section (publisher, pages, ISBN, categories)
  • Availability checking (home store vs Ingram)
  • Add to cart with store context
  • Add to wishlist with guest user handling
  • Stock alerts for unavailable books

Show Method:

void showBookInfoOverlay(BuildContext context, Book book) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.9,
minChildSize: 0.5,
maxChildSize: 0.95,
builder: (context, scrollController) => BookInfoOverlay(
book: book,
scrollController: scrollController,
),
),
);
}

Usage:

// From book card or search result
showBookInfoOverlay(context, book);

Settings Overlay

Located in /lib/ui/overlays/settings_overlay.dart

Features:

  • Two tabs: Account and Preferences
  • Account tab:
    • Profile editing (display name, username, email)
    • Avatar upload with loading state
    • Subscription tier display
    • Home store management
    • Shipping addresses CRUD
    • Password change dialog
  • Preferences tab:
    • Push/email notification toggles
    • Private profile setting
    • Stock alerts management
    • Blocked users management
    • Browser helper toggle
    • About/legal links
    • App version display

Show Method:

void showSettingsOverlay(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.85,
minChildSize: 0.5,
maxChildSize: 0.95,
expand: false,
builder: (context, scrollController) => SettingsOverlay(
scrollController: scrollController,
),
),
);
}

Tabs Pattern:

enum SettingsView { account, preferences }

// Use BwFilterChips for tab selection
BwFilterChips<SettingsView>(
options: const [
FilterOption(value: SettingsView.account, label: 'Account'),
FilterOption(value: SettingsView.preferences, label: 'Preferences'),
],
selected: _selectedView,
onChanged: (view) => setState(() => _selectedView = view),
),

Profile Overlay

Located in /lib/ui/overlays/profile_overlay.dart

Features:

  • User avatar, display name, username
  • Follow/unfollow button
  • User statistics (followers, following, books)
  • Activity feed (reviews, lines, notes)
  • Block/report actions

Show Method:

void showProfileOverlay(BuildContext context, String userId) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.9,
minChildSize: 0.5,
maxChildSize: 0.95,
builder: (context, scrollController) => ProfileOverlay(
userId: userId,
scrollController: scrollController,
),
),
);
}

Scribble Overlay

Located in /lib/ui/overlays/scribble_overlay.dart

Features:

  • Rich text editing
  • Book association
  • Save/discard actions
  • Character count

Usage:

showScribbleOverlay(
context,
bookId: bookId,
existingNote: note, // null for new note
);

Store Search Overlay

Located in /lib/ui/overlays/store_search_overlay.dart

Features:

  • Search bookstores by name or location
  • Display store cards with distance
  • Return selected store to caller

Show Method:

Future<Store?> showStoreSearchOverlay(BuildContext context) async {
return await showModalBottomSheet<Store>(
context: context,
isScrollControlled: true,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.9,
minChildSize: 0.5,
maxChildSize: 0.95,
builder: (context, scrollController) => StoreSearchOverlay(
scrollController: scrollController,
),
),
);
}

// Usage
final selectedStore = await showStoreSearchOverlay(context);
if (selectedStore != null) {
// Use selected store
}

Overlay Structure

Standard Header

All overlays should include:

  1. Handle bar - Visual drag indicator
  2. Title - Clear overlay purpose
  3. Close button - X icon in top right
Column(
children: [
const SizedBox(height: 8),
// Handle bar
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 16),
// Title and close button
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Overlay Title', style: AppTypography.headingLarge),
IconButton(
icon: Icon(
CupertinoIcons.xmark_circle_fill,
color: AppColors.inkBlue.withValues(alpha: 0.5),
),
onPressed: () => Navigator.pop(context),
),
],
),
),
// Content below
],
)

Scrollable Content

Use the provided scrollController for the main content area:

ListView(
controller: scrollController,
padding: const EdgeInsets.all(16),
children: [
// Content
],
)

Or for single-child scrolling:

SingleChildScrollView(
controller: scrollController,
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Content
],
),
)

Design Guidelines

Sizing

  • initialChildSize: 0.85 - 0.9 (most overlays)
  • minChildSize: 0.5 (minimum drag height)
  • maxChildSize: 0.95 (leave status bar visible)

Colors

  • Background: AppColors.parchment (matches app background)
  • Surface: Colors.white for cards/containers
  • Border: AppColors.border for subtle borders

Border Radius

  • Top corners: 16px rounded
  • Bottom corners: 0px (full width)
decoration: BoxDecoration(
color: AppColors.parchment,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),

Spacing

  • Top padding: 8px (before handle)
  • Handle to title: 16px
  • Content padding: 16px all sides
  • Section spacing: 24px between major sections

Typography

  • Title: AppTypography.headingLarge
  • Section headers: AppTypography.heading
  • Body text: AppTypography.body or AppTypography.cardBody for cards
  • Captions: AppTypography.caption

Loading States

Handle async data with proper loading indicators:

final dataAsync = ref.watch(dataProvider);

return dataAsync.when(
data: (data) => _buildContent(data),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(CupertinoIcons.exclamationmark_circle, size: 48),
SizedBox(height: 16),
Text('Error: $error'),
TextButton(
onPressed: () => ref.invalidate(dataProvider),
child: Text('Retry'),
),
],
),
),
);

State Management

ConsumerWidget for Read-Only

class ExampleOverlay extends ConsumerWidget {

Widget build(BuildContext context, WidgetRef ref) {
final data = ref.watch(dataProvider);
return Container();
}
}

ConsumerStatefulWidget for Mutable State

class ExampleOverlay extends ConsumerStatefulWidget {

ConsumerState<ExampleOverlay> createState() => _ExampleOverlayState();
}

class _ExampleOverlayState extends ConsumerState<ExampleOverlay> {
bool _isEditing = false;


Widget build(BuildContext context) {
return Container();
}
}

Best Practices

  1. Always pass scrollController - Enables drag-to-dismiss
  2. Use consistent sizing - Stick to standard initialChildSize values
  3. Include handle bar - Visual affordance for dragging
  4. Provide close button - Don't rely solely on drag gesture
  5. Handle loading states - Show spinners for async operations
  6. Close before navigation - Use Navigator.pop() before context.go()
  7. Return values - Use generic type for selection overlays
  8. Test keyboard behavior - Ensure inputs work with keyboard open
  9. Avoid nested scrolling - Use single scroll controller
  10. Match theme - Use AppColors and AppTypography consistently

Common Patterns

Overlay with Tabs

enum TabType { tab1, tab2 }

class TabbedOverlay extends ConsumerStatefulWidget {

ConsumerState<TabbedOverlay> createState() => _TabbedOverlayState();
}

class _TabbedOverlayState extends ConsumerState<TabbedOverlay> {
TabType _selectedTab = TabType.tab1;


Widget build(BuildContext context) {
return Container(
child: Column(
children: [
// Header with handle and title
BwFilterChips<TabType>(
options: const [
FilterOption(value: TabType.tab1, label: 'Tab 1'),
FilterOption(value: TabType.tab2, label: 'Tab 2'),
],
selected: _selectedTab,
onChanged: (tab) => setState(() => _selectedTab = tab),
),
Expanded(
child: _selectedTab == TabType.tab1
? _Tab1Content()
: _Tab2Content(),
),
],
),
);
}
}

Overlay with Actions

// Bottom action buttons
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancel'),
),
),
SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: _handleSave,
child: Text('Save'),
),
),
],
),
)

Selection Overlay

Future<String?> showSelectionOverlay(BuildContext context) async {
return await showModalBottomSheet<String>(
context: context,
builder: (context) => DraggableScrollableSheet(
builder: (context, scrollController) => ListView(
controller: scrollController,
children: options.map((option) => ListTile(
title: Text(option),
onTap: () => Navigator.pop(context, option),
)).toList(),
),
),
);
}