İçeriğe geç / Skip to content / Zum Inhalt

Flutter: Card Widget and Design Usage

Ahmet Balaman

Last updated:

6 min read

FlutterCardMaterial 3InkWellWidgetUI
Flutter: Card Widget and Design Usage

Card is the Material Design widget that gathers related information on a single surface with a light shadow and rounded corners. We use it to lift content that is "a whole on its own" off the background: a product, a news item, a user summary. It has no size of its own; it takes the shape of whatever child you put in it, which is why you almost always see it together with Padding, Column or ListTile.

Live Demo

You can try this widget in the interactive example below:

💡 If the example above doesn't load, click DartPad to run it in a new tab.

Basic Usage

Card(
  child: Padding(
    padding: EdgeInsets.all(16),
    child: Text('Card Content'),
  ),
)

A Card gives its content no inner padding by itself; without a Padding the text sticks to the edge of the card. On the outside, however, it leaves a small default margin on every side, so cards stacked in a column don't touch each other.

Important Properties

Property Description
elevation Shadow height
shadowColor Shadow color
shape Card shape (corner radius, border)
color Background color
margin Outer spacing
clipBehavior Whether the content is clipped to the card's shape

Material 3 Card Types

Material 3 defines three kinds of card, and Flutter has a ready-made constructor for each:

// Elevated: light shadow, the default card
Card(child: ...)

// Filled: no shadow, stands out through its fill colour
Card.filled(child: ...)

// Outlined: no shadow, thin border
Card.outlined(child: ...)

Which one to pick depends on the background and on how important the card is. The elevated card draws the most attention; in busy lists where many cards follow each other, Card.outlined or Card.filled looks calmer. Because they take their colors from the theme's ColorScheme, they look right in both light and dark themes without extra work.

Customized Card

When the built-in types aren't enough, shape, elevation and color allow fine tuning:

Card(
  elevation: 4,
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(16),
  ),
  color: Theme.of(context).colorScheme.surfaceContainerHigh,
  margin: const EdgeInsets.all(8),
  child: ...
)

To define the border yourself, use side inside shape. This is the hand-written form of Card.outlined and is useful when the border color or thickness has to change:

Card(
  elevation: 0,
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(12),
    side: BorderSide(color: Theme.of(context).colorScheme.outlineVariant),
  ),
  child: ...
)

Card with an Image and clipBehavior

A Card has rounded corners, but its default clipBehavior is Clip.none; in other words, the card does not clip its child. Put a full-width image at the top and the image's sharp corners poke out of the card's rounded corners. Clip.antiAlias fixes that:

Card(
  clipBehavior: Clip.antiAlias, // Clip the image corners to the card's shape
  child: Column(
    children: [
      Image.network(
        'https://example.com/image.jpg',
        height: 150,
        width: double.infinity,
        fit: BoxFit.cover,
      ),
      const Padding(
        padding: EdgeInsets.all(16),
        child: Text('Card Title'),
      ),
    ],
  ),
)

Clipping isn't free, which is why Flutter keeps it off by default. Turning it on for the cards that need it is enough. How to add images to a project is covered in the assets and Image.asset post.

Using with ListTile

For single-row information cards, ListTile gives the tidiest result with the least code:

Card(
  child: ListTile(
    leading: const CircleAvatar(child: Icon(Icons.person)),
    title: const Text('Username'),
    subtitle: const Text('Subtitle'),
    trailing: const Icon(Icons.chevron_right),
    onTap: () {},
  ),
)

Tappable Card with InkWell

Card has no onTap parameter of its own. To make it tappable we put an InkWell inside it; since a Card is a Material surface, the ripple shows up right on the card:

Card(
  clipBehavior: Clip.antiAlias,
  child: InkWell(
    onTap: () {
      print('Card tapped');
    },
    child: const Padding(
      padding: EdgeInsets.all(16),
      child: Text('Tappable Card'),
    ),
  ),
)

clipBehavior matters here too: without it the ripple spreads as a rectangle and spills over the card's rounded corners. I compare InkWell and GestureDetector in the GestureDetector post.

When to Use It and When Not To

Card is the right choice when a screen holds several independent pieces of content that need to be told apart: a product grid, a news feed, summary boxes on a dashboard.

