An animated day/night toggle for Flutter. The sun sits in a blue sky with clouds drifting past; flip it and the sky turns to night, stars fade in and the sun becomes a cratered moon.
- One
CustomPainter. No image assets, no dependencies beyond Flutter, sharp at any size. - Idle motion: stars twinkle, clouds drift, the sun's rays turn, and a meteor crosses the night sky once per cycle.
- Drag the orb and it tracks your finger exactly, squashing while you hold it. A quick flick counts even before the midpoint.
- Screen reader labels, keyboard focus and activation, and automatic reduce-motion support.
| Requirement | Minimum |
|---|---|
| Flutter | 3.27.0 |
| Dart | 3.6.0 |
| Android | iOS | Web | macOS | Windows | Linux |
|---|---|---|---|---|---|
| ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
There is no platform-specific code, so it runs anywhere Flutter runs.
flutter pub add sun_moon_switchimport 'package:sun_moon_switch/sun_moon_switch.dart';SunMoonSwitch is controlled, like Flutter's own Switch. It holds no state of its own,
so give it a value and pass a new one back from onChanged.
bool _isDark = false;
SunMoonSwitch(
value: _isDark,
onChanged: (bool isDark) => setState(() => _isDark = isDark),
)class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool _isDark = false;
@override
Widget build(BuildContext context) {
return MaterialApp(
themeMode: _isDark ? ThemeMode.dark : ThemeMode.light,
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
home: Scaffold(
body: Center(
child: SunMoonSwitch(
value: _isDark,
onChanged: (bool isDark) => setState(() => _isDark = isDark),
),
),
),
);
}
}The orb is sized as a fraction of the track height, so the presets hold up whatever dimensions you pick.
SunMoonSwitch(
value: _isDark,
onChanged: _set,
thumbSize: SunMoonSwitchSize.large,
width: 160,
height: 68,
)| Preset | Ratio | Looks like |
|---|---|---|
small |
55 % | Compact orb, plenty of open sky |
medium |
70 % | The default |
large |
85 % | Bold orb filling the track |
Every color is a parameter. Start from the defaults and override only what you need.
SunMoonSwitch(
value: _isDark,
onChanged: _set,
colors: const SunMoonSwitchColors(
daySkyTop: Color(0xFFFFC48C),
daySkyBottom: Color(0xFFF2709C),
nightSkyTop: Color(0xFF3A1C71),
nightSkyBottom: Color(0xFF10061F),
sunEdge: Color(0xFFFF7B54),
moonGlow: Color(0xFFD3A4FF),
),
)SunMoonSwitchColors.midnight is a second ready-made palette. copyWith and lerp are
there for deriving or animating your own.
While you drag, the orb tracks your finger one to one with no easing in between. Let go past the midpoint and it commits. Flick it and the fling wins, even from a standstill on the wrong side.
| Property | Type | Default | Description |
|---|---|---|---|
value |
bool |
required | true is night/moon, false is day/sun |
onChanged |
ValueChanged<bool>? |
required | Called with the new value. null disables the switch |
thumbSize |
SunMoonSwitchSize |
medium |
Orb diameter preset |
width |
double |
110.0 |
Track width |
height |
double |
48.0 |
Track height |
duration |
Duration |
600ms |
Toggle animation length |
curve |
Curve |
SunMoonSwitch.defaultCurve |
Toggle curve. Overshoot is supported |
colors |
SunMoonSwitchColors |
.defaults |
Full palette |
animateAmbient |
bool |
true |
Twinkle, drift, rays and meteors |
ambientDuration |
Duration |
8s |
Length of one idle-motion cycle |
enableFeedback |
bool |
true |
Haptics on Android and iOS |
focusNode |
FocusNode? |
null |
Keyboard focus control |
autofocus |
bool |
false |
Take focus when first shown |
semanticLabel |
String? |
'Dark mode' |
What screen readers announce |
daySkyTop, daySkyBottom, nightSkyTop, nightSkyBottom, sunCore, sunEdge,
sunGlow, moonCore, moonEdge, moonGlow, crater, star, cloud, plus
SunMoonSwitchColors.defaults, .midnight, copyWith() and lerp().
Idle motion loops forever, the same way LinearProgressIndicator does, so
pumpAndSettle() will time out on any screen holding a SunMoonSwitch. Pump fixed
durations instead:
await tester.pump(const Duration(milliseconds: 700));Or turn the idle motion off for the test:
SunMoonSwitch(value: false, onChanged: _set, animateAmbient: false)cd example
flutter runIf something is missing, broken, or just annoying, say so. Bug reports, questions and ideas all go to GitHub issues; there is a template for each.
New palettes, extra animation touches, or knobs you need exposed are all fair game. Most of the widget is one painter, so small additions are usually easy.
Sending a pull request? Run these first:
dart format .
flutter analyze
flutter testThe README animations are generated rather than screen-recorded. See tool/ for
the frame recorder and the ffmpeg script that stitches the GIFs.
This is free and MIT licensed, and it stays that way. If it saved you an afternoon and you feel like giving something back:
Starring the repo and reporting bugs helps just as much.
MIT. See LICENSE.





