How to Build a No Events Empty State in Flutter (Full Code + Preview)
When a calendar or scheduling app has nothing on the agenda yet, a blank list feels broken — a good empty state turns that void into a friendly nudge. This tutorial builds exactly that in Flutter: a centered 'Nothing to do' title, a bundled hand-highlighting-a-calendar illustration that scales to the screen width, a two-line prompt, and a glowing circular '+' button that presses in when tapped. By the end you'll have a fully responsive zero-state screen that centers on tall phones and scrolls instead of overflowing on short ones — all in pure Flutter with one bundled font.

What you'll build
- ✓A white empty-state screen with a centered 'Nothing to do' title over a bundled calendar illustration
- ✓An illustration that scales to 58% of the viewport width, clamped between 190 and 300px so it never gets too small or too large
- ✓A flex-centered layout that stays balanced on tall devices and scrolls instead of overflowing on short ones
- ✓A circular brand-blue '+' add button with a soft glow shadow and a press-scale animation
- ✓Clean color and font tokens plus the bundled Inter font
Step-by-step build
Create the file
Add a new file at lib/no_events/no_events_screen.dart in your Flutter project.
Register the bundled fonts
No external packages — this is pure Flutter. It does bundle its design font (Inter), so drop the font file into fonts/ and declare it in pubspec.yaml:
flutter:
fonts:
- family: Inter
fonts:
- asset: fonts/Inter-Regular.ttfBuild it, piece by piece
Here's how the screen goes together. Each block below is a real slice of the code with a plain-English explanation — paste them in order, or grab the whole file from the next section.
Imports, the screen class, and design tokens
import 'package:flutter/material.dart';
/// "No Events" empty-state screen from the Empty States UI Kit: a centred title
/// ("Nothing to do"), a hand-highlighting-a-calendar illustration, a short
/// supporting line, and a circular "+" action button pinned near the bottom to
/// create the first event.
///
/// Self-contained, pure Flutter. Bundles its design font (Inter) and the kit's
/// original illustration (converted to WebP). Fully responsive — flex spacers
/// centre the content on tall devices and a scroll fallback prevents overflow
/// on short ones. Renders standalone when pushed as a route.
class NoEventsScreen extends StatelessWidget {
const NoEventsScreen({super.key});
static const Color _ink = Color(0xFF1B3554);
static const Color _body = Color(0xFF344B67);
static const Color _blue = Color(0xFF4476FF);
static const String _font = 'Inter';
static const String _image =
'lib/screens/empty_states/no_events/images/no_events_calendar.webp';The file imports only Flutter's material library, then declares NoEventsScreen as a StatelessWidget — the empty state itself never changes, so stateless is correct. Four const tokens hold the palette and assets: _ink (#1B3554) is the dark slate used for the title, _body (#344B67) is the slightly softer slate for the supporting line, _blue (#4476FF) is the brand blue for the add button, and _font is 'Inter'. _image points at the bundled WebP calendar illustration. Pulling these out as named constants keeps the theming in one place.
A responsive, scroll-safe scaffold and a scaling illustration size
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
// Illustration scales to the viewport width within a sensible
// range; the artwork is square (812:812).
final double imgSize =
(constraints.maxWidth * 0.58).clamp(190.0, 300.0);
return SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),build() returns a Scaffold with a plain white background, and SafeArea keeps content clear of the notch and home indicator. LayoutBuilder hands you the available constraints so imgSize can be computed as 58% of the viewport width, then .clamp(190.0, 300.0) locks that between 190 and 300px so the square (812:812) artwork never shrinks or blows up. SingleChildScrollView with ClampingScrollPhysics, wrapped around a ConstrainedBox(minHeight: maxHeight) and IntrinsicHeight, is the classic combo that fills the screen on tall phones but scrolls instead of overflowing on short ones. A symmetric 24px horizontal padding insets everything from the edges.
The title and the calendar illustration
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
const Spacer(flex: 2),
const Text(
'Nothing to do',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: _font,
fontWeight: FontWeight.w500,
fontSize: 21,
height: 1.2,
color: _ink,
),
),
const Spacer(flex: 1),
Image.asset(
_image,
width: imgSize,
height: imgSize,
fit: BoxFit.contain,
filterQuality: FilterQuality.high,
),
const Spacer(flex: 1),Everything sits in a center-aligned Column, and the vertical placement is done entirely with Spacer widgets carrying different flex weights — Spacer(flex: 2) at the top, then flex: 1 gaps around the artwork. The 'Nothing to do' title is 21px Inter at FontWeight.w500 with a 1.2 line-height in the _ink slate, centered. Below it, Image.asset draws the bundled calendar WebP at the computed imgSize for both width and height, with BoxFit.contain and FilterQuality.high so the artwork stays crisp when scaled.
Supporting copy and the bottom-pinned button
const Text(
'You have no events scheduled yet.\n'
'Tap the + button to create your first one.',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: _font,
fontWeight: FontWeight.w500,
fontSize: 16,
height: 1.37,
color: _body,
),
),
const Spacer(flex: 3),
const _AddButton(),
const SizedBox(height: 34),
],
),
),
),
),
);
},
),
),
);
}
}The two-line prompt uses adjacent string literals with a \n between them — 'You have no events scheduled yet.' and 'Tap the + button to create your first one.' — rendered at 16px Inter w500 with a 1.37 line-height in the softer _body slate. A Spacer(flex: 3) below it is heavier than the top spacer, which pushes the _AddButton down toward the bottom of the screen, and a final SizedBox(height: 34) leaves breathing room beneath it. The uneven flex weights are what create the 'content up top, action near the bottom' rhythm.
A tactile circular '+' add button
/// Circular "+" button with a pressed-scale animation and a soft brand-blue
/// shadow, matching the kit's floating add action.
class _AddButton extends StatefulWidget {
const _AddButton();
@override
State<_AddButton> createState() => _AddButtonState();
}
class _AddButtonState extends State<_AddButton> {
bool _pressed = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => setState(() => _pressed = true),
onTapUp: (_) => setState(() => _pressed = false),
onTapCancel: () => setState(() => _pressed = false),
onTap: () {},
child: AnimatedScale(
scale: _pressed ? 0.94 : 1,
duration: const Duration(milliseconds: 150),
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: NoEventsScreen._blue,
shape: BoxShape.circle,
boxShadow: <BoxShadow>[
BoxShadow(
color: NoEventsScreen._blue.withValues(alpha: 0.35),
blurRadius: 18,
offset: const Offset(0, 8),
),
],
),
child: const Icon(Icons.add, color: Colors.white, size: 28),
),
),
);
}
}The add button is its own StatefulWidget so it can react to touch. A single _pressed boolean is flipped in onTapDown, onTapUp, and onTapCancel, and AnimatedScale reads it to shrink the button to 0.94 for 150ms while held — that tiny squeeze is what makes it feel physically pressable. The button is a 64x64 Container with BoxShape.circle filled in the brand _blue, and a soft glow comes from a BoxShadow using _blue.withValues(alpha: 0.35), an 18px blur, and an 8px downward offset. A white Icons.add at size 28 sits in the center. The onTap is left empty ({}) for you to wire up your 'create event' flow.
Full code
The complete, ready-to-paste source. Free to use in your projects — one click copies it all.
import 'package:flutter/material.dart';
/// "No Events" empty-state screen from the Empty States UI Kit: a centred title
/// ("Nothing to do"), a hand-highlighting-a-calendar illustration, a short
/// supporting line, and a circular "+" action button pinned near the bottom to
/// create the first event.
///
/// Self-contained, pure Flutter. Bundles its design font (Inter) and the kit's
/// original illustration (converted to WebP). Fully responsive — flex spacers
/// centre the content on tall devices and a scroll fallback prevents overflow
/// on short ones. Renders standalone when pushed as a route.
class NoEventsScreen extends StatelessWidget {
const NoEventsScreen({super.key});
static const Color _ink = Color(0xFF1B3554);
static const Color _body = Color(0xFF344B67);
static const Color _blue = Color(0xFF4476FF);
static const String _font = 'Inter';
static const String _image =
'lib/screens/empty_states/no_events/images/no_events_calendar.webp';
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
// Illustration scales to the viewport width within a sensible
// range; the artwork is square (812:812).
final double imgSize =
(constraints.maxWidth * 0.58).clamp(190.0, 300.0);
return SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
const Spacer(flex: 2),
const Text(
'Nothing to do',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: _font,
fontWeight: FontWeight.w500,
fontSize: 21,
height: 1.2,
color: _ink,
),
),
const Spacer(flex: 1),
Image.asset(
_image,
width: imgSize,
height: imgSize,
fit: BoxFit.contain,
filterQuality: FilterQuality.high,
),
const Spacer(flex: 1),
const Text(
'You have no events scheduled yet.\n'
'Tap the + button to create your first one.',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: _font,
fontWeight: FontWeight.w500,
fontSize: 16,
height: 1.37,
color: _body,
),
),
const Spacer(flex: 3),
const _AddButton(),
const SizedBox(height: 34),
],
),
),
),
),
);
},
),
),
);
}
}
/// Circular "+" button with a pressed-scale animation and a soft brand-blue
/// shadow, matching the kit's floating add action.
class _AddButton extends StatefulWidget {
const _AddButton();
@override
State<_AddButton> createState() => _AddButtonState();
}
class _AddButtonState extends State<_AddButton> {
bool _pressed = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => setState(() => _pressed = true),
onTapUp: (_) => setState(() => _pressed = false),
onTapCancel: () => setState(() => _pressed = false),
onTap: () {},
child: AnimatedScale(
scale: _pressed ? 0.94 : 1,
duration: const Duration(milliseconds: 150),
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: NoEventsScreen._blue,
shape: BoxShape.circle,
boxShadow: <BoxShadow>[
BoxShadow(
color: NoEventsScreen._blue.withValues(alpha: 0.35),
blurRadius: 18,
offset: const Offset(0, 8),
),
],
),
child: const Icon(Icons.add, color: Colors.white, size: 28),
),
),
);
}
}
Plus bundled 2 binary assets (fonts/images). The CLI and MCP install those for you automatically.
Two faster ways to add it
Copy-paste works, but you can skip it entirely.
1. FlutterKit CLI
One command drops this screen — and its fonts — straight into your project.
$ flutterkit add no-events2. AI agent (MCP)
Connect FlutterKit's MCP server in Claude or Cursor and just ask your agent to install no-events — it fetches and writes the files for you.
FAQ
Is this Flutter empty-state screen free to use?
Yes. The full Dart source on this page is free to copy into your own projects, personal or commercial. Paste it directly, install it with the FlutterKit CLI (flutterkit add no-events), or have an AI agent add it for you via MCP.
Does it need any external packages?
No — it's pure Flutter, built entirely on the material library. The extras are the bundled Inter font and the calendar illustration (a WebP asset), both of which you register in pubspec.yaml as shown in step 2. The CLI and MCP copy the font and image files in for you automatically.
Which Flutter version does it target?
It uses modern Flutter APIs like Color.withValues() and super parameters, so it targets Flutter 3.22+ (Dart 3). On an older SDK, swap _blue.withValues(alpha: 0.35) for _blue.withOpacity(0.35) and it will compile.