Web380 views

How to Build a Crypto Wallet Recovery Phrase Screen in Flutter (Full Code + Preview)

A seed phrase is the one screen in a crypto wallet where a design mistake costs users real money. This tutorial builds Aurum's recovery-phrase screen in Flutter: twelve numbered word chips in a six-row grid, hidden behind a live BackdropFilter blur until the user deliberately taps reveal, an amber banner warning that support will never ask for the phrase, a copy control that stays disabled while the words are hidden, and a pinned bar that converts from the reveal trigger into a gold 'I've saved it' continue gate.

Aurum · Recovery Phrase — Web3 Flutter UI screen
Live preview — Aurum · Recovery Phrase, built in pure Flutter.

What you'll build

  • ✓A blur-until-reveal overlay that blurs the real word chips with ImageFilter.blur(sigma 9) instead of showing a fake placeholder
  • ✓A six-row grid of twelve numbered word pills with tabular-figure indices so every word aligns
  • ✓A copy-to-clipboard control that is colour-disabled and tap-guarded until the phrase is revealed
  • ✓One pinned action bar that starts as 'Tap to reveal phrase' and becomes the gold 'I've saved it' continue gate
  • ✓An amber warning banner reminding users that support never asks for the phrase

Step-by-step build

1

Create the file

Add a new file at lib/web3_create_seed_phrase/web3_create_seed_phrase_screen.dart in your Flutter project.

2

Register the bundled fonts

No external packages — this is pure Flutter. It does bundle its design font (Inter), so drop the font file into fonts/ and declare it in pubspec.yaml:

pubspec.yaml
flutter:
  fonts:
    - family: Inter
      fonts:
        - asset: fonts/Inter-Regular.ttf
3

Build it, piece by piece

Here's how the screen goes together. Each block below is a real slice of the code with a plain-English explanation — paste them in order, or grab the whole file from the next section.

One extra import, and a screen with two states

web3_create_seed_phrase_screen.dart
import 'dart:ui' as ui;

import 'package:flutter/material.dart';

/// Web3 create — Your Recovery Phrase. A 12-word numbered grid in surface chips
/// hidden behind a blur-until-reveal overlay, with copy and an "I’ve saved it"
/// pinned bar (disabled until revealed). Self-contained per CONVENTIONS.md:
/// pure Flutter, bundled Inter font, painted graphics only, forced dark theme
/// so it renders standalone as a route.
class Web3CreateSeedPhraseScreen extends StatefulWidget {
  const Web3CreateSeedPhraseScreen({
    super.key,
    this.onContinue,
    this.onCopy,
    this.onBack,
  });

  final VoidCallback? onContinue;
  final VoidCallback? onCopy;
  final VoidCallback? onBack;

  @override
  State<Web3CreateSeedPhraseScreen> createState() =>
      _Web3CreateSeedPhraseScreenState();
}

The only import beyond `material.dart` is `dart:ui as ui`, and it is there for exactly one call — `ui.ImageFilter.blur` in the reveal overlay. Unlike a static confirmation page this is a `StatefulWidget`, because two things genuinely change on-device: whether the phrase is revealed and whether it has been copied. The three optional callbacks — `onContinue`, `onCopy`, `onBack` — draw the line of responsibility: the screen owns its reveal choreography, while navigation and the actual clipboard write belong to the caller. That is what lets the same file drop into any wallet backend unchanged.

A gold-on-near-black palette, twelve words, two booleans

