card_deck_view

English | 简体中文 | API reference (中文) | 0.0.2 release notes (中文)

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 by default; optionally use left-next/right-previous navigation.
  • Choose off-screen exits or reversible cycling behind the stack.
  • 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 version 0.0.2 is published, install from pub.dev with flutter pub add card_deck_view, or add:

dependencies:
  flutter:
    sdk: flutter
  card_deck_view: ^0.0.2

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
swipeBehavior CardDeckSwipeBehavior.advanceBothDirections Both directions advance, or left next / right previous
transition CardDeckTransition.slideOut Off-screen exit or cycleToBack
onSwipe null (int, DeckSwipeDirection) → void; original index and physical swipe 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. onSwipe reports the original index and physical swipe direction after the target index and controller have been updated, before onChanged. Each successful swipe emits each callback once; initialization, boundary returns and a single looping item emit neither. Controller previous, jump and reset do not emit onSwipe.

Reversible stack navigation

CardDeckView<TraceModel>(
  items: traces,
  itemKeyBuilder: (trace) => trace.id,
  visibleCount: 3,
  loop: true,
  swipeBehavior: CardDeckSwipeBehavior.leftNextRightPrevious,
  transition: CardDeckTransition.cycleToBack,
  style: const CardDeckStyle(shadows: []),
  onChanged: (index) {
    if (index < traces.length) updateCoverAndColors(traces[index]);
  },
  itemBuilder: (context, trace, index) => GestureDetector(
    onTap: () => openTrace(trace),
    onLongPress: () => previewTrace(trace),
    child: buildTraceCard(trace),
  ),
)

This is an application integration sketch: supply your model and handlers, and place the deck inside a finite SizedBox or constrained AspectRatio. Use shadows: [] when the card already draws its own shadow. Drive cover/color updates from onChanged, not a compensating previous() call in onSwipe.

cycleToBack sends the held top card around the swipe side into the rear. With left-next/right-previous navigation, a right swipe promotes the previous item inside the stack; it does not pull that item in from the side. The explicit controller previous() command retains the reverse entry animation. The source list is never reordered. Both options are independent, so cycling can also use both-directions-advance navigation. With leftNextRightPrevious in non-looping mode, a right swipe at the first item springs back; after exhaustion use previous() to restore the last item. Reduced motion completes immediately. Changing data, direction behavior or transition cancels pending motion.

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() Always advance, using the selected transition
swipeRight() Advance or return to the previous item according to swipeBehavior
previous() Always go back: enter from the left with slideOut, reverse around the right with cycleToBack; 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 both transition styles. Only slideOut uses the viewport width to determine exit distance.

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.
  • Dragging keeps the current top card under the pointer in both directions. Only a committed backward transition reuses the forward buffer for the previous item. Only the idle top card accepts child gestures and semantics; card interaction is disabled during navigation.
  • 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.