E-commerce23 views

How to Build a Fashion Size Guide Screen in Flutter (Full Code + Preview)

Most apparel returns happen because the shopper guessed a size, and a size chart buried in a product description does not stop the guess. This tutorial builds StyleCart's size-guide screen in Flutter: a segmented Centimetres / Inches toggle driven by a single `_cmUnit` boolean, a six-row `Table` from XS to XXL whose cells convert through one `_v()` helper, a gradient 'Find my size' card wired to `onFindMySize`, and a 'How to measure' list generated from a nested string list. Pure Flutter, no packages, no images.

Size Guide — E-commerce Flutter UI screen
Live preview — Size Guide, built in pure Flutter.

What you'll build

  • A cm/inch segmented control whose selected tab is a white pill on a `_surface` track, flipped by one `bool _cmUnit`
  • A `_v(int cm)` formatter that keeps centimetres as integers and renders inches to one decimal place
  • A `ClipRRect`-rounded `Table` with a `_ink` header row, `FlexColumnWidth` ratios and zebra rows generated from a `List<List<int>>`
  • A coral `LinearGradient` 'Find my size' card with a translucent ruler badge and a white 'Start' pill bound to `onFindMySize`
  • A 'How to measure' section that maps a `const List<List<String>>` into icon-led tip rows

Step-by-step build

1

Create the file

Add a new file at lib/ecom_product_size_guide/ecom_product_size_guide_screen.dart in your Flutter project.

2

Register the bundled fonts

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

pubspec.yaml
flutter:
  fonts:
    - family: Manrope
      fonts:
        - asset: fonts/Manrope-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.

A stateful screen with two exits

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

/// StyleCart — Size Guide.
///
/// A measurements reference: a unit toggle (cm / in), a size-chart table with
/// crisp rules, body-measuring tips and a "find my size" helper that suggests a
/// size from height + weight.
///
/// Self-contained per CONVENTIONS.md: pure Flutter, bundled Manrope, inline
/// Airbnb-style tokens, own light theme + SafeArea, no images (table + icons).
/// Exposes callbacks only; the registry wires navigation.
class EcomProductSizeGuideScreen extends StatefulWidget {
  const EcomProductSizeGuideScreen({
    super.key,
    this.onClose,
    this.onFindMySize,
  });

  final VoidCallback? onClose;
  final VoidCallback? onFindMySize;

  @override
  State<EcomProductSizeGuideScreen> createState() =>
      _EcomProductSizeGuideScreenState();
}

Unlike most reference screens, this one is a `StatefulWidget`, and the reason is small but real: the unit toggle changes every number in the chart, so something has to hold which unit is selected. The widget itself stays thin — two optional callbacks, `onClose` and `onFindMySize`, and nothing else. There is no size parameter and no data parameter because the chart is a static reference for the shop's own garments; the registry only needs to wire where the close icon and the Start button go. The doc comment spells out the self-contained contract: pure Flutter, the bundled Manrope font, inline tokens, its own light theme and `SafeArea`, and no images, since a table and two icons are all the screen needs.

Tokens, the measurement matrix and the unit formatter