web3_create_seed_phrase_screen.dart
class _Web3CreateSeedPhraseScreenState
    extends State<Web3CreateSeedPhraseScreen> {
  static const String _font = 'Inter';
  static const Color _bg = Color(0xFF0B0E11);
  static const Color _surface = Color(0xFF161A1E);
  static const Color _surfaceAlt = Color(0xFF1E2329);
  static const Color _brand = Color(0xFFF0B90B);
  static const Color _warn = Color(0xFFF0A020);
  static const Color _text = Color(0xFFEAECEF);
  static const Color _muted = Color(0xFF848E9C);
  static const Color _hairline = Color(0xFF2B3139);

  static const List<String> _words = <String>[
    'harbor', 'velvet', 'orbit', 'pledge',
    'cactus', 'nimble', 'salmon', 'tunnel',
    'gravity', 'wisdom', 'enroll', 'fabric',
  ];

  bool _revealed = false;
  bool _copied = false;

The palette is exchange-app dark: `#0B0E11` background, `#161A1E` surface, `#1E2329` for the chips one step lighter, `#EAECEF` text, `#848E9C` muted, `#2B3139` hairlines. Two yellows are deliberately kept apart — `_brand` gold `#F0B90B` is reserved for actions (the continue bar, the enabled copy tint), while `_warn` amber `#F0A020` is reserved for danger (the banner). If the warning wore the brand colour, the scariest element on screen would look tappable. The twelve words live in a `static const List<String>`, written four per line so the 6×2 grid is visible in the source, and the entire interactive surface hangs off just `_revealed` and `_copied`.

Forced dark theme and a phantom-balanced header

web3_create_seed_phrase_screen.dart
  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData.dark(useMaterial3: true),
      child: Scaffold(
        backgroundColor: _bg,
        body: SafeArea(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.fromLTRB(20, 8, 20, 0),
                child: Row(
                  children: <Widget>[
                    _CircleButton(
                      icon: Icons.arrow_back_ios_new_rounded,
                      onTap: widget.onBack,
                    ),
                    const Spacer(),
                    const Text(
                      'Step 1 of 3',
                      style: TextStyle(
                        fontFamily: _font,
                        fontSize: 13,
                        fontWeight: FontWeight.w500,
                        color: _muted,
                      ),
                    ),
                    const Spacer(),
                    const SizedBox(width: 40),
                  ],
                ),
              ),

The tree is wrapped in `Theme(data: ThemeData.dark(useMaterial3: true))`, so the screen renders correctly as a standalone route even inside a light-themed host app — ripples, icons and defaults all inherit dark. The header row is a `_CircleButton` back control, a `Spacer`, the 'Step 1 of 3' label at 13px muted, another `Spacer`, then `const SizedBox(width: 40)`. That trailing box is a phantom twin of the 40px back button: without it the two Spacers would centre the label between the button's right edge and the screen edge, nudging it off the true centreline. Framing the phrase as step 1 of 3 also promises a confirm step, which lowers the temptation to skim this one.

Body copy that states the stakes, and a guarded copy tap

web3_create_seed_phrase_screen.dart
              Expanded(
                child: ListView(
                  padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
                  children: <Widget>[
                    const Text(
                      'Your recovery phrase',
                      style: TextStyle(
                        fontFamily: _font,
                        fontSize: 24,
                        fontWeight: FontWeight.w700,
                        letterSpacing: -0.3,
                        color: _text,
                      ),
                    ),
                    const SizedBox(height: 8),
                    const Text(
                      'Write these 12 words down in order and keep them '
                      'offline. Anyone with this phrase can access your funds.',
                      style: TextStyle(
                        fontFamily: _font,
                        fontSize: 14,
                        height: 1.45,
                        color: _muted,
                      ),
                    ),
                    const SizedBox(height: 20),
                    _WarningBanner(),
                    const SizedBox(height: 20),
                    _PhraseGrid(words: _words, revealed: _revealed),
                    const SizedBox(height: 16),
                    Center(
                      child: _CopyButton(
                        copied: _copied,
                        enabled: _revealed,
                        onTap: () {
                          if (!_revealed) return;
                          setState(() => _copied = true);
                          widget.onCopy?.call();
                        },
                      ),
                    ),
                  ],
                ),

The body is a `ListView` so short phones scroll instead of overflowing. The 24px `w700` headline with `letterSpacing: -0.3` sits over guidance that ends on the sentence that matters: 'Anyone with this phrase can access your funds.' Order is intentional — `_WarningBanner()` renders before `_PhraseGrid`, so the reader passes the caution before reaching the secret. The grid receives both `_words` and `_revealed`, letting it manage its own overlay. The copy control is double-locked: it gets `enabled: _revealed` for its visual state, and its `onTap` opens with `if (!_revealed) return;` — so even a registered tap on the blurred state does nothing. Only after that guard does `setState` flip `_copied` and forward to `widget.onCopy?.call()`.

One pinned bar, two jobs

web3_create_seed_phrase_screen.dart
              Padding(
                padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
                child: SizedBox(
                  height: 54,
                  child: Material(
                    color: _revealed ? _brand : _surfaceAlt,
                    borderRadius: BorderRadius.circular(14),
                    child: InkWell(
                      borderRadius: BorderRadius.circular(14),
                      onTap: _revealed
                          ? widget.onContinue
                          : () => setState(() => _revealed = true),
                      child: Center(
                        child: Text(
                          _revealed ? 'I’ve saved it' : 'Tap to reveal phrase',
                          style: TextStyle(
                            fontFamily: _font,
                            fontSize: 16,
                            fontWeight: FontWeight.w600,
                            color: _revealed ? _bg : _text,
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

The bottom bar is a single 54px `Material` + `InkWell`, and every property forks on `_revealed`. Hidden: `_surfaceAlt` fill, `_text` label, 'Tap to reveal phrase', and a tap that just runs `setState(() => _revealed = true)`. Revealed: gold `_brand` fill, near-black `_bg` label for contrast on gold, 'I've saved it', wired to `widget.onContinue`. Making reveal and continue the same physical control is the gate — there is no path to the continue action that does not pass through revealing the words first, enforced by the widget tree rather than by validation logic. Both `Material` and `InkWell` carry the same `BorderRadius.circular(14)` so the ripple clips to the rounded shape.

The word grid and the blur that hides real content

web3_create_seed_phrase_screen.dart
class _PhraseGrid extends StatelessWidget {
  const _PhraseGrid({required this.words, required this.revealed});

  final List<String> words;
  final bool revealed;

  @override
  Widget build(BuildContext context) {
    final Widget grid = Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: _Web3CreateSeedPhraseScreenState._surface,
        borderRadius: BorderRadius.circular(16),
        border: Border.all(color: _Web3CreateSeedPhraseScreenState._hairline),
      ),
      child: Column(
        children: <Widget>[
          for (int row = 0; row < 6; row++)
            Padding(
              padding: EdgeInsets.only(bottom: row == 5 ? 0 : 10),
              child: Row(
                children: <Widget>[
                  Expanded(child: _WordChip(index: row * 2, word: words[row * 2])),
                  const SizedBox(width: 10),
                  Expanded(
                      child: _WordChip(
                          index: row * 2 + 1, word: words[row * 2 + 1])),
                ],
              ),
            ),
        ],
      ),
    );

    return Stack(
      children: <Widget>[
        grid,
        if (!revealed)
          Positioned.fill(
            child: ClipRRect(
              borderRadius: BorderRadius.circular(16),
              child: BackdropFilter(
                filter: ui.ImageFilter.blur(sigmaX: 9, sigmaY: 9),
                child: Container(
                  decoration: BoxDecoration(
                    color: _Web3CreateSeedPhraseScreenState._bg
                        .withValues(alpha: 0.55),
                    borderRadius: BorderRadius.circular(16),
                  ),
                  alignment: Alignment.center,
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: <Widget>[
                      Container(
                        width: 46,
                        height: 46,
                        decoration: BoxDecoration(
                          color: _Web3CreateSeedPhraseScreenState._surfaceAlt,
                          shape: BoxShape.circle,
                          border: Border.all(
                              color: _Web3CreateSeedPhraseScreenState._hairline),
                        ),
                        child: const Icon(Icons.visibility_off_rounded,
                            size: 20,
                            color: _Web3CreateSeedPhraseScreenState._muted),
                      ),
                      const SizedBox(height: 12),
                      const Text(
                        'Tap “reveal” when you’re alone',
                        style: TextStyle(
                          fontFamily: _Web3CreateSeedPhraseScreenState._font,
                          fontSize: 13,
                          fontWeight: FontWeight.w600,
                          color: _Web3CreateSeedPhraseScreenState._text,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
      ],
    );
  }
}

`_PhraseGrid` first builds the grid into a local `grid` variable: a `_surface` card with a `_hairline` border and 16px radius, emitting six rows via a collection-for where `EdgeInsets.only(bottom: row == 5 ? 0 : 10)` puts 10px between rows and none after the last, and the chip indices come from `row * 2` and `row * 2 + 1`. Then a `Stack` layers the mask over it — but only `if (!revealed)`, so once revealed the overlay leaves the tree entirely. The mask is `Positioned.fill` → `ClipRRect` at the same 16px radius (so the blur cannot bleed past the card's corners) → `BackdropFilter` with `ui.ImageFilter.blur(sigmaX: 9, sigmaY: 9)` → a scrim of `_bg.withValues(alpha: 0.55)`. Because it is a backdrop blur, the real chips are genuinely rendered underneath and smeared live — the hidden state is honest, not a screenshot or placeholder. Centred on the scrim: a 46px `_surfaceAlt` circle holding a `visibility_off_rounded` icon, and the line 'Tap "reveal" when you're alone', which turns the mask itself into a shoulder-surfing prompt.

Word chips with tabular figures

web3_create_seed_phrase_screen.dart
class _WordChip extends StatelessWidget {
  const _WordChip({required this.index, required this.word});

  final int index;
  final String word;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 11),
      decoration: BoxDecoration(
        color: _Web3CreateSeedPhraseScreenState._surfaceAlt,
        borderRadius: BorderRadius.circular(10),
      ),
      child: Row(
        children: <Widget>[
          SizedBox(
            width: 20,
            child: Text(
              '${index + 1}',
              style: const TextStyle(
                fontFamily: _Web3CreateSeedPhraseScreenState._font,
                fontSize: 12,
                fontWeight: FontWeight.w600,
                fontFeatures: <FontFeature>[FontFeature.tabularFigures()],
                color: _Web3CreateSeedPhraseScreenState._muted,
              ),
            ),
          ),
          Expanded(
            child: Text(
              word,
              style: const TextStyle(
                fontFamily: _Web3CreateSeedPhraseScreenState._font,
                fontSize: 14,
                fontWeight: FontWeight.w600,
                color: _Web3CreateSeedPhraseScreenState._text,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

`_WordChip` is a `_surfaceAlt` pill with a 10px radius and 12×11 padding. Its index sits in a fixed `SizedBox(width: 20)` styled with `FontFeature.tabularFigures()` — every digit takes the same advance width, so '1' and '12' occupy identical space and all twelve words start on the same vertical line without any per-row measuring. The index is deliberately quieter than the word (12px `_muted` versus 14px `_text`, both `w600`), because during a written backup the number is a checkpoint but the word is the payload. The word sits in an `Expanded`, so an unusually long word truncates inside its own chip instead of shoving its row wider than the others.

The warning banner, copy button and circle button

web3_create_seed_phrase_screen.dart
class _WarningBanner extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(14),
      decoration: BoxDecoration(
        color: _Web3CreateSeedPhraseScreenState._warn.withValues(alpha: 0.10),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(
            color: _Web3CreateSeedPhraseScreenState._warn.withValues(alpha: 0.3)),
      ),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          const Icon(Icons.warning_amber_rounded,
              size: 20, color: _Web3CreateSeedPhraseScreenState._warn),
          const SizedBox(width: 10),
          const Expanded(
            child: Text(
              'Never share your phrase. Aurum support will never ask for it.',
              style: TextStyle(
                fontFamily: _Web3CreateSeedPhraseScreenState._font,
                fontSize: 12.5,
                height: 1.35,
                fontWeight: FontWeight.w500,
                color: _Web3CreateSeedPhraseScreenState._warn,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _CopyButton extends StatelessWidget {
  const _CopyButton({
    required this.copied,
    required this.enabled,
    required this.onTap,
  });

  final bool copied;
  final bool enabled;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    final Color tint = enabled
        ? _Web3CreateSeedPhraseScreenState._brand
        : _Web3CreateSeedPhraseScreenState._muted;
    return Material(
      color: Colors.transparent,
      borderRadius: BorderRadius.circular(10),
      child: InkWell(
        borderRadius: BorderRadius.circular(10),
        onTap: onTap,
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              Icon(copied ? Icons.check_rounded : Icons.copy_rounded,
                  size: 16, color: tint),
              const SizedBox(width: 6),
              Text(
                copied ? 'Copied' : 'Copy to clipboard',
                style: TextStyle(
                  fontFamily: _Web3CreateSeedPhraseScreenState._font,
                  fontSize: 13.5,
                  fontWeight: FontWeight.w600,
                  color: tint,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _CircleButton extends StatelessWidget {
  const _CircleButton({required this.icon, required this.onTap});

  final IconData icon;
  final VoidCallback? onTap;

  @override
  Widget build(BuildContext context) {
    return Material(
      color: _Web3CreateSeedPhraseScreenState._surface,
      shape: const CircleBorder(),
      child: InkWell(
        customBorder: const CircleBorder(),
        onTap: onTap,
        child: SizedBox(
          width: 40,
          height: 40,
          child: Icon(icon,
              size: 16, color: _Web3CreateSeedPhraseScreenState._text),
        ),
      ),
    );
  }
}

Three small widgets finish the screen. `_WarningBanner` uses one amber at two strengths — `_warn.withValues(alpha: 0.10)` fill and `alpha: 0.3` border — with the icon and text in full `_warn`, so the whole banner reads as a single temperature; `CrossAxisAlignment.start` plus an `Expanded` keeps the 20px `warning_amber_rounded` icon pinned to the first line if the copy wraps. `_CopyButton` computes one `tint` — `_brand` when enabled, `_muted` when not — and applies it to both icon and label, so the disabled state is pure colour with no extra widgets, and the copied state swaps `copy_rounded` for `check_rounded` and 'Copy to clipboard' for 'Copied'. `_CircleButton` pairs `Material(shape: CircleBorder())` with `InkWell(customBorder: CircleBorder())`, which is the standard trick for a ripple that clips to a circle, sized 40×40 with a small 16px glyph.

Full code

The complete, ready-to-paste source. Free to use in your projects — one click copies it all.

import 'dart:ui' as ui;

import 'package:flutter/material.dart';

/// Web3 create — Your Recovery Phrase. A 12-word numbered grid in surface chips
/// hidden behind a blur-until-reveal overlay, with copy and an "I’ve saved it"
/// pinned bar (disabled until revealed). Self-contained per CONVENTIONS.md:
/// pure Flutter, bundled Inter font, painted graphics only, forced dark theme
/// so it renders standalone as a route.
class Web3CreateSeedPhraseScreen extends StatefulWidget {
  const Web3CreateSeedPhraseScreen({
    super.key,
    this.onContinue,
    this.onCopy,
    this.onBack,
  });

  final VoidCallback? onContinue;
  final VoidCallback? onCopy;
  final VoidCallback? onBack;

  @override
  State<Web3CreateSeedPhraseScreen> createState() =>
      _Web3CreateSeedPhraseScreenState();
}

class _Web3CreateSeedPhraseScreenState
    extends State<Web3CreateSeedPhraseScreen> {
  static const String _font = 'Inter';
  static const Color _bg = Color(0xFF0B0E11);
  static const Color _surface = Color(0xFF161A1E);
  static const Color _surfaceAlt = Color(0xFF1E2329);
  static const Color _brand = Color(0xFFF0B90B);
  static const Color _warn = Color(0xFFF0A020);
  static const Color _text = Color(0xFFEAECEF);
  static const Color _muted = Color(0xFF848E9C);
  static const Color _hairline = Color(0xFF2B3139);

  static const List<String> _words = <String>[
    'harbor', 'velvet', 'orbit', 'pledge',
    'cactus', 'nimble', 'salmon', 'tunnel',
    'gravity', 'wisdom', 'enroll', 'fabric',
  ];

  bool _revealed = false;
  bool _copied = false;

  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData.dark(useMaterial3: true),
      child: Scaffold(
        backgroundColor: _bg,
        body: SafeArea(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.fromLTRB(20, 8, 20, 0),
                child: Row(
                  children: <Widget>[
                    _CircleButton(
                      icon: Icons.arrow_back_ios_new_rounded,
                      onTap: widget.onBack,
                    ),
                    const Spacer(),
                    const Text(
                      'Step 1 of 3',
                      style: TextStyle(
                        fontFamily: _font,
                        fontSize: 13,
                        fontWeight: FontWeight.w500,
                        color: _muted,
                      ),
                    ),
                    const Spacer(),
                    const SizedBox(width: 40),
                  ],
                ),
              ),
              Expanded(
                child: ListView(
                  padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
                  children: <Widget>[
                    const Text(
                      'Your recovery phrase',
                      style: TextStyle(
                        fontFamily: _font,
                        fontSize: 24,
                        fontWeight: FontWeight.w700,
                        letterSpacing: -0.3,
                        color: _text,
                      ),
                    ),
                    const SizedBox(height: 8),
                    const Text(
                      'Write these 12 words down in order and keep them '
                      'offline. Anyone with this phrase can access your funds.',
                      style: TextStyle(
                        fontFamily: _font,
                        fontSize: 14,
                        height: 1.45,
                        color: _muted,
                      ),
                    ),
                    const SizedBox(height: 20),
                    _WarningBanner(),
                    const SizedBox(height: 20),
                    _PhraseGrid(words: _words, revealed: _revealed),
                    const SizedBox(height: 16),
                    Center(
                      child: _CopyButton(
                        copied: _copied,
                        enabled: _revealed,
                        onTap: () {
                          if (!_revealed) return;
                          setState(() => _copied = true);
                          widget.onCopy?.call();
                        },
                      ),
                    ),
                  ],
                ),
              ),
              Padding(
                padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
                child: SizedBox(
                  height: 54,
                  child: Material(
                    color: _revealed ? _brand : _surfaceAlt,
                    borderRadius: BorderRadius.circular(14),
                    child: InkWell(
                      borderRadius: BorderRadius.circular(14),
                      onTap: _revealed
                          ? widget.onContinue
                          : () => setState(() => _revealed = true),
                      child: Center(
                        child: Text(
                          _revealed ? 'I’ve saved it' : 'Tap to reveal phrase',
                          style: TextStyle(
                            fontFamily: _font,
                            fontSize: 16,
                            fontWeight: FontWeight.w600,
                            color: _revealed ? _bg : _text,
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _PhraseGrid extends StatelessWidget {
  const _PhraseGrid({required this.words, required this.revealed});

  final List<String> words;
  final bool revealed;

  @override
  Widget build(BuildContext context) {
    final Widget grid = Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: _Web3CreateSeedPhraseScreenState._surface,
        borderRadius: BorderRadius.circular(16),
        border: Border.all(color: _Web3CreateSeedPhraseScreenState._hairline),
      ),
      child: Column(
        children: <Widget>[
          for (int row = 0; row < 6; row++)
            Padding(
              padding: EdgeInsets.only(bottom: row == 5 ? 0 : 10),
              child: Row(
                children: <Widget>[
                  Expanded(child: _WordChip(index: row * 2, word: words[row * 2])),
                  const SizedBox(width: 10),
                  Expanded(
                      child: _WordChip(
                          index: row * 2 + 1, word: words[row * 2 + 1])),
                ],
              ),
            ),
        ],
      ),
    );

    return Stack(
      children: <Widget>[
        grid,
        if (!revealed)
          Positioned.fill(
            child: ClipRRect(
              borderRadius: BorderRadius.circular(16),
              child: BackdropFilter(
                filter: ui.ImageFilter.blur(sigmaX: 9, sigmaY: 9),
                child: Container(
                  decoration: BoxDecoration(
                    color: _Web3CreateSeedPhraseScreenState._bg
                        .withValues(alpha: 0.55),
                    borderRadius: BorderRadius.circular(16),
                  ),
                  alignment: Alignment.center,
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: <Widget>[
                      Container(
                        width: 46,
                        height: 46,
                        decoration: BoxDecoration(
                          color: _Web3CreateSeedPhraseScreenState._surfaceAlt,
                          shape: BoxShape.circle,
                          border: Border.all(
                              color: _Web3CreateSeedPhraseScreenState._hairline),
                        ),
                        child: const Icon(Icons.visibility_off_rounded,
                            size: 20,
                            color: _Web3CreateSeedPhraseScreenState._muted),
                      ),
                      const SizedBox(height: 12),
                      const Text(
                        'Tap “reveal” when you’re alone',
                        style: TextStyle(
                          fontFamily: _Web3CreateSeedPhraseScreenState._font,
                          fontSize: 13,
                          fontWeight: FontWeight.w600,
                          color: _Web3CreateSeedPhraseScreenState._text,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
      ],
    );
  }
}

class _WordChip extends StatelessWidget {
  const _WordChip({required this.index, required this.word});

  final int index;
  final String word;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 11),
      decoration: BoxDecoration(
        color: _Web3CreateSeedPhraseScreenState._surfaceAlt,
        borderRadius: BorderRadius.circular(10),
      ),
      child: Row(
        children: <Widget>[
          SizedBox(
            width: 20,
            child: Text(
              '${index + 1}',
              style: const TextStyle(
                fontFamily: _Web3CreateSeedPhraseScreenState._font,
                fontSize: 12,
                fontWeight: FontWeight.w600,
                fontFeatures: <FontFeature>[FontFeature.tabularFigures()],
                color: _Web3CreateSeedPhraseScreenState._muted,
              ),
            ),
          ),
          Expanded(
            child: Text(
              word,
              style: const TextStyle(
                fontFamily: _Web3CreateSeedPhraseScreenState._font,
                fontSize: 14,
                fontWeight: FontWeight.w600,
                color: _Web3CreateSeedPhraseScreenState._text,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _WarningBanner extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(14),
      decoration: BoxDecoration(
        color: _Web3CreateSeedPhraseScreenState._warn.withValues(alpha: 0.10),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(
            color: _Web3CreateSeedPhraseScreenState._warn.withValues(alpha: 0.3)),
      ),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          const Icon(Icons.warning_amber_rounded,
              size: 20, color: _Web3CreateSeedPhraseScreenState._warn),
          const SizedBox(width: 10),
          const Expanded(
            child: Text(
              'Never share your phrase. Aurum support will never ask for it.',
              style: TextStyle(
                fontFamily: _Web3CreateSeedPhraseScreenState._font,
                fontSize: 12.5,
                height: 1.35,
                fontWeight: FontWeight.w500,
                color: _Web3CreateSeedPhraseScreenState._warn,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _CopyButton extends StatelessWidget {
  const _CopyButton({
    required this.copied,
    required this.enabled,
    required this.onTap,
  });

  final bool copied;
  final bool enabled;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    final Color tint = enabled
        ? _Web3CreateSeedPhraseScreenState._brand
        : _Web3CreateSeedPhraseScreenState._muted;
    return Material(
      color: Colors.transparent,
      borderRadius: BorderRadius.circular(10),
      child: InkWell(
        borderRadius: BorderRadius.circular(10),
        onTap: onTap,
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              Icon(copied ? Icons.check_rounded : Icons.copy_rounded,
                  size: 16, color: tint),
              const SizedBox(width: 6),
              Text(
                copied ? 'Copied' : 'Copy to clipboard',
                style: TextStyle(
                  fontFamily: _Web3CreateSeedPhraseScreenState._font,
                  fontSize: 13.5,
                  fontWeight: FontWeight.w600,
                  color: tint,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _CircleButton extends StatelessWidget {
  const _CircleButton({required this.icon, required this.onTap});

  final IconData icon;
  final VoidCallback? onTap;

  @override
  Widget build(BuildContext context) {
    return Material(
      color: _Web3CreateSeedPhraseScreenState._surface,
      shape: const CircleBorder(),
      child: InkWell(
        customBorder: const CircleBorder(),
        onTap: onTap,
        child: SizedBox(
          width: 40,
          height: 40,
          child: Icon(icon,
              size: 16, color: _Web3CreateSeedPhraseScreenState._text),
        ),
      ),
    );
  }
}

Plus bundled 1 binary asset (fonts/images). The CLI and MCP install those for you automatically.

Two faster ways to add it

Copy-paste works, but you can skip it entirely.

1. FlutterKit CLI

One command drops this screen — and its fonts — straight into your project.

$ flutterkit add web3-create-seed-phrase

2. AI agent (MCP)

Connect FlutterKit's MCP server in Claude or Cursor and just ask your agent to install web3-create-seed-phrase — it fetches and writes the files for you.

FAQ

Is this recovery phrase screen free to use, including commercially?

Yes. FlutterKit screens are free for personal and commercial projects alike — copy the code from this page, install it via the CLI, or pull it through MCP, and ship it in a production wallet. No licence key, no sign-up, no attribution required.

Are the twelve words a real BIP-39 mnemonic?

No — they are a hard-coded `static const` placeholder list ('harbor', 'velvet', 'orbit'…) chosen to look plausible in previews, and the screen performs no key derivation. In a real wallet, generate the mnemonic with a vetted BIP-39 implementation and pass those words into `_PhraseGrid` instead; the UI does not care where the list comes from.

Does the copy button actually write to the clipboard?

Not by itself. Tapping it flips the local `_copied` flag for the 'Copied' check state and invokes `onCopy` — the actual `Clipboard.setData` call is left to you deliberately, since many wallets restrict or discourage copying a seed phrase at all. If you do wire it up, join `_words` with spaces and consider clearing the clipboard after a timeout.

Does it need any packages or fonts?

No third-party packages — it is pure Flutter, and even the blur comes from the SDK's own `dart:ui` `ImageFilter`, not a plugin. The Inter font ships bundled with the screen at `lib/web3_create_seed_phrase/fonts/Inter.ttf`, so typography matches the preview with no Google Fonts dependency.

Which Flutter version does this require?

Flutter 3.22 or newer: the scrim and warning banner use `Color.withValues(alpha: …)`, and the constructor uses `super.key`. On an older SDK, replace each `withValues(alpha: x)` with `withOpacity(x)` and expand the constructor to the `{Key? key} : super(key: key)` form.

Related screens