Blogs
  • Design System
  • Flutter
  • Payments

Pay DS: payment components for Flutter, built for UPI

Payment successful

₹450

Paid to Priya Sharma

To
priya.sharma@upi
From
Northwind Bank ••4242
UPI ref
4821 0937 5512
Done
PdsPaymentStatusSuccess state

A payment is the one tap you can’t take back

Most UI forgives mistakes. You can undo a deleted email or edit a sent message. A UPI payment is different: once it leaves, it’s gone. That makes the payment screen the place where an interface has to be fastest, clearest and most trustworthy, all at once.

Pay DS is our answer to that. It is a Flutter package of design tokens, a Material 3 theme, payment components and GPU shader effects, built for Indian (UPI) payments. In its own words: payment components for Flutter, built on Material 3 and set in Plus Jakarta Sans.

The workflow is three steps: import one library, apply the theme, compose components. Because it is Flutter, the same components run on Android and iOS from one Dart codebase.

The catalogue lives in a Widgetbook (think Storybook, for Flutter). It lists 33 components and 98 use-cases, including docs and foundation pages, and 20 widgets with their own API pages. You can switch dark mode, change text size and even slow time down while you explore.

Why payments need their own design system

A generic component library gets you buttons and text fields. It doesn’t know that ₹1,23,456 is grouped differently from $123,456, that a pay button must never freeze, or that a celebration should only play when money actually moved. Pay DS is built on five pillars.

  1. 01Token-driven
  2. 02Indian payments
  3. 03Motion
  4. 04GPU shadersBeta
  5. 05Accessible
The five pillars · select one to read its row

PillarWhat it means in practice
Token-drivenColour, type, spacing, radius, motion and springs come from one token layer on Material 3. A rebrand is a seed change.
Built for Indian paymentsLakh/crore grouping as you type, UPI limits, paise-precise amounts, tokenised cards and UPI receipts.
Motion with a reasonFast ease-out for responses, springs for gestures, celebrations only where they’re rare. Exits are quicker than entrances.
Custom GPU shadersHand-written GLSL: warp tunnel, refractive shockwave, aurora, holographic foil and burn dissolve. They degrade to no effect and turn off under reduced motion.
Accessible by defaultSemantic labels, live regions, a non-gesture path for every gesture, text scaling, and reduced motion that keeps fades but removes movement.

Components are tagged Stable. The five shader effects are tagged Beta.

Foundations: one token layer

Every colour, space, corner and duration in Pay DS is a named token on top of Material 3. Change the seed colour and the whole system re-themes. Payment status colours ride along in the theme as a Flutter ThemeExtension, so context.pdsStatus.success works anywhere.

Primary

Secondary & tertiary

Surfaces

Payment status

Spacing PdsSpace

  • xxs2
  • xs4
  • sm8
  • md12
  • lg16
  • xl24
  • xxl32
  • xxxl48

Radius PdsRadius

  • sm 8
  • md 12
  • lg 16
  • card 20
  • xl 28
Token sheetSelect a swatch to copy its hex · drag the seed

Colour (light theme)

GroupTokens
Primaryprimary #3B43D9 · primaryContainer #DFE0FF
Secondary & tertiarysecondaryContainer #CDE5FF · tertiary #A62C7A · tertiaryContainer #FFD8EC
Surfacessurface #F9F9FF · surfaceContainerLow #F2F3FC · surfaceContainerHigh #E5E6F2 · surfaceContainerHighest #DFE0EC
Payment statussuccess #188038 · successContainer #C4EED0 · pendingContainer #FFE8A3 · error #BA1A1A · errorContainer #FFDAD6

Spacing and radius

Spacing sits on a 4dp grid (PdsSpace): xxs 2 · xs 4 · sm 8 · md 12 · lg 16 · xl 24 · xxl 32 · xxxl 48.

Corners (PdsRadius): sm 8 · md 12 · lg 16 · card 20 · xl 28.

The type scale uses Material 3 roles, from displayLarge down to labelSmall, all set in Plus Jakarta Sans.

Motion with a reason