ecom_product_size_guide_screen.dart
class _EcomProductSizeGuideScreenState
    extends State<EcomProductSizeGuideScreen> {
  static const String _font = 'Manrope';
  static const Color _canvas = Color(0xFFFFFFFF);
  static const Color _ink = Color(0xFF222222);
  static const Color _muted = Color(0xFF6A6A6A);
  static const Color _brand = Color(0xFFFF385C);
  static const Color _surface = Color(0xFFF2F2F2);
  static const Color _hairline = Color(0xFFEBEBEB);

  // Measurements in cm: [chest, waist, length].
  static const List<String> _sizes = <String>['XS', 'S', 'M', 'L', 'XL', 'XXL'];
  static const List<List<int>> _cm = <List<int>>[
    <int>[88, 72, 68],
    <int>[94, 78, 70],
    <int>[100, 84, 72],
    <int>[106, 90, 74],
    <int>[112, 96, 76],
    <int>[118, 102, 78],
  ];

  bool _cmUnit = true;

  String _v(int cm) =>
      _cmUnit ? '$cm' : (cm / 2.54).toStringAsFixed(1);

The palette is Airbnb-flavoured: `_brand` `#FF385C` coral, `_ink` `#222222`, `_muted` `#6A6A6A`, a `_surface` `#F2F2F2` for the toggle track and a `_hairline` `#EBEBEB` for rules. The chart data is a `List<List<int>> _cm` — six rows of `[chest, waist, length]` in centimetres — paired with a parallel `_sizes` list from XS to XXL, so adding a size means appending one entry to each. The only mutable state is `bool _cmUnit = true`. Everything flows through `_v(int cm)`: with centimetres selected it returns the integer as-is, otherwise it divides by 2.54 and calls `toStringAsFixed(1)`, so 88 cm becomes '34.6' rather than a long decimal tail. Storing metric only and converting at render time keeps a single source of truth and makes the toggle a pure display concern.

Forcing a light theme and stacking the page

ecom_product_size_guide_screen.dart
  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData.light(useMaterial3: true),
      child: Scaffold(
        backgroundColor: _canvas,
        body: SafeArea(
          child: Column(
            children: <Widget>[
              _header(),
              const Divider(height: 1, color: _hairline),
              Expanded(
                child: ListView(
                  padding: const EdgeInsets.fromLTRB(20, 18, 20, 28),
                  children: <Widget>[
                    _unitToggle(),
                    const SizedBox(height: 18),
                    _table(),
                    const SizedBox(height: 26),
                    _findMySize(),
                    const SizedBox(height: 26),
                    _tips(),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

`build` wraps the `Scaffold` in `Theme(data: ThemeData.light(useMaterial3: true))`, so the screen looks identical even if the host app runs a dark theme — a size chart with inverted zebra stripes would be hard to read. Inside `SafeArea`, a `Column` places `_header()` and a one-pixel `Divider` in `_hairline` above an `Expanded` `ListView`. The list carries the four sections in reading order — `_unitToggle()`, `_table()`, `_findMySize()`, `_tips()` — separated by 18px and 26px `SizedBox` gaps, with padding of `fromLTRB(20, 18, 20, 28)` so the last tip clears the home indicator. Using `ListView` rather than `SingleChildScrollView` means the whole body scrolls as one and the header stays pinned.

The close-only header

ecom_product_size_guide_screen.dart
  Widget _header() {
    return Padding(
      padding: const EdgeInsets.fromLTRB(8, 4, 16, 4),
      child: Row(
        children: <Widget>[
          const Expanded(
            child: Padding(
              padding: EdgeInsets.only(left: 12),
              child: Text(
                'Size guide',
                style: TextStyle(
                  fontFamily: _font,
                  fontSize: 19,
                  fontWeight: FontWeight.w800,
                  letterSpacing: -0.3,
                  color: _ink,
                ),
              ),
            ),
          ),
          IconButton(
            onPressed: widget.onClose,
            icon: const Icon(Icons.close_rounded, size: 24, color: _ink),
          ),
        ],
      ),
    );
  }

The header is a `Row` with an `Expanded` title and a trailing `IconButton`. The title 'Size guide' is 19px Manrope `w800` with `letterSpacing: -0.3` and is nudged 12px right by an inner `Padding`, while the row's own padding is `fromLTRB(8, 4, 16, 4)` — the asymmetric left inset compensates for the fact that there is no leading icon on this side. The trailing icon is `Icons.close_rounded` rather than a back arrow, because this screen opens as a sheet or modal from a product page and closing it returns to that product; a back arrow would imply it sits inside a longer flow. Its `onPressed` is `widget.onClose`, left null-safe so the screen still renders in a preview with no navigation attached.

The segmented Centimetres / Inches toggle

ecom_product_size_guide_screen.dart
  Widget _unitToggle() {
    return Container(
      height: 42,
      padding: const EdgeInsets.all(4),
      decoration: BoxDecoration(
        color: _surface,
        borderRadius: BorderRadius.circular(12),
      ),
      child: Row(
        children: <Widget>[
          _unitTab('Centimetres', _cmUnit, () => setState(() => _cmUnit = true)),
          _unitTab('Inches', !_cmUnit, () => setState(() => _cmUnit = false)),
        ],
      ),
    );
  }

  Widget _unitTab(String t, bool on, VoidCallback onTap) {
    return Expanded(
      child: GestureDetector(
        onTap: onTap,
        child: Container(
          alignment: Alignment.center,
          decoration: BoxDecoration(
            color: on ? _canvas : Colors.transparent,
            borderRadius: BorderRadius.circular(9),
          ),
          child: Text(
            t,
            style: TextStyle(
              fontFamily: _font,
              fontSize: 13.5,
              fontWeight: FontWeight.w700,
              color: on ? _ink : _muted,
            ),
          ),
        ),
      ),
    );
  }

`_unitToggle` is a 42px `Container` in `_surface` with 12px corners and 4px inner padding, holding two `_unitTab` widgets in a `Row`. Each tab is `Expanded`, so both halves are equal width, and is wrapped in a `GestureDetector` whose `onTap` runs `setState(() => _cmUnit = true)` or `false`. The selected tab paints a `_canvas` white pill with 9px corners — 3px less than the track, so the inner and outer radii stay concentric — while the unselected one is `Colors.transparent`. Text is 13.5px `w700`, `_ink` when on and `_muted` when off. Because the whole screen rebuilds on `setState`, the table below recomputes every cell through `_v()` in the same frame; there is no separate conversion step or cached inch list.

The size-chart table with dark header and zebra rows

ecom_product_size_guide_screen.dart
  Widget _table() {
    return ClipRRect(
      borderRadius: BorderRadius.circular(14),
      child: Table(
        border: TableBorder.symmetric(
          inside: const BorderSide(color: _hairline),
        ),
        columnWidths: const <int, TableColumnWidth>{
          0: FlexColumnWidth(1.1),
          1: FlexColumnWidth(1.3),
          2: FlexColumnWidth(1.3),
          3: FlexColumnWidth(1.3),
        },
        children: <TableRow>[
          TableRow(
            decoration: const BoxDecoration(color: _ink),
            children: <Widget>[
              _th('Size'),
              _th('Chest'),
              _th('Waist'),
              _th('Length'),
            ],
          ),
          ...List<TableRow>.generate(_sizes.length, (int i) {
            return TableRow(
              decoration: BoxDecoration(
                color: i.isEven ? _canvas : _surface.withValues(alpha: 0.5),
              ),
              children: <Widget>[
                _td(_sizes[i], bold: true),
                _td(_v(_cm[i][0])),
                _td(_v(_cm[i][1])),
                _td(_v(_cm[i][2])),
              ],
            );
          }),
        ],
      ),
    );
  }

  Widget _th(String t) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
      child: Text(
        t,
        textAlign: TextAlign.center,
        style: const TextStyle(
          fontFamily: _font,
          fontSize: 12.5,
          fontWeight: FontWeight.w700,
          color: _canvas,
        ),
      ),
    );
  }

  Widget _td(String t, {bool bold = false}) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 8),
      child: Text(
        t,
        textAlign: TextAlign.center,
        style: TextStyle(
          fontFamily: _font,
          fontSize: 13.5,
          fontWeight: bold ? FontWeight.w800 : FontWeight.w500,
          color: _ink,
        ),
      ),
    );
  }

`_table` uses Flutter's `Table` widget rather than a column of rows, so the four columns align exactly regardless of text width. `ClipRRect` with a 14px radius rounds the corners because `Table` cannot round itself. `TableBorder.symmetric(inside: BorderSide(color: _hairline))` draws rules only between cells, leaving the outside clean. `columnWidths` gives the Size column `FlexColumnWidth(1.1)` against `1.3` for the three measurements, so the short labels take slightly less room. The header `TableRow` is solid `_ink` with white 12.5px `w700` `_th` cells. Body rows come from `List.generate(_sizes.length, ...)`: even rows are `_canvas`, odd rows `_surface.withValues(alpha: 0.5)` for a soft zebra, and each row calls `_td` once for the bold size label and three times through `_v(_cm[i][n])` for chest, waist and length. `_td` uses 13px vertical padding versus 12px in `_th`, giving body rows a touch more breathing room than the header.

The gradient 'Find my size' card

ecom_product_size_guide_screen.dart
  Widget _findMySize() {
    return Container(
      padding: const EdgeInsets.all(18),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(16),
        gradient: const LinearGradient(
          colors: <Color>[Color(0xFFFF385C), Color(0xFFE0244E)],
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
        ),
      ),
      child: Row(
        children: <Widget>[
          Container(
            width: 44,
            height: 44,
            alignment: Alignment.center,
            decoration: BoxDecoration(
              color: Colors.white.withValues(alpha: 0.18),
              shape: BoxShape.circle,
            ),
            child: const Icon(Icons.straighten_rounded,
                size: 22, color: Colors.white),
          ),
          const SizedBox(width: 14),
          const Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text(
                  'Find my size',
                  style: TextStyle(
                    fontFamily: _font,
                    fontSize: 15.5,
                    fontWeight: FontWeight.w800,
                    color: Colors.white,
                  ),
                ),
                SizedBox(height: 2),
                Text(
                  'Answer 3 quick questions for a fit match',
                  style: TextStyle(
                    fontFamily: _font,
                    fontSize: 12.5,
                    fontWeight: FontWeight.w500,
                    height: 1.3,
                    color: Colors.white70,
                  ),
                ),
              ],
            ),
          ),
          GestureDetector(
            onTap: widget.onFindMySize,
            child: Container(
              padding:
                  const EdgeInsets.symmetric(horizontal: 16, vertical: 9),
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.circular(20),
              ),
              child: const Text(
                'Start',
                style: TextStyle(
                  fontFamily: _font,
                  fontSize: 13.5,
                  fontWeight: FontWeight.w800,
                  color: _brand,
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

The fit helper is the only saturated block on the page, so it pulls the eye after the table. It is a `Container` with 16px corners and a `LinearGradient` from `#FF385C` to a darker `#E0244E` running top-left to bottom-right — the two coral stops give it depth without a shadow. On the left, a 44px circle at `Colors.white.withValues(alpha: 0.18)` holds a white `Icons.straighten_rounded` ruler. The `Expanded` middle stacks 'Find my size' at 15.5px `w800` over 'Answer 3 quick questions for a fit match' at 12.5px in `Colors.white70`, with `height: 1.3` for a tidy wrap. The trailing 'Start' button is a plain `GestureDetector` around a white 20px-radius pill with the label in `_brand` — a colour inversion that reads as the primary action against the gradient. It fires `widget.onFindMySize`; the screen itself has no quiz, so the host app decides where that flow lives.

How-to-measure tips from a nested list

ecom_product_size_guide_screen.dart
  Widget _tips() {
    const List<List<String>> tips = <List<String>>[
      <String>['Chest', 'Measure around the fullest part, under the arms.'],
      <String>['Waist', 'Measure around your natural waistline.'],
      <String>['Length', 'From the shoulder seam down to the hem.'],
    ];
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        const Text(
          'How to measure',
          style: TextStyle(
            fontFamily: _font,
            fontSize: 16,
            fontWeight: FontWeight.w700,
            letterSpacing: -0.2,
            color: _ink,
          ),
        ),
        const SizedBox(height: 14),
        ...tips.map(
          (List<String> t) => Padding(
            padding: const EdgeInsets.only(bottom: 14),
            child: Row(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                const Padding(
                  padding: EdgeInsets.only(top: 1),
                  child: Icon(Icons.straighten_outlined,
                      size: 18, color: _brand),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(
                        t[0],
                        style: const TextStyle(
                          fontFamily: _font,
                          fontSize: 13.5,
                          fontWeight: FontWeight.w700,
                          color: _ink,
                        ),
                      ),
                      const SizedBox(height: 1),
                      Text(
                        t[1],
                        style: const TextStyle(
                          fontFamily: _font,
                          fontSize: 13,
                          fontWeight: FontWeight.w500,
                          height: 1.35,
                          color: _muted,
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }

`_tips` declares its copy as a local `const List<List<String>>` — three `[title, body]` pairs for Chest, Waist and Length, matching the three chart columns in the same order. A 16px `w700` heading 'How to measure' with `letterSpacing: -0.2` sits above, and the rows are produced with `...tips.map(...)` spread into the `Column`. Each row is `crossAxisAlignment: CrossAxisAlignment.start` with a `_brand` `Icons.straighten_outlined` at 18px, pushed down 1px so it lines up with the title's cap height, then a 12px gap and an `Expanded` text column. The title is 13.5px `w700` `_ink`, the instruction 13px `w500` `_muted` with `height: 1.35` so a two-line sentence stays readable. Every row carries `bottom: 14` padding, so the last item's spacing is absorbed by the list's 28px bottom padding rather than a dedicated trailing gap.

Full code

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

import 'package:flutter/material.dart';

/// StyleCart — Size Guide.
///
/// A measurements reference: a unit toggle (cm / in), a size-chart table with
/// crisp rules, body-measuring tips and a "find my size" helper that suggests a
/// size from height + weight.
///
/// Self-contained per CONVENTIONS.md: pure Flutter, bundled Manrope, inline
/// Airbnb-style tokens, own light theme + SafeArea, no images (table + icons).
/// Exposes callbacks only; the registry wires navigation.
class EcomProductSizeGuideScreen extends StatefulWidget {
  const EcomProductSizeGuideScreen({
    super.key,
    this.onClose,
    this.onFindMySize,
  });

  final VoidCallback? onClose;
  final VoidCallback? onFindMySize;

  @override
  State<EcomProductSizeGuideScreen> createState() =>
      _EcomProductSizeGuideScreenState();
}

class _EcomProductSizeGuideScreenState
    extends State<EcomProductSizeGuideScreen> {
  static const String _font = 'Manrope';
  static const Color _canvas = Color(0xFFFFFFFF);
  static const Color _ink = Color(0xFF222222);
  static const Color _muted = Color(0xFF6A6A6A);
  static const Color _brand = Color(0xFFFF385C);
  static const Color _surface = Color(0xFFF2F2F2);
  static const Color _hairline = Color(0xFFEBEBEB);

  // Measurements in cm: [chest, waist, length].
  static const List<String> _sizes = <String>['XS', 'S', 'M', 'L', 'XL', 'XXL'];
  static const List<List<int>> _cm = <List<int>>[
    <int>[88, 72, 68],
    <int>[94, 78, 70],
    <int>[100, 84, 72],
    <int>[106, 90, 74],
    <int>[112, 96, 76],
    <int>[118, 102, 78],
  ];

  bool _cmUnit = true;

  String _v(int cm) =>
      _cmUnit ? '$cm' : (cm / 2.54).toStringAsFixed(1);

  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData.light(useMaterial3: true),
      child: Scaffold(
        backgroundColor: _canvas,
        body: SafeArea(
          child: Column(
            children: <Widget>[
              _header(),
              const Divider(height: 1, color: _hairline),
              Expanded(
                child: ListView(
                  padding: const EdgeInsets.fromLTRB(20, 18, 20, 28),
                  children: <Widget>[
                    _unitToggle(),
                    const SizedBox(height: 18),
                    _table(),
                    const SizedBox(height: 26),
                    _findMySize(),
                    const SizedBox(height: 26),
                    _tips(),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _header() {
    return Padding(
      padding: const EdgeInsets.fromLTRB(8, 4, 16, 4),
      child: Row(
        children: <Widget>[
          const Expanded(
            child: Padding(
              padding: EdgeInsets.only(left: 12),
              child: Text(
                'Size guide',
                style: TextStyle(
                  fontFamily: _font,
                  fontSize: 19,
                  fontWeight: FontWeight.w800,
                  letterSpacing: -0.3,
                  color: _ink,
                ),
              ),
            ),
          ),
          IconButton(
            onPressed: widget.onClose,
            icon: const Icon(Icons.close_rounded, size: 24, color: _ink),
          ),
        ],
      ),
    );
  }

  Widget _unitToggle() {
    return Container(
      height: 42,
      padding: const EdgeInsets.all(4),
      decoration: BoxDecoration(
        color: _surface,
        borderRadius: BorderRadius.circular(12),
      ),
      child: Row(
        children: <Widget>[
          _unitTab('Centimetres', _cmUnit, () => setState(() => _cmUnit = true)),
          _unitTab('Inches', !_cmUnit, () => setState(() => _cmUnit = false)),
        ],
      ),
    );
  }

  Widget _unitTab(String t, bool on, VoidCallback onTap) {
    return Expanded(
      child: GestureDetector(
        onTap: onTap,
        child: Container(
          alignment: Alignment.center,
          decoration: BoxDecoration(
            color: on ? _canvas : Colors.transparent,
            borderRadius: BorderRadius.circular(9),
          ),
          child: Text(
            t,
            style: TextStyle(
              fontFamily: _font,
              fontSize: 13.5,
              fontWeight: FontWeight.w700,
              color: on ? _ink : _muted,
            ),
          ),
        ),
      ),
    );
  }

  Widget _table() {
    return ClipRRect(
      borderRadius: BorderRadius.circular(14),
      child: Table(
        border: TableBorder.symmetric(
          inside: const BorderSide(color: _hairline),
        ),
        columnWidths: const <int, TableColumnWidth>{
          0: FlexColumnWidth(1.1),
          1: FlexColumnWidth(1.3),
          2: FlexColumnWidth(1.3),
          3: FlexColumnWidth(1.3),
        },
        children: <TableRow>[
          TableRow(
            decoration: const BoxDecoration(color: _ink),
            children: <Widget>[
              _th('Size'),
              _th('Chest'),
              _th('Waist'),
              _th('Length'),
            ],
          ),
          ...List<TableRow>.generate(_sizes.length, (int i) {
            return TableRow(
              decoration: BoxDecoration(
                color: i.isEven ? _canvas : _surface.withValues(alpha: 0.5),
              ),
              children: <Widget>[
                _td(_sizes[i], bold: true),
                _td(_v(_cm[i][0])),
                _td(_v(_cm[i][1])),
                _td(_v(_cm[i][2])),
              ],
            );
          }),
        ],
      ),
    );
  }

  Widget _th(String t) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
      child: Text(
        t,
        textAlign: TextAlign.center,
        style: const TextStyle(
          fontFamily: _font,
          fontSize: 12.5,
          fontWeight: FontWeight.w700,
          color: _canvas,
        ),
      ),
    );
  }

  Widget _td(String t, {bool bold = false}) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 8),
      child: Text(
        t,
        textAlign: TextAlign.center,
        style: TextStyle(
          fontFamily: _font,
          fontSize: 13.5,
          fontWeight: bold ? FontWeight.w800 : FontWeight.w500,
          color: _ink,
        ),
      ),
    );
  }

  Widget _findMySize() {
    return Container(
      padding: const EdgeInsets.all(18),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(16),
        gradient: const LinearGradient(
          colors: <Color>[Color(0xFFFF385C), Color(0xFFE0244E)],
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
        ),
      ),
      child: Row(
        children: <Widget>[
          Container(
            width: 44,
            height: 44,
            alignment: Alignment.center,
            decoration: BoxDecoration(
              color: Colors.white.withValues(alpha: 0.18),
              shape: BoxShape.circle,
            ),
            child: const Icon(Icons.straighten_rounded,
                size: 22, color: Colors.white),
          ),
          const SizedBox(width: 14),
          const Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text(
                  'Find my size',
                  style: TextStyle(
                    fontFamily: _font,
                    fontSize: 15.5,
                    fontWeight: FontWeight.w800,
                    color: Colors.white,
                  ),
                ),
                SizedBox(height: 2),
                Text(
                  'Answer 3 quick questions for a fit match',
                  style: TextStyle(
                    fontFamily: _font,
                    fontSize: 12.5,
                    fontWeight: FontWeight.w500,
                    height: 1.3,
                    color: Colors.white70,
                  ),
                ),
              ],
            ),
          ),
          GestureDetector(
            onTap: widget.onFindMySize,
            child: Container(
              padding:
                  const EdgeInsets.symmetric(horizontal: 16, vertical: 9),
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.circular(20),
              ),
              child: const Text(
                'Start',
                style: TextStyle(
                  fontFamily: _font,
                  fontSize: 13.5,
                  fontWeight: FontWeight.w800,
                  color: _brand,
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _tips() {
    const List<List<String>> tips = <List<String>>[
      <String>['Chest', 'Measure around the fullest part, under the arms.'],
      <String>['Waist', 'Measure around your natural waistline.'],
      <String>['Length', 'From the shoulder seam down to the hem.'],
    ];
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        const Text(
          'How to measure',
          style: TextStyle(
            fontFamily: _font,
            fontSize: 16,
            fontWeight: FontWeight.w700,
            letterSpacing: -0.2,
            color: _ink,
          ),
        ),
        const SizedBox(height: 14),
        ...tips.map(
          (List<String> t) => Padding(
            padding: const EdgeInsets.only(bottom: 14),
            child: Row(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                const Padding(
                  padding: EdgeInsets.only(top: 1),
                  child: Icon(Icons.straighten_outlined,
                      size: 18, color: _brand),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(
                        t[0],
                        style: const TextStyle(
                          fontFamily: _font,
                          fontSize: 13.5,
                          fontWeight: FontWeight.w700,
                          color: _ink,
                        ),
                      ),
                      const SizedBox(height: 1),
                      Text(
                        t[1],
                        style: const TextStyle(
                          fontFamily: _font,
                          fontSize: 13,
                          fontWeight: FontWeight.w500,
                          height: 1.35,
                          color: _muted,
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }
}

Plus bundled 5 binary assets (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 ecom-product-size-guide

2. AI agent (MCP)

Connect FlutterKit's MCP server in Claude or Cursor and just ask your agent to install ecom-product-size-guide — it fetches and writes the files for you.

FAQ

Can I ship this size guide in a commercial app?

Yes. FlutterKit screens are free under MIT-style terms for personal and commercial projects alike — there is no key to enter and no attribution required. Copy the code from this page or run `flutterkit add ecom-product-size-guide` and use it in your storefront.

How do I load the chart from my own product data?

Replace the static `_sizes` and `_cm` lists with constructor parameters — for example `List<String> sizes` and `List<List<int>> measurementsCm` — and keep everything in centimetres. `_v()` already handles the inch conversion at render time, so the toggle keeps working with no further change. If a product has different columns (say, hip instead of length), pass the header labels too and generate the `_th` cells from them.

Why does the inch value show one decimal but centimetres none?

Garment charts are published as whole centimetres, so `_v()` returns the integer unchanged when `_cmUnit` is true. Dividing by 2.54 produces values like 34.645..., which would clutter the table, so the inch branch calls `toStringAsFixed(1)`. If your brand rounds to the nearest half inch, swap that line for a small rounding helper.

Does it need any pub packages or extra fonts?

No packages — it is pure Flutter using `Table`, `ClipRRect`, `LinearGradient` and Material icons. The only asset is the Manrope font family, which `flutterkit add ecom-product-size-guide` bundles and registers in your pubspec for you.

Which Flutter version does this need?

Flutter 3.22 or newer, because the zebra rows and the ruler badge use `Color.withValues(alpha: ...)` and the constructor uses `super.key`. On an older SDK change those calls to `withOpacity(0.5)` and `withOpacity(0.18)`, and write the constructor as `{Key? key, ...} : super(key: key)`.

Related screens