Fliin
On this page

Blocks / dashboard

Stat card

Shows a key figure such as a balance in large type, with its change in a signed pill, a short note and an optional action to hide it.

16 of 16 variants pass
Stat card, positive state
Platform
Brightness
Text scale
Direction

Import

Stat card is part of Fliin Pro. With a license key, sign in once with fliin login, then add it in a Flutter app where fliin init has run:

fliin login
fliin add stat-card

Also installs: core, button, card, chip

Examples

Positive

The balance with its change this week. The “Hide” button masks the amount, and valueSemanticLabel says so to the screen reader.

Stat card, positive state
import 'package:your_app/ui/fliin/components/button/fl_button.dart';
import 'package:your_app/ui/fliin/components/stat_card/fl_stat_card.dart';
import 'package:flutter/widgets.dart';

class StatCardPositiveExample extends StatefulWidget {
  const StatCardPositiveExample({super.key});

  @override
  State<StatCardPositiveExample> createState() => _StatCardPositiveExampleState();
}

class _StatCardPositiveExampleState extends State<StatCardPositiveExample> {
  var _hidden = false;

  @override
  Widget build(BuildContext context) {
    return FlStatCard(
      label: 'Balance',
      value: _hidden ? '••••••' : '€2,340.50',
      valueSemanticLabel: _hidden ? 'Balance hidden' : null,
      change: _hidden ? null : const FlStatChange(label: '+€120.00', tone: FlStatTone.positive),
      caption: _hidden ? null : 'this week',
      action: FlButton(
        label: 'Hide',
        size: FlButtonSize.sm,
        variant: _hidden ? FlButtonVariant.secondary : FlButtonVariant.ghost,
        selected: _hidden,
        onPressed: () => setState(() => _hidden = !_hidden),
      ),
    );
  }
}

Hidden

The same card with the amount hidden from the start: the change and the caption go too.

Stat card, hidden state
import 'package:your_app/ui/fliin/components/button/fl_button.dart';
import 'package:your_app/ui/fliin/components/stat_card/fl_stat_card.dart';
import 'package:flutter/widgets.dart';

class StatCardHiddenExample extends StatefulWidget {
  const StatCardHiddenExample({super.key});

  @override
  State<StatCardHiddenExample> createState() => _StatCardHiddenExampleState();
}

class _StatCardHiddenExampleState extends State<StatCardHiddenExample> {
  var _hidden = true;

  @override
  Widget build(BuildContext context) {
    return FlStatCard(
      label: 'Balance',
      value: _hidden ? '••••••' : '€2,340.50',
      valueSemanticLabel: _hidden ? 'Balance hidden' : null,
      change: _hidden ? null : const FlStatChange(label: '+€120.00', tone: FlStatTone.positive),
      caption: _hidden ? null : 'this week',
      action: FlButton(
        label: 'Hide',
        size: FlButtonSize.sm,
        variant: _hidden ? FlButtonVariant.secondary : FlButtonVariant.ghost,
        selected: _hidden,
        onPressed: () => setState(() => _hidden = !_hidden),
      ),
    );
  }
}

Negative

A rise in spending is bad news: FlStatTone.negative colors the change, whatever its sign.

Stat card, negative state
import 'package:your_app/ui/fliin/components/stat_card/fl_stat_card.dart';
import 'package:flutter/widgets.dart';

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

  @override
  Widget build(BuildContext context) {
    return const FlStatCard(
      label: 'Spending',
      value: '€179.70',
      change: FlStatChange(label: '+€35.10', tone: FlStatTone.negative),
      caption: 'vs last week',
    );
  }
}

Neutral

A figure that did not move: without a tone, the change stays neutral.

Stat card, neutral state
import 'package:your_app/ui/fliin/components/stat_card/fl_stat_card.dart';
import 'package:flutter/widgets.dart';

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

  @override
  Widget build(BuildContext context) {
    return const FlStatCard(label: 'Subscriptions', value: '4', change: FlStatChange(label: '0'));
  }
}

Grid

Two figures side by side, as at the top of a dashboard: size: sm keeps each figure on its line in half the width, and an icon before the name says what it measures.

Stat card, grid state
import 'package:your_app/ui/fliin/components/stat_card/fl_stat_card.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:your_app/ui/fliin/icons/fl_icons.dart';
import 'package:flutter/widgets.dart';

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

  @override
  Widget build(BuildContext context) {
    final space = FlScope.of(context).space;
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      spacing: space.sm,
      children: [
        Expanded(
          child: FlStatCard(
            icon: FlIcon(FlIcons.shoppingCart),
            label: 'Orders',
            value: '128',
            change: const FlStatChange(label: '+12 %', tone: FlStatTone.positive),
            size: FlStatCardSize.sm,
          ),
        ),
        Expanded(
          child: FlStatCard(
            icon: FlIcon(FlIcons.person),
            label: 'Visitors',
            value: '3,204',
            change: const FlStatChange(label: '−4 %', tone: FlStatTone.negative),
            size: FlStatCardSize.sm,
          ),
        ),
      ],
    );
  }
}

Trend

The figure and its trend: a small area FlChart in the footer, without its time axis nor its tooltip, scaled to its values rather than to zero.

Stat card, trend state
import 'package:your_app/ui/fliin/components/chart/fl_chart.dart';
import 'package:your_app/ui/fliin/components/stat_card/fl_stat_card.dart';
import 'package:flutter/widgets.dart';