In these situations something else works better:

  • Simple, uniform list rows: on screens like settings or a contact list, wrapping every row in a card makes the UI heavy. ListTile with a Divider in between is enough.
  • Gradients, custom shadows or a non-Material look: Card's color parameter takes a single color. For a gradient you need a Container with a BoxDecoration.
  • An area covering the whole page or a large section of it: a card exists to separate content from the background; if everything is a card, nothing stands out. A card inside a card is a pattern to avoid for the same reason.

Common Mistakes

1. The image corners stick out of the card

Symptom: the card's bottom corners are rounded, the top corners where the image sits are sharp. As explained above, the cause is the default Clip.none; adding clipBehavior: Clip.antiAlias to the Card solves it. There is no need to wrap the image in a separate ClipRRect.

2. Wrapping the Card in a GestureDetector from the outside

Symptom: the card reacts to taps but shows no visual response, so the user can't tell whether the touch was registered. Putting an InkWell on the outside is no fix either; the ripple then ends up behind the card. The right order is Card > InkWell > content.

3. Hard-coding the color

Symptom: a card that looks fine in the light theme stays bright white in the dark theme, and the light-colored text inside becomes unreadable. The cause is a fixed value such as color: Colors.white. Leaving the color out, or picking it from Theme.of(context).colorScheme, keeps the card correct in both themes. Color roles and setting up light and dark themes are covered in the Theme and ThemeData post.

4. A card with unbounded width inside a Row

Symptom: placing a card that contains a ListTile inside a Row produces a layout error about infinite width. A Row puts no width limit on its children, while a ListTile wants all the width it can find. Wrap the card in Expanded or give it a width with SizedBox. The same applies to horizontally scrolling lists.

Mini Scenario: A Product Card

Think of the product grid of a shopping app. Each card shows an image, a title of up to two lines and a price; tapping the card opens the product detail. Since we will reuse this structure again and again, we extract it into its own widget:

class ProductCard extends StatelessWidget {
  const ProductCard({
    super.key,
    required this.title,
    required this.price,
    required this.imageUrl,
    required this.onTap,
  });

  final String title;
  final String price;
  final String imageUrl;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;

    return Card(
      clipBehavior: Clip.antiAlias,
      child: InkWell(
        onTap: onTap,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            AspectRatio(
              aspectRatio: 16 / 9,
              child: Image.network(imageUrl, fit: BoxFit.cover),
            ),
            Padding(
              padding: const EdgeInsets.all(12),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    title,
                    style: textTheme.titleMedium,
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                  ),
                  const SizedBox(height: 4),
                  Text(price, style: textTheme.bodyMedium),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

A few deliberate decisions are in there. Instead of a fixed image height we used AspectRatio; the image keeps its proportions whether the card is narrow or wide. The title is limited with maxLines and ellipsis, because a long product name would otherwise break the height of the cards in the grid. Text styles come from the theme rather than being written by hand. One more detail: because the ripple is painted on the Material surface, it isn't visible on top of the image, only over the text area. If it should appear over the image as well, Ink.image can replace Image.network.

Using this card inside a GridView completes the product grid. The reasoning behind extracting repeated UI pieces into their own classes is explained in detail in the creating our own widgets post.

Frequently Asked Questions

What is the difference between Card and Container?

Card is a ready-made surface with Material Design shadow, corner radius and theme colors, and it provides the Material ancestor that InkWell needs. Container is a general-purpose box; choose it when you need a gradient or a fully custom decoration.

How do I make a Card tappable?

Card has no onTap parameter. Wrap the content in an InkWell and give the Card clipBehavior: Clip.antiAlias so the ripple follows the card's corners. If the content is a single row, ListTile's own onTap parameter works too.

When should I use Card.filled and Card.outlined?

Both are shadowless Material 3 card types. On dense screens with many cards, or where a subtle separation from the background is enough, they look calmer than the default elevated card.

Why aren't the corners of the image inside my Card rounded?

By default a Card does not clip its child, because clipBehavior is Clip.none. Once you add clipBehavior: Clip.antiAlias to the Card, the image is clipped to the card's shape.

Comments