CARDS

Cards

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.

cardlayoutcontainer
Preview4 variants

Basic

Tech acquisitions 2025

The biggest acquisitions of the year, in order.

With button

Get started with Flutter

Build apps from a single codebase.

Read more

What's Included

  • Basic content card with title and body on a hairline-bordered surface
  • Card with a solid lavender action button and trailing arrow
  • Image-header card with a rounded cover photo and category badge
  • Horizontal card — cover image beside a title and description
  • One AppCard widget driven by a CardKind enum, zero duplication
  • House design tokens — 12px radius, #E6E8EB hairline, #5E6AD2 accent

Use Cases

  • Article, blog and news feed cards
  • Product and category tiles in a shop grid
  • Dashboard call-to-action and summary panels
  • List rows pairing a thumbnail with a title and description

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.

Complete Dart Code

Copy this into your Flutter project. No external packages required.

dart
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),
        ],
      ),
    );
  }
}

How to use this widget

  1. 1Copy the complete Dart code above
  2. 2Create a new file — app_card.dart in your Flutter project
  3. 3Use AppCard(title: '...', body: '...') — switch the layout with the kind parameter (basic, button, image, horizontal) and pass imageUrl, badge or actionLabel as needed

Related components

Data Display

Stat Card

Dashboard metric cards in 4 styles — Simple, Icon, Trend, and Progress

View component →
UI Elements (Misc)

Accordion (Card)

Card-style accordion with animated chevron and single-open behavior

View component →
Avatars & Chips

Badge

One enum-driven badge — count, dot, label & status kinds on the house theme

View component →