Stat Card
Dashboard metric cards in 4 styles — Simple, Icon, Trend, and Progress
View component →One enum-driven Flutter card widget covering the four layouts you reach for most: a basic content card, a card with an action button, an image-header card with a badge, and a horizontal image-beside-text card. Built only from Flutter SDK primitives on the house design system — hairline borders, a 12px radius and a lavender accent — so every card stays visually consistent and themeable from a single set of tokens.
What's Included
Use Cases
PRO TIP
Drive layout variants with an enum on one widget instead of copy-pasting four near-identical card classes. A single CardKind switch keeps the border, radius and color tokens defined once, so restyling every card in the app is a one-line edit — and wrapping the whole card in an InkWell with a matching borderRadius keeps the ripple clipped to the rounded corners.
Copy this into your Flutter project. No external packages required.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
backgroundColor: const Color(0xFFFAFAFB),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
// Image-header card with a badge + action
AppCard(
kind: CardKind.image,
title: 'Streamlining your design process',
body: 'A faster way to ship polished Flutter UI.',
imageUrl:
'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?w=600&q=80',
badge: 'Trending',
actionLabel: 'Read more',
onAction: () {},
),
const SizedBox(height: 16),
// Basic content card (whole card tappable)
AppCard(
kind: CardKind.basic,
title: 'Noteworthy tech acquisitions 2025',
body:
'The biggest technology acquisitions of 2025 so far, in '
'reverse chronological order.',
onTap: () {},
),
const SizedBox(height: 16),
// Card with an action button
AppCard(
kind: CardKind.button,
title: 'Get started with Flutter',
body:
'Build natively compiled apps from a single codebase '
'with copy-paste widgets.',
actionLabel: 'Read more',
onAction: () {},
),
const SizedBox(height: 16),
// Horizontal card — image beside text
AppCard(
kind: CardKind.horizontal,
title: 'Collaborate seamlessly',
body: 'Bring developers and IT operations onto one workflow.',
imageUrl:
'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=400&q=80',
onTap: () {},
),
],
),
),
),
),
);
}
}
enum CardKind { basic, button, image, horizontal }
// ── House tokens (Linear-light) ──
const _accent = Color(0xFF5E6AD2);
const _accentTint = Color(0xFFEEF0FB);
const _accentBorder = Color(0xFFD5D9F4);
const _surface = Color(0xFFFFFFFF);
const _hairline = Color(0xFFE6E8EB);
const _ink = Color(0xFF1C1E26);
const _inkMuted = Color(0xFF51555E);
/// One card widget — the layout is chosen by [kind]. Pass an [imageUrl] for the
/// image/horizontal kinds, a [badge] and [actionLabel] when you need them.
class AppCard extends StatelessWidget {
final CardKind kind;
final String title;
final String body;
final String? imageUrl;
final String? badge;
final String? actionLabel;
final VoidCallback? onAction;
final VoidCallback? onTap;
const AppCard({
super.key,
required this.title,
required this.body,
this.kind = CardKind.basic,
this.imageUrl,
this.badge,
this.actionLabel,
this.onAction,
this.onTap,
});
@override
Widget build(BuildContext context) {
return Material(
color: _surface,
borderRadius: BorderRadius.circular(12),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: _hairline),
borderRadius: BorderRadius.circular(12),
),
clipBehavior: Clip.antiAlias,
child: kind == CardKind.horizontal ? _horizontal() : _vertical(),
),
),
);
}
Widget _vertical() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (kind == CardKind.image && imageUrl != null)
Image.network(
imageUrl!,
height: 150,
width: double.infinity,
fit: BoxFit.cover,
),
Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (badge != null) ...[
_Badge(label: badge!),
const SizedBox(height: 12),
],
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: _ink,
height: 1.25,
),
),
const SizedBox(height: 8),
Text(
body,
style: const TextStyle(
fontSize: 14,
color: _inkMuted,
height: 1.5,
),
),
if (actionLabel != null) ...[
const SizedBox(height: 18),
_ActionButton(label: actionLabel!, onPressed: onAction),
],
],
),
),
],
);
}
Widget _horizontal() {
return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (imageUrl != null)
Image.network(imageUrl!, width: 104, fit: BoxFit.cover),
Expanded(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: _ink,
),
),
const SizedBox(height: 6),
Text(
body,
style: const TextStyle(
fontSize: 13,
color: _inkMuted,
height: 1.4,
),
),
],
),
),
),
],
),
);
}
}
class _Badge extends StatelessWidget {
final String label;
const _Badge({required this.label});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _accentTint,
border: Border.all(color: _accentBorder),
borderRadius: BorderRadius.circular(6),
),
child: Text(
label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: _accent,
),
),
);
}
}
class _ActionButton extends StatelessWidget {
final String label;
final VoidCallback? onPressed;
const _ActionButton({required this.label, this.onPressed});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: _accent,
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 11),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
),
const SizedBox(width: 6),
const Icon(Icons.arrow_forward, size: 16),
],
),
);
}
}Dashboard metric cards in 4 styles — Simple, Icon, Trend, and Progress
View component →Card-style accordion with animated chevron and single-open behavior
View component →One enum-driven badge — count, dot, label & status kinds on the house theme
View component →