Motion has one rule, written on the Motion page: responses stay under 300ms and enter on a strong ease-out. Exits are faster than entrances. Springs handle anything a finger can interrupt. Celebration is reserved for rare moments. Keyboard input never animates position. Reduced motion keeps fades and drops movement.

TokenDuration
PdsMotion.press120ms
PdsMotion.short180ms
PdsMotion.medium260ms
PdsMotion.long420ms
PdsMotion.extraLong700ms

Curves are chosen by job: easeOut for entering and responding to input, easeInOut for moving between two on-screen positions, drawer for stacks and sheets, standard for cross-fades, and linear only for continuous motion like progress.

Two springs cover gestures. PdsMotion.snappy uses stiffness 520 and damping ratio 0.82. PdsMotion.playful uses stiffness 300 and damping ratio 0.58. A lower damping ratio means more bounce, so playful overshoots more than snappy.

Durations

  • PdsMotion.press120ms
  • PdsMotion.short180ms
  • PdsMotion.medium260ms
  • PdsMotion.long420ms
  • PdsMotion.extraLong700ms

Springs

  • PdsMotion.snappyk 520 · ζ 0.82 · overshoot ≈ 1.1%
  • PdsMotion.playfulk 300 · ζ 0.58 · overshoot ≈ 10.7%
Motion tokens on easeOutSprings simulated from stiffness and damping

Built for Indian payments

Pay DS stores every amount as a whole number of paise: 45000 means ₹450. Floating-point numbers can’t represent values like 0.1 exactly, so money code commonly uses whole minor units. Pay DS follows that best practice everywhere, so there are no rounding errors between what you type and what you send.

PdsAmountEntry is the full amount screen for a UPI payment: payee header, amount readout, validation, an optional note, keypad and confirm action. It handles the details Indian users notice:

  • Lakh/crore grouping as you type. ₹1,23,456.50, not ₹123,456.50. Digits roll in one by one and commas slide to their new positions.
  • Limits explained in words. The UPI maximum and the ₹1 minimum each come with a plain-language message and an icon. Rejected input shakes; crossing the limit gives a heavy haptic.
  • Hardware keys. Type digits directly, and press Enter to pay.
  • Screen readers. The amount is a live region read as currency (“Amount ₹450”), and the readout scales to fit at 200% text.
Paying Priya Sharmapriya.sharma@upi

UPI allows up to ₹1,00,000 in one payment.

Try it

  1. Type ₹1,23,456.50. Grouping follows lakh and crore.
  2. Push past the limit, or add a third decimal.
  3. Hold ⌫ or press Esc to clear.
  4. Press Enter or the pay button.

Focus the phone to type with your keyboard.

Screen reader hearsAmount ₹1,23,456.50

PdsAmountEntryPdsKeypadLive demo

The keypad underneath, PdsKeypad, fires on press rather than release, with no delay even inside scroll views. Hold delete and a red fill sweeps across to clear the amount. An “Add a note” pill (PdsNoteField) morphs into an inline field and collapses back, keeping the screen calm.

The checkout journey, end to end

The Checkout flow pattern in the catalogue strings the components into one journey:

  1. Amount entry with the keypad and live validation.
  2. Confirm with a pay button or a slide.
  3. Processing, with hyperspace streaks streaming from the spinner.
  4. Result: success with a shockwave and aurora, or failure with a plain reason and Try again.

In the Widgetbook you can make the first attempt fail (the retry then succeeds), set the processing time, and choose “Confirm with: button / slide”.

  1. 01 Amount entry

  2. 02 Confirm

  3. 03 Processing

  4. 04 Result

Checkout flow patternConfirm with: slide

A pay button that never freezes

PdsPayButton is a full-width pill with a lit gradient. When the payment starts, it contracts into a circle with a spinner, so the press visibly becomes progress instead of the button freezing. It presses to 0.96 scale with a spring release, uses tabular figures so the amount never jitters, and stays disabled until the amount is valid. Screen readers hear “Processing payment” while it loads.

Slide to pay: a tap never pays