String _euros(double value) => '€${value.round()}';

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

  @override
  Widget build(BuildContext context) {
    return FlStatCard(
      label: 'Revenue',
      value: '€12,480',
      change: const FlStatChange(label: '+8.2 %', tone: FlStatTone.positive),
      caption: 'this month',
      footer: FlChart.line(
        points: const [
          FlChartPoint('W1', semanticLabel: 'Week 1'),
          FlChartPoint('W2', semanticLabel: 'Week 2'),
          FlChartPoint('W3', semanticLabel: 'Week 3'),
          FlChartPoint('W4', semanticLabel: 'Week 4'),
          FlChartPoint('W5', semanticLabel: 'Week 5'),
          FlChartPoint('W6', semanticLabel: 'Week 6'),
        ],
        series: const [
          FlChartSeries(label: 'Revenue', values: [9800, 10400, 10100, 11300, 11900, 12480]),
        ],
        formatValue: _euros,
        semanticLabel: 'Revenue by week',
        plotHeight: 48,
        showTimeAxis: false,
        tooltip: false,
        area: true,
        zeroBased: false,
      ),
    );
  }
}

Usage

The code the reference images render:

final example = ColoredBox(
  color: FlScope.of(context).colors.surface,
  child: SingleChildScrollView(
    padding: EdgeInsetsDirectional.all(FlScope.of(context).space.lg),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      spacing: FlScope.of(context).space.sm,
      children: [
        FlStatCard(
          label: 'Balance',
          value: '€2,340.50',
          change: FlStatChange(label: '+€120.00', tone: FlStatTone.positive),
          caption: 'this week',
          action: FlButton(
            label: 'Hide',
            size: FlButtonSize.sm,
            variant: FlButtonVariant.ghost,
            selected: false,
            onPressed: () {},
          ),
        ),
        FlStatCard(
          label: 'Balance',
          value: '••••••',
          valueSemanticLabel: 'Balance hidden',
          action: FlButton(
            label: 'Hide',
            size: FlButtonSize.sm,
            variant: FlButtonVariant.secondary,
            selected: true,
            onPressed: () {},
          ),
        ),
        FlStatCard(
          label: 'Spending',
          value: '€179.70',
          change: FlStatChange(label: '+€35.10', tone: FlStatTone.negative),
          caption: 'compared with last week',
        ),
        FlStatCard(
          label: 'Subscriptions',
          value: '4',
          change: FlStatChange(label: '0'),
        ),
      ],
    ),
  ),
);

API

FlStatTone: enum {positive, negative, neutral}
FlStatTone.positive: static final FlStatTone
FlStatTone.negative: static final FlStatTone
FlStatTone.neutral: static final FlStatTone
FlStatCardSize: enum {md, sm}
FlStatCardSize.md: static final FlStatCardSize
FlStatCardSize.sm: static final FlStatCardSize
FlStatChange: final class
FlStatChange.new: const new({required String label, FlStatTone tone, String? semanticLabel})
FlStatChange.label: get String
FlStatChange.tone: get FlStatTone
FlStatChange.semanticLabel: get String?
FlStatCard: class extends StatelessWidget
FlStatCard.new: const new({super.key, required String label, required String value, String? valueSemanticLabel, FlStatChange? change, String? caption, Widget? action, Widget? icon, Widget? footer, FlStatCardSize size})
FlStatCard.label: get String
FlStatCard.value: get String
FlStatCard.valueSemanticLabel: get String?
FlStatCard.change: get FlStatChange?
FlStatCard.caption: get String?
FlStatCard.action: get Widget?
FlStatCard.icon: get Widget?
FlStatCard.footer: get Widget?
FlStatCard.size: get FlStatCardSize
FlStatCard.build: Widget(BuildContext context)

Reference renders

The 16 images published with version 0.2.4, rendered by the shared harness on macOS with Flutter 3.47.7. fliin verify stat-card compares your copy to them.

iOS · Light
Stat card, iOS, Light, text 1.0×, Left to right
Text 1.0× · Left to right
Stat card, iOS, Light, text 1.0×, Right to left
Text 1.0× · Right to left
Stat card, iOS, Light, text 2.0×, Left to right
Text 2.0× · Left to right
Stat card, iOS, Light, text 2.0×, Right to left
Text 2.0× · Right to left
iOS · Dark
Stat card, iOS, Dark, text 1.0×, Left to right
Text 1.0× · Left to right
Stat card, iOS, Dark, text 1.0×, Right to left
Text 1.0× · Right to left
Stat card, iOS, Dark, text 2.0×, Left to right
Text 2.0× · Left to right
Stat card, iOS, Dark, text 2.0×, Right to left
Text 2.0× · Right to left
Android · Light
Stat card, Android, Light, text 1.0×, Left to right
Text 1.0× · Left to right
Stat card, Android, Light, text 1.0×, Right to left
Text 1.0× · Right to left
Stat card, Android, Light, text 2.0×, Left to right
Text 2.0× · Left to right
Stat card, Android, Light, text 2.0×, Right to left
Text 2.0× · Right to left
Android · Dark
Stat card, Android, Dark, text 1.0×, Left to right
Text 1.0× · Left to right
Stat card, Android, Dark, text 1.0×, Right to left
Text 1.0× · Right to left
Stat card, Android, Dark, text 2.0×, Left to right
Text 2.0× · Left to right
Stat card, Android, Dark, text 2.0×, Right to left
Text 2.0× · Right to left

Use it to

  • show an account balance in large type with its change
  • show a key figure going up or down with a signed pill
  • hide a sensitive amount behind dots
  • present this period's spending against the last one
  • show several headline figures at the top of a dashboard
Not for
  • progress toward a goal: use a progress indicator (progress)
  • change over time: use a chart (chart)
States
  • Positive
  • Hidden
  • Negative
  • Neutral

Arrows to move, Enter to open, Escape to close.