Illustration Onboarding
Loading preview
Starting the Flutter app
A Flutter onboarding flow with illustrated slides, page dots, and skip and next controls.
import 'package:flutter/material.dart';
class _Slide {
final IconData icon;
final String title;
final String body;
const _Slide(this.icon, this.title, this.body);
}
class IllustrationOnboarding extends StatefulWidget {
const IllustrationOnboarding({super.key});
@override
State<IllustrationOnboarding> createState() => _IllustrationOnboardingState();
}
class _IllustrationOnboardingState extends State<IllustrationOnboarding> {
final _controller = PageController();
int _page = 0;
static const _slides = [
_Slide(Icons.checklist_rtl, 'Plan your day',
'Organize tasks, set priorities, and keep everything in one place.'),
_Slide(Icons.insights, 'Track your progress',
'Visualize streaks and habits so you always know where you stand.'),
_Slide(Icons.notifications_active_outlined, 'Stay on time',
'Smart reminders nudge you at just the right moment.'),
];
@override
void dispose() {
_controller.dispose();
super.dispose();
}
bool get _isLast => _page == _slides.length - 1;
// Replace with navigation to your first real screen.
void _exit(String message) {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(SnackBar(content: Text(message)));
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scheme = theme.colorScheme;
return Scaffold(
body: SafeArea(
child: Column(
children: [
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => _exit('Onboarding skipped'),
child: const Text('Skip')),
),
Expanded(
child: PageView.builder(
controller: _controller,
onPageChanged: (i) => setState(() => _page = i),
itemCount: _slides.length,
itemBuilder: (context, i) {
final s = _slides[i];
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: scheme.primaryContainer,
shape: BoxShape.circle,
),
child: Icon(s.icon,
size: 96, color: scheme.onPrimaryContainer),
),
const SizedBox(height: 40),
Text(s.title,
style: theme.textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Text(s.body,
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge
?.copyWith(color: scheme.onSurfaceVariant)),
],
),
);
},
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(_slides.length, (i) {
final active = i == _page;
return AnimatedContainer(
duration: const Duration(milliseconds: 250),
margin: const EdgeInsets.symmetric(horizontal: 4),
width: active ? 24 : 8,
height: 8,
decoration: BoxDecoration(
color: active ? scheme.primary : scheme.outlineVariant,
borderRadius: BorderRadius.circular(4),
),
);
}),
),
Padding(
padding: const EdgeInsets.all(24),
child: SizedBox(
width: double.infinity,
height: 52,
child: FilledButton(
onPressed: () {
if (_isLast) {
_exit('Onboarding finished');
return;
}
_controller.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut);
},
child: Text(_isLast ? 'Get started' : 'Next'),
),
),
),
],
),
),
);
}
}
Add it to your project
Copies the Dart source into your app, so you own it. Flutter SDK only, no extra packages.
First time? Run dart pub global activate fluttertemplates_dev, or read the CLI guide.
How it works
A three-slide PageView.builder where _page drives the pill-shaped dots and the _isLast getter flips the button label from Next to Get started. Advancing uses nextPage with a 300ms ease. On the final slide the button calls _exit, as Skip does from any slide, and that only shows a SnackBar, so the flow has no real destination yet. The controller is disposed properly.
Adapting it
- Replace
_exitwith navigation to your first real screen, saving a “seen” flag. - Give
SkipandGet starteddifferent destinations if your flow needs it. - Swap
s.iconin the 200px circle for actual artwork.