Money movement is irreversible, so PdsSlideToPay asks for a deliberate gesture. A plain tap never pays; it teaches the gesture.

  • The thumb follows your finger 1:1 with a trail.
  • It commits past 82% of the track, or on a fast flick past 40%. Otherwise it springs back.
  • A haptic tick plays every fifth of the track, and a heavy haptic on commit.
  • Press and hold is the alternative: a ring fills, and the payment goes through. Screen readers get one button, “<label>, double tap to pay”, and Enter or Space works on a keyboard.
PdsSlideToPayLive demo · demo thresholds

The impact beat

The success screen, PdsPaymentStatus, is the showpiece. Several GPU effects play in sequence, and they are all timed to one callback.

While the payment processes, hyperspace streaks stream from the spinner. The spinner is PdsStatusMark, one continuous object that goes from arc to closed ring to filled disc to a drawn check or cross. The moment the ring floods into a disc, it fires onImpact. On that beat the streaks jump to light speed and stop dead, a refractive shockwave bends the whole screen, and an aurora blooms. Then the content staggers in.

Payment successful

₹450

Paid to Priya Sharma · UPI ref 4821 0937 5512

3.80s Content in

PdsPaymentStatusPdsStatusMark.onImpactReplay · outcome · slow time

Several effects timed to one callback: that is the whole trick. PdsStatusMark exposes onImpact as the beat other effects should land on, so your own screens can use it too.

Failure gets its own treatment: a smaller red wave, a shake, a plain-language reason and a Try again button. In both cases the haptic lands when the check or cross finishes drawing, not when the state flips. Screen readers hear the result explicitly, such as “Payment successful. ₹450 paid to Priya Sharma.”, and failures are announced assertively.

  1. (1) Spinner and warp streaks

    processing

  2. (2) Ring floods to a disc

    onImpact fires

  3. (3) Shockwave ring bending the screen

    1100ms · 28px

  4. (4) Aurora with content staggered in

    result

The impact beat in four frames

Five hand-written shaders (Beta)

The effects are fragment shaders written in GLSL: small programs that run on the GPU for every pixel. Every one is purely decorative and never carries information on its own. Each switches off under reduced motion and never flashes more than three times a second.

WidgetWhat it doesKey parameters
PdsWarpFieldHyperspace streaks radiating from a point. Speed and opacity are targets the field eases toward, so streaks never jump. At opacity 0 it stops ticking.speed (0 drift, 1 cruise, >1 jump) · opacity · origin
PdsShockwaveA refractive lens ring through any widget subtree. The child is captured to a texture only while the wave travels, so at rest it renders and takes taps normally.trigger · duration = 1100ms · strength = 28px
PdsAuroraA slow gradient bloom built from fractal noise, fading to nothing at every edge so it never shows a seam. Fades in over 1.2s.visible · colors · opacity = 0.5
PdsHoloFoilIridescent foil over any child: diffraction bands, a glare sliding opposite the tilt, sparkles. Used on payment cards.tilt · intensity = 1
PdsDissolveBurns a child away along a glowing, noisy edge. Falls back to a plain fade when shaders aren’t available.progress · origin · edgeColor = gold
  • PdsWarpFieldBeta

  • PdsShockwaveBeta

  • PdsAuroraBeta

  • PdsHoloFoilBeta

  • PdsDissolveBeta

The five effects, in table order · all Beta

Cards, history and rewards

Cards that feel like cards

PdsPaymentCardView shows a tokenised card at ID-1 proportions, the standard bank-card size. It tilts in real 3D toward the pointer under a holographic foil shader, and the token number reveals in staggered groups of four. Screen readers hear one clean sentence, such as “Northwind Bank Visa Credit card ending 4242, default”, with decoration left out.

PdsCardWallet stacks several cards. Back cards peek above, each level scaled 3.5% smaller and dimmed 7%. Drag the front card to tilt it; fling it and it tucks behind the stack with your release velocity. A revealed token number sits behind a draining countdown ring and hides itself after 5 seconds, a security measure you can configure. Every swipe has a tap alternative.

Drag the front card to tilt it. Fling it to tuck it behind.

Screen reader hearsNorthwind Bank credit card ending 4242, default

PdsCardWalletPdsPaymentCardViewLive demo

History you can scan

