theme_debug_overlay

pub.dev License: MIT flutter_lints Platform Give Gursha

A draggable, animated debug overlay that lets you toggle ThemeMode (light / dark / system) at runtime — without touching a single line of app code.

Zero overhead in production. The overlay renders an empty SizedBox in profile and release builds (kDebugMode == false), so shipping it unconditionally is completely safe.


Features

  • Three-mode switcher — Light, Dark, System — with animated selection highlighting.
  • Fully draggable — long-press and drag to any corner of the screen.
  • Position persistence — the last position is restored across app restarts via SharedPreferences (opt-out with persistPosition: false).
  • Smooth animationsAnimatedSize for the expanding panel, AnimatedContainer for button state, AnimatedRotation on the main icon.
  • Tooltip support — every button carries a tooltip for accessibility and hover UX.
  • Framework agnostic — no Riverpod, Bloc, or Provider required. Pass your current ThemeMode and a callback; plug it into any state solution.
  • Customisable — configure initialPosition, storageKey, and persistPosition as needed.

Getting started

1. Add the dependency

# pubspec.yaml
dependencies:
  theme_debug_overlay: ^1.0.0
flutter pub get

2. Wrap your screen

import 'package:theme_debug_overlay/theme_debug_overlay.dart';

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  ThemeMode _themeMode = ThemeMode.system;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      themeMode: _themeMode,
      theme: ThemeData.light(useMaterial3: true),
      darkTheme: ThemeData.dark(useMaterial3: true),
      home: ThemeDebugOverlayScaffold(
        themeMode: _themeMode,
        onThemeModeChanged: (mode) => setState(() => _themeMode = mode),
        child: const HomeScreen(),
      ),
    );
  }
}

Option B — manual Stack placement

Stack(
  children: [
    const HomeScreen(),
    ThemeDebugOverlay(
      themeMode: _themeMode,
      onThemeModeChanged: (mode) => setState(() => _themeMode = mode),
    ),
  ],
)

Usage

With Riverpod

// In your widget
final themeMode = ref.watch(themeModeProvider);

ThemeDebugOverlayScaffold(
  themeMode: themeMode,
  onThemeModeChanged: (mode) =>
      ref.read(themeModeProvider.notifier).set(mode),
  child: const HomeScreen(),
)

With Bloc / Cubit

BlocBuilder<ThemeCubit, ThemeMode>(
  builder: (context, themeMode) => ThemeDebugOverlayScaffold(
    themeMode: themeMode,
    onThemeModeChanged: context.read<ThemeCubit>().setThemeMode,
    child: const HomeScreen(),
  ),
)

With Provider / ChangeNotifier

Consumer<ThemeNotifier>(
  builder: (context, notifier, _) => ThemeDebugOverlayScaffold(
    themeMode: notifier.themeMode,
    onThemeModeChanged: notifier.setThemeMode,
    child: const HomeScreen(),
  ),
)

Custom initial position

ThemeDebugOverlay(
  themeMode: _themeMode,
  onThemeModeChanged: _handleThemeChange,
  initialPosition: const Offset(16, 300), // bottom-left area
)

Disable persistence

ThemeDebugOverlay(
  themeMode: _themeMode,
  onThemeModeChanged: _handleThemeChange,
  persistPosition: false, // position resets each launch
)

Multiple overlays (unique storage keys)

ThemeDebugOverlay(
  themeMode: _themeMode,
  onThemeModeChanged: _handleThemeChange,
  storageKey: 'overlay_screen_a',
)

API reference

ThemeDebugOverlay

Parameter Type Default Description
themeMode ThemeMode required The currently active theme mode.
onThemeModeChanged ValueChanged<ThemeMode> required Called when the user picks a new mode.
persistPosition bool true Save/restore position with SharedPreferences.
initialPosition Offset Offset(16, 100) Starting position (used when no persisted value exists).
storageKey String 'theme_debug_overlay' SharedPreferences key prefix. Change for multiple overlays.

ThemeDebugOverlayScaffold

All parameters from ThemeDebugOverlay, plus:

Parameter Type Default Description
child Widget required The widget displayed beneath the overlay.

How it works

ThemeDebugOverlay
 └─ Positioned           (left: _x, top: _y — updated on drag end)
     └─ ScaleTransition  (entrance animation)
         └─ Draggable    (long-press to drag)
             └─ Column
                 ├─ AnimatedSize
                 │   └─ [Light / Dark / System option buttons]
                 └─ Main toggle button (opens/closes the panel)

Position saves are debounced by 500 ms, so a single drag results in at most one SharedPreferences write after the user stops moving the overlay.


Platform support

Platform Supported
Android
iOS
Web
macOS
Windows
Linux

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feat/amazing-feature)
  3. Run the tests (flutter test)
  4. Open a pull request

Support

If this package saves you time, consider giving a Gursha!

Give Gursha

License

MIT © 2026 Habesha

Libraries

theme_debug_overlay
A draggable, animated Flutter debug overlay for toggling ThemeMode at runtime. Renders only in debug builds — zero overhead in release.