card_deck_view

English | 简体中文

A Flutter card deck with continuous stack animation, horizontal swiping and spring return. Build cards from any widget, control navigation programmatically, and display large lists through a small mounted window.

Card deck drag, spring return and swipe demo

Desktop preview · Mobile preview · Example guide

Features

  • Rear cards move forward as you drag; subtle rotation gives the stack depth.
  • Distance and release velocity determine whether a card advances or springs back.
  • Both swipe directions advance; controller navigation also supports previous, jump and reset.
  • Configure visible layers, spacing, rotation, shadows and physics.
  • Touch, mouse, trackpad, horizontal wheel, keyboard and accessibility actions.
  • Pure Flutter, with no swiper or native plugin dependency.

Getting started

Requires Flutter 3.44.0+ and Dart 3.12.2+ (<4.0.0).

After the first release is published, install from pub.dev with flutter pub add card_deck_view, or add:

dependencies:
  flutter:
    sdk: flutter
  card_deck_view: ^0.0.1

Run flutter pub get. To develop from this checkout before publication, use a local path dependency; the bundled example already uses path: ../. Only import the public library:

import 'package:card_deck_view/card_deck_view.dart';

Minimal example

import 'package:card_deck_view/card_deck_view.dart';
import 'package:flutter/material.dart';

void main() => runApp(const MaterialApp(home: DeckExample()));

class DeckExample extends StatelessWidget {
  const DeckExample({super.key});

  @override
  Widget build(BuildContext context) => Scaffold(
    body: Center(
      child: SizedBox(
        width: 300,
        height: 400,
        child: CardDeckView<String>(
          items: const ['One', 'Two', 'Three', 'Four'],
          itemBuilder: (context, item, index) => ColoredBox(
            color: Colors.white,
            child: Center(child: Text(item)),
          ),
        ),
      ),
    ),
  );
}

Give the deck finite width and height, using a SizedBox, AspectRatio within bounded constraints, or another constrained parent. Leave space around it for rear cards, shadows and swipes. Card content and aspect ratio belong to your application.

Widget API

Property Default Purpose
items Required List<T> containing the card data
itemBuilder Required (BuildContext, T, int) → Widget; index refers to the source list
visibleCount 3 Positive number of visible layers; mounts at most this number plus one buffer
loop true Wrap to the beginning after the last card
initialIndex 0 Non-negative start index, clamped to available items
controller null Optional CardDeckController
itemKeyBuilder null (T) → Object returning a unique stable identity; index identity otherwise
style CardDeckStyle() Stack appearance
physics CardDeckPhysics() Swipe and return behavior
onSwipe null (int, DeckSwipeDirection) → void; removed index and physical exit direction
onChanged null (int) → void; current index after navigation or data reconciliation
onCompleted null Called once each time a non-looping deck becomes exhausted
emptyBuilder null Empty/exhausted state; a blank widget by default

DeckSwipeDirection contains left and right. Both advance to the next item. onSwipe fires after an exit animation completes, before onChanged; initialization does not emit onChanged. Previous, jump and reset do not emit onSwipe.

Controller

Create the controller in your widget's State, pass it to CardDeckView(controller: controller, ...), and dispose it with that State:

final controller = CardDeckController();

@override
void dispose() {
  controller.dispose();
  super.dispose();
}
Method Behavior
next() / swipeLeft() Exit left and advance
swipeRight() Exit right and advance
previous() Bring the previous card back from the left; wrap only when looping
moveTo(index) Cancel motion and immediately position at a valid index
reset() Cancel motion and return to initialIndex, clamped to current data

Read currentIndex, isBusy and isAttached. The controller extends ChangeNotifier, so ListenableBuilder can observe changes. isBusy includes dragging and animations. New navigation commands are ignored during animations; users can catch a returning card by dragging again. Jump and reset cancel motion immediately.

Commands while detached do nothing. With a deck attached, an invalid moveTo index throws RangeError. One controller can bind to only one deck; a disposed controller cannot be reused. Binding and data updates synchronize values without notifying listeners during build; use onChanged for index changes caused by data updates.

Style and physics

Pass these objects through the widget's style and physics properties:

const style = CardDeckStyle(
  spacing: 12,
  scaleStep: 0.04,
  rotationStep: -0.015,
  dragRotation: 0.08,
  maxRotation: 0.08,
  borderRadius: 24,
);

const physics = CardDeckPhysics(
  swipeThreshold: 0.28,
  velocityThreshold: 700,
  spring: SpringDescription(mass: 1, stiffness: 300, damping: 30),
  swipeDuration: Duration(milliseconds: 280),
  swipeCurve: Curves.easeOutCubic,
);

These are the defaults. Import package:flutter/material.dart alongside the package for SpringDescription and Curves.

  • spacing is the vertical distance between layers; scaleStep is the scale reduction per layer. Deep stacks clamp scale to a positive minimum.
  • Angles use radians. rotationStep affects rear layers; dragRotation maps horizontal displacement to top-card rotation; maxRotation caps it.
  • shadows accepts a list of BoxShadow; use const [] for none. The default is a subtle shadow.
  • swipeThreshold is a fraction of card width, in (0, 1]. velocityThreshold uses logical pixels per second. Fast releases use velocity direction; slower releases use displacement.
  • The spring drives return motion. Duration and curve drive exits and previous-card entry. Exit distance accounts for the viewport width.

Lists, empty states and images

  • The deck mounts at most visibleCount + 1 cards and never repeats an item inside that window. Animation ticks update transforms without rebuilding card content.
  • Supply a new list when data changes. With itemKeyBuilder, the active item follows reordering; if removed, the index is clamped. Data changes cancel motion. Stable identities must be unique.
  • Cards remaining in the window retain their State; off-window cards are disposed. Store persistent business state outside individual card widgets.
  • An empty list shows emptyBuilder. A single looping item can be dragged and returned but does not advance.
  • With loop: false, removing the last item enters the empty state. currentIndex and onChanged then equal items.length—check the range before indexing your list. previous() restores the last card; reset() restores the starting position.

Images are supplied through itemBuilder; the package does not download or cache them. The default example uses twelve local transparent animal illustrations, so it works offline. Its custom AnimalCard combines an image, handwritten text and a reset button. You can supply any Widget through itemBuilder. The optional PhotoCard recipe demonstrates network loading, errors, retry and decode sizing without changing the global image cache.

Input and accessibility

Horizontal touch/mouse drags and trackpad pans move the deck. Vertical gestures remain available to a parent scroll view. Horizontal wheel events accumulate until 120 ms of inactivity before release; vertical wheel events are ignored.

Focus the deck with Tab or a drag, then use left/right keys to exit in that direction. Accessibility exposes the current card number and increase/decrease actions; rear card semantics are excluded. System reduced-motion settings skip automatic travel and rebound while retaining direct drag feedback.

Examples

The surrounding interface uses monochrome text navigation and lightweight controls. The animal cards and their motion stay independent of the application chrome.

cd example
flutter pub get
flutter run -d chrome
# or: flutter run -d windows
Page What it demonstrates
Animal cards 12 transparent illustrations, handwritten captions and navigation
Playground Compact settings panel; collapsible on narrow screens
1,000 cards A large list reusing 12 local illustrations, with a live mounted-card count

Playground preview · 1,000-card preview

The shared toolbar provides Previous and Next. The More actions menu contains Reset, Jump and About the artwork. Each card also has a reset button. White rounded cards, soft shadows and handwritten captions are built entirely in the example. See artwork and font provenance.

Platforms and development

The example includes Android, iOS, Web, Windows, macOS and Linux runners. Windows/Web builds and Android debug APK compilation have been verified; Web interaction and Windows startup have been smoke-tested. Android device runtime and iOS/macOS/Linux builds require additional verification on the corresponding devices/hosts. See the example guide for details and manual checks.

flutter pub get
flutter analyze
flutter test
cd example
flutter pub get
flutter analyze
flutter test

Implementation lives in lib/src/; consumers should use lib/card_deck_view.dart. The package has no CI configuration or automatic publication workflow.

License

Package source code is available under the MIT License. Example illustrations are not relicensed under MIT; see artwork and font notices.

Libraries

card_deck_view
Physics-driven, interactive card stacks for Flutter.