PdsTransactionList groups payments by day under sticky headers (“Today”, “Yesterday”, “Mon, 6 Oct”). It adds search across names and notes, filters for Sent, Received, Pending and Failed, skeleton loading and empty states that explain themselves, with a “Clear filters” action. Rows stagger in at 35ms steps.

Status is never colour alone. Credits show a “+” in green, failed rows are struck through and labelled, and each row reads as one sentence: “Paid ₹450 to Priya Sharma, success, 2:15 pm”. PdsStatusChip always pairs an icon with a label; only Pending breathes softly.

History

Today

Yesterday

Mon, 6 Oct

Each row reads as one sentence“Paid ₹450 to Priya Sharma, success, 2:15 pm”

Hover or focus a row. Filter, search or refresh.

PdsTransactionListPdsStatusChipLive demo

PdsReceiptCard finishes the story: a ticket-style UPI receipt with notched edges and a perforation line. The 12-digit UPI reference is grouped in fours and copies to the clipboard with a check-mark confirmation.

Rewards without exclusion

PdsScratchCard is scratch-to-reveal. An idle sheen invites touch, scratching sheds flakes with haptic ticks, and past half the card the foil burns away from your last scratch point while confetti flutters. A visible “Reveal without scratching” button is always there. Cover colours are configurable, for example a red-and-yellow Diwali foil.

PdsScratchCardLive demo · foil presets: Gold, Diwali

Accessibility is tested, not asserted

Pay DS is designed and tested against WCAG 2.2 AA criteria. A test file, test/a11y_audit_test.dart, runs Flutter’s guideline checkers on every component in light and dark, renders each one at 200% text, and checks contrast for every semantic colour pair. It runs on every deploy.

Every gesture has another way

Gestures are fast for some people and impossible for others. So every gesture in Pay DS has a non-gesture path:

  • Slide to pay → press and hold
  • Scratch card → “Reveal without scratching” button
  • Wallet swipe → tap
  • Keypad hold-to-clear → Esc
Every gesture has another way.

What the tests cover

WCAG 2.2 criterionHow Pay DS meets it
1.3.1 Info & relationshipsHeadings, merged rows, mutually exclusive filter groups, selected states
1.4.3 Contrast (text)Every token text pair ≥ 4.5:1, verified in light and dark by test
1.4.4 Resize textEvery component rendered at 200% text without overflow, by test
1.4.11 Non-text contrastStatus disc, thumb, outlines ≥ 3:1, by test
2.1.1 KeyboardFilters, cards, slide, scratch, copy: Tab + Enter/Space; amount: direct typing
2.2.1 Timing adjustableCard-number auto-hide is configurable and repeatable
2.3.1 Three flashesNo effect flashes; the shockwave is a single slow wave
2.4.7 Focus visible2px primary focus ring on custom controls, keyboard only
2.5.1 Pointer gesturesSlide → hold; scratch → reveal button; swipe → tap
2.5.8 Target sizeAndroid 48dp and iOS 44pt guidelines pass on every component, by test
3.3.1 Error identificationLimit and minimum errors explained in words plus an icon
4.1.2 Name, role, valueLabelled tap targets guideline passes on every component, by test
4.1.3 Status messagesPayment result, reveal, copy and default change are announced

Automated checks have limits, and we say so. The Widgetbook’s own guidance: automated checks catch roughly a third of real issues. Before shipping, test with TalkBack and VoiceOver on real devices, with Switch Access, and at the largest system font size.

Six ideas we’re exploring

Beyond the stable components, the catalogue holds six interactive prototypes. They are explorations, not shipped features, and none of them moves real money. Each one starts from a problem with today’s payment apps.

  • 01Sentence payPrototype
  • 02Friction that fitsPrototype
  • 03Hand it overPrototype
  • 04Money in motionPrototype
  • 05Bill-first splitPrototype
  • 06Settle stackPrototype
Six prototypes · explorations, not shipped features

#ConceptThe problemThe ideaWhat it would need
01Sentence payA payment is split across four screens; you only see it whole on a confirm sheet you’re least likely to read.One editable sentence, “Pay Priya ₹500 for dinner from Northwind Bank”, where each underlined word is a slot. Free text or voice is parsed, including “2k”.—
02Friction that fits₹40 at a tea stall and ₹40,000 to a stranger get the same confirm step, which is how scams work.Effort scales with risk: tap for routine, slide for a friend, type two digits and hold for a stranger, a pause and plain warning when flagged.A server-side risk score; thresholds owned by the risk team
03Hand it over₹50 and ₹50,000 look identical on screen.The amount becomes an object, from coin to bundle. Heavier money lags your finger and needs a harder flick. A button does the same for screen readers.—
04Money in motionInstant payments are irreversible, and stuck ones just say “pending”.A few seconds to “Catch it back”, then a live route from your bank through the UPI network to theirs, in plain words.Holding the debit a few seconds; hop-level status from the PSP
05Bill-first splitApps only understand “divide by N”.Scan a receipt, drag item chips onto people, spread tax by share, then request each exact share in one tap.On-device OCR; multi-payee collect requests
06Settle stackHome screens are launchers; real to-dos hide in SMS and notifications.Open to a short stack of what you owe: swipe right to pay, left to snooze. Amounts of ₹10,000+ still need hold-to-confirm.—

Two lines from the concept pages sum up the thinking. On Friction that fits: breaking the tap-tap-done rhythm is the single best defence against social-engineering scams. On Sentence pay: the confirmation is the input, so there is no separate review screen to skip.

Getting started

Pay DS ships as a Flutter package, pay_ds, installed from a local path or a private pub server. Five steps take you from install to a working payment.

1. Add the package

pubspec.yaml
# pubspec.yaml
dependencies:
  pay_ds:
    path: ../pay_ds   # or your private pub server

2. Apply the theme

PdsTheme builds light and dark Material 3 themes in Plus Jakarta Sans, with payment status colours as a ThemeExtension. PdsShaders.precache() loads the effect shaders before first use, so the first animation doesn’t stutter.

main.dart
import 'package:pay_ds/pay_ds.dart';

void main() {
  PdsShaders.precache(); // warm the effect shaders
  runApp(MaterialApp(
    theme: PdsTheme.light(),
    darkTheme: PdsTheme.dark(),
    home: const PayScreen(),
  ));
}

3. Compose a payment

upi.pay, payee and result stand in for your app’s own payment code.

pay_screen.dart
PdsAmountEntry(
  payeeName: payee.name,
  payeeHandle: payee.upiId,
  confirmStyle: PdsConfirmStyle.slide,
  onSubmit: (paise) async {
    final result = await upi.pay(payee, paise); // your payment call
    if (!context.mounted) return;
    Navigator.of(context).push(MaterialPageRoute(
      builder: (context) => PdsPaymentStatus(
        phase: result.ok ? PaymentPhase.success : PaymentPhase.failed,
        amountPaise: paise,
        payeeName: payee.name,
        referenceId: result.reference,
        onDone: () => Navigator.of(context).pop(),
      ),
    ));
  },
)

4. Connect your APIs

Components are controlled: you own loading, paging and errors; they own presentation. Models parse your wire format with fromJson and degrade safely. An unknown status reads as pending, and missing card art falls back to the network colours. A PaymentsRepository interface covers cursor-paged history and token reveal behind device auth.

5. Localise

Override any subset of strings; the rest default to English.

app.dart
PdsStringsScope(
  strings: PdsStrings(
    payAmount: (amount) => l10n.payAmount(amount),
    paymentFailed: l10n.paymentFailed,
    // ...any subset; the rest default to English
  ),
  child: app,
)

Using tokens directly

tokens.dart
Padding(
  padding: const EdgeInsets.all(PdsSpace.lg),
  child: AnimatedContainer(
    duration: PdsMotion.medium,
    curve: PdsMotion.easeOut,
    decoration: BoxDecoration(
      color: context.pdsStatus.successContainer,
      borderRadius: BorderRadius.circular(PdsRadius.card),
    ),
  ),
)

The fastest way to understand Pay DS is to open the Widgetbook, switch on dark mode, push text to 200%, slow time down, and run the checkout flow.

Open the Widgetbook