Skip to content
GlyphPact
Menu

Flutter integration

Use a custom icon font in Flutter with const IconData

GlyphPact compiles an SVG directory into an OpenType font plus a tree-shakeable const IconData class, and records every codepoint in a file you commit. Adding icons later does not renumber the ones your widgets already reference.

  • Python 3.10 or newer
  • Dart 3 or newer, to consume the generated provider
  • No Node toolchain
Compile pipelineFlutter-ready output
Repository contract
  1. SVG sources
  2. OpenType font
  3. lock file
  4. const IconData

Same inputs. Same codepoints. Reviewable output.

Quick start

Register the generated font and provider in your app

Need recursive folder conversion, pack auditing, sharding, and MCP? Start with the bulk SVG guide.

  1. Install GlyphPact

    uv tool install glyphpact

    Installs the latest stable release from PyPI, currently v1.1.0. Already installed? Run uv tool upgrade glyphpact. Release notes.

  2. Compile the pack into your app

    Sources are discovered recursively. GlyphPact takes ownership of the output directory and will not overwrite a non-empty directory that lacks its marker, so point it somewhere generated.

    bash
    glyphpact assets/icons \
      --output lib/generated/app_icons \
      --name AppIcons
  3. Register the font in pubspec.yaml

    This step is yours: GlyphPact writes the font but does not edit your pubspec. Asset paths are relative to thepubspec.yaml that declares them, and thefamily must equal the configured font family.

    pubspec.yaml
    flutter:
      fonts:
        - family: AppIcons
          fonts:
            - asset: lib/generated/app_icons/fonts/AppIcons.otf
  4. Use the generated constants

    Generated IconData carries no label. Supply a localized semanticLabel for meaningful icons, use a labelled IconButton, or exclude decorative ones from semantics.

    dart
    import 'package:my_app/generated/app_icons/app_icons.dart';
    
    // Meaningful icon: give it a localized label.
    Icon(
      AppIcons.back,
      semanticLabel: 'Back',
    )
    
    // Decorative icon: keep it out of the semantics tree.
    ExcludeSemantics(
      child: Icon(AppIcons.verified),
    )

Generated Dart

What the provider looks like

Ordinary Dart, checked into your repository, reviewable in a pull request. Every constant records the source file it came from and any licence you declared for it.

lib/generated/app_icons/app_icons.dart
// GENERATED CODE - DO NOT MODIFY BY HAND.
// Generated by GlyphPact 1.1.0.

import 'package:flutter/widgets.dart' as flutter;

/// Constant [flutter.IconData] values backed by the AppIcons font.
@flutter.staticIconProvider
abstract final class AppIcons {
  static const String _fontFamily = 'AppIcons';
  static const String? _fontPackage = null;

  /// Source: arrows/back.svg
  /// License: MIT
  static const flutter.IconData back = flutter.IconData(
    0xE000,
    fontFamily: _fontFamily,
    fontPackage: _fontPackage,
    matchTextDirection: true,
  );

  /// Source: status/verified.svg
  /// License: MIT
  static const flutter.IconData verified = flutter.IconData(
    0xE001,
    fontFamily: _fontFamily,
    fontPackage: _fontPackage,
  );
}

Enumerate the generated API without maintaining a second list

Set catalog in the checked-in config when a gallery, picker, or coverage test needs every icon by its Dart name. GlyphPact adds a separate AppIconsCatalog companion to the existing generated Dart file.

icon_font.json
{
  "catalog": true
}
dart
for (final entry in AppIconsCatalog.byName.entries) {
  gallery.add(
    IconCard(name: entry.key, icon: entry.value),
  );
}

AppIconsCatalog.byName is a static const map containing every base glyph in ascending codepoint order. Packs containing partial-alpha icons also receive layeredByName, which contains only their lossless layered descriptors. Their static-const descriptor provider is annotated separately so a direct reference to one layered icon does not retain its siblings.

Generated regions whose cross-version layout differs remain stable under dart format. The narrow controls cover the catalog, layered descriptors, and an over-width base provider when needed. Dart 3.0 through 3.6 formatters ignore them but receive legacy-canonical layout; Dart 3.7 and later honor them.

Tree shaking, precisely

The base provider is emitted the way Flutter's icon subsetting requires: an abstract final class annotated@flutter.staticIconProvider with only static const members. Partial-alpha packs apply the same annotation to their static-const descriptor provider. The catalog companion also has only static const members and carries the provider annotation so unreachable catalog declarations are ignored.

The optional catalog remains outside the base provider namespace. Its annotation suppresses declaration-only constants, but reachable map values remain visible to Flutter. Enabling it does not itself enlarge a release: when the catalog is unreachable, Flutter removes it and subsets from the individual provider constants used by the app.

Making byName reachable retains every base glyph. Making layeredByName reachable retains those icons' fallbacks and layer-font glyphs. A catalog imported only by tests has no release cost.

What GlyphPact does and does not control. It emits a provider that satisfies the contract. Whether subsetting actually runs is your build's decision: it applies to release builds and is disabled by --no-tree-shake-icons. ConstructingIconData from a variable anywhere else in your app is the usual reason subsetting gets switched off.

Source control

Commit the whole owned output

The lock is the codepoint ABI. Losing it can reassign icons even when no source file changed, so it belongs in Git with everything else.

lib/generated/app_icons/
fonts/AppIcons.otfframework-neutral Validated OpenType/CFF font. Framework-neutral.
iconfont.lock.jsonframework-neutral Codepoint registry and provenance. Schema version 1. Commit this.
app_icons.dartFlutter only const Flutter IconData provider and optional name-keyed catalog. Dart-specific.
layer_fonts/layer_*.otfframework-neutral Optional solid-alpha paint-order layers, for icons that opt in.
iconfont.report.jsonbuild metadata Deterministic build report and stable code-generation input. Schema version 3.
ATTRIBUTION.mdbuild metadata Declared artwork authorship, licences, and unattributed count.
.glyphpact.jsonbuild metadata Ownership marker. GlyphPact refuses to overwrite a directory without it.
../.app_icons.glyphpact.lockPublication coordination file. A sibling of the output directory, not part of the owned tree. Leave it in place and ignore it in version control.
.gitignore
# GlyphPact leaves this coordination file beside the output directory.
lib/generated/.app_icons.glyphpact.lock

Shipping icons from a Dart package

When the font lives in a package rather than the application, pass the package name. The generated IconData constants then carryfontPackage, which is how Flutter resolves the asset from a dependency.

bash
glyphpact assets/icons \
  --output lib/generated/app_icons \
  --name AppIcons \
  --font-package my_icon_package

Keep the family entry in the package's ownpubspec.yaml equal to the configured font family, or Flutter will not find the glyphs at runtime.

Icons that genuinely need more than one alpha value

A single icon-font glyph stores one coverage mask, so it cannot hold two different opacities at once. Rather than silently flattening such an icon, GlyphPact can represent it as up to eight ordered solid-alpha outlines, each emitted into an auxiliary font family at the icon's unchanged codepoint.

Opt in per icon, in the config:

icon_font.json
{
  "icons": {
    "status/verified_layers.svg": {
      "partialAlpha": {
        "mode": "layers",
        "fallback": "silhouette"
      }
    }
  }
}
pubspec.yaml
flutter:
  fonts:
    - family: AppIcons
      fonts:
        - asset: lib/generated/app_icons/fonts/AppIcons.otf
    - family: AppIcons Layer 1
      fonts:
        - asset: lib/generated/app_icons/layer_fonts/layer_1.otf
    - family: AppIcons Layer 2
      fonts:
        - asset: lib/generated/app_icons/layer_fonts/layer_2.otf

The generated Dart adds a layered widget and layer descriptors, and the plain IconData remains available as the fallback you explicitly chose:

dart
AppIconsLayeredIcon(
  AppIconsLayers.verifiedLayers,
  size: 24,
  color: const Color(0xFF222222),
  semanticLabel: 'Verified',
)

The report records every layer's paint order, opacity, family, file, codepoint, and bounds. Seethe runnable layered example.

Check in a config instead of memorising flags

For anything you build more than once, a JSON config beats a shell command. Paths resolve relative to the config file, CLI flags override config values, and jobs: 0 selects up to eight bounded worker processes.

icon_font.json
{
  "$schema": "https://raw.githubusercontent.com/omar-hanafy/glyphpact/main/schema/icon-font-config.schema.json",
  "input": "assets/icons",
  "output": "lib/generated/app_icons",
  "fontFamily": "AppIcons",
  "className": "AppIcons",
  "catalog": true,
  "fontPackage": null,
  "startCodepoint": "0xE000",
  "unitsPerEm": 1000,
  "padding": 0,
  "clipToViewBox": true,
  "policy": {
    "lossy": "error",
    "unrepresentable": "error"
  },
  "jobs": 0,
  "icons": {
    "arrows/back.svg": {
      "name": "back",
      "matchTextDirection": true,
      "author": "Your team",
      "license": "MIT"
    }
  }
}

Per-icon entries are also where provenance goes.author, license, sourceUrl, andcopyright flow into the generatedATTRIBUTION.md, which also reports how many emitted icons have no declared provenance at all.

Use the report for a different generated shape

The built-in catalog is the standard Dart enumeration API. Report schema v3 is the stable lower-level input for another collection, order, variable name, or language. It records the font and Dart provider fields plus every emitted glyph's source, Dart name, codepoint, directionality, and layered metadata. It also recordscodepointsRemaining andrangeUtilization for allocation planning. The schema still validates historical v1 and v2 reports, but a custom generator must recognize v3 before upgrading to GlyphPact 1.1.

Check schemaVersion before generating. Report codepoints are 0x... strings. Parse them only in build-time generators and artifact tests; generated Dart should reference the reported provider constants instead of constructingIconData dynamically.

dart
final raw = reportGlyph['codepoint'] as String; // "0xE000"
final codepoint = int.parse(raw.substring(2), radix: 16);

GlyphPact owns the entire configured output directory. Write custom generated files elsewhere, run that generator after GlyphPact, and give it its own non-rewriting drift check.

Make CI fail when the committed font goes stale

Generated artifacts drift. Someone edits an SVG and does not regenerate; someone regenerates and does not commit. Without a check, the repository quietly holds a font built from an older pack.

.github/workflows/ci.yml
name: CI
on: [push, pull_request]

jobs:
  icons:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
      - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
      - run: uv tool install glyphpact==1.1.0
      # Exit code 3 means the committed artifacts are stale.
      - run: glyphpact --config icon_font.json --check --json

--check rebuilds a candidate set and compares it without rewriting the owned output. It may create the output's parent directory and leaves the sibling coordination file in place. With--json, inspect quality,policy, glyph counts, and typed issues rather than treating every zero exit as equivalent.

Large icon sets

One font holds at most 65,534 usable glyphs, an OpenType indexing limit GlyphPact enforces as a per-build ceiling. In practice the allocation range binds first: the default BMP private use area U+E000 - U+F8FF provides 6,400 lifetime slots, and tombstones consume them too because codepoints are never recycled.

Report schema v3 exposes codepointsRemaining andrangeUtilization. At or above 80% utilization, build and check emitCODEPOINT_RANGE_NEAR_EXHAUSTION while still succeeding, giving CI time to plan a new stable shard before allocation stops.

For a larger catalogue, allocate from a supplementary private use area with --start-codepoint 0xF0000, which provides 65,534 slots. Beyond that, split into several independently versioned fonts.

Compile time depends far more on SVG complexity than on file count, so GlyphPact publishes no private-corpus headline number. Thebenchmark guide provides a reproducible local runner for measuring your own pack instead.

The bulk SVG-to-Flutter guidecovers recursive discovery, CI, MCP automation, and the sharding plan for catalogues that cannot fit in one font.

Reference

Go deeper

FAQ

Flutter questions

Should iconfont.lock.json be committed to Git?

Yes. The lock file is the codepoint ABI for the generated font, and it only protects existing assignments if the next build can read the same file the previous build wrote. Committing it alongside the generated font, Dart provider, report, and attribution file is the intended workflow.

If the lock is deleted or lost, the next build has no record of prior assignments and will allocate codepoints from scratch, which can reassign icons even when no source file changed.

How can CI detect stale generated icon output?

Run the same GlyphPact build with the --check flag: glyphpact --config icon_font.json --check. It rebuilds a candidate artifact set from the current sources and compares it against the committed output without rewriting the owned output directory.

Exit code 0 means the committed artifacts are current, and exit code 3 means they are stale, which fails the job. Adding --json produces a stable machine-readable result at schema version 2 so a workflow can inspect glyph counts, the active policy, and typed issues rather than treating every successful run as equivalent.

CI setup for Flutter projects

Can I use the generated font outside Flutter?

Partly, and the boundary is worth stating precisely. The compiled OpenType/CFF font, the lock registry, and iconfont.report.json are framework-neutral. Report schema v3 records every emitted glyph name and codepoint plus the font family, file, hashes, layered metadata, and remaining allocation capacity, so it is the stable lower-level input for another language's generator.

The generated high-level binding is Dart-specific: it contains const Flutter IconData values and, when enabled, a name-keyed catalog companion. GlyphPact does not generate a ready-made web, React, Android, or JavaScript binding.

A non-Flutter project can consume the font and generate its own integration from the report. That custom output must live outside GlyphPact's owned output directory and needs its own drift check.

Should I use an icon font or flutter_svg?

Use an icon font when the artwork is monochrome, is reused throughout the interface, and should behave like text through IconData, IconTheme, and font subsetting. Use flutter_svg when an image needs multiple colours, gradients, filters, animation, or other SVG semantics that a monochrome glyph cannot preserve.

For mixed packs, audit first and keep unsuitable artwork as SVG assets. Converting every file is not the goal; choosing a faithful runtime representation is.

Can Flutter code enumerate every generated icon by name?

Yes. Set catalog to true in the checked-in GlyphPact config. The existing generated Dart file then includes a separate AppIconsCatalog companion whose static const byName map contains every emitted IconData in ascending codepoint order. Packs with partial-alpha icons also receive layeredByName for their lossless layered descriptors.

The config switch alone does not enlarge a release. If the catalog is unreachable, Flutter removes it and subsets the font from the individual provider constants the app uses. Reachable byName retains every base glyph, while reachable layeredByName retains those icons' fallbacks and layer-font glyphs. A catalog referenced only from tests has no release cost.

For another collection shape, order, variable name, or language, use report schema v3 as a build-time input. Report codepoints are 0x... strings, and codepointsRemaining plus rangeUtilization expose the current allocation headroom. Generated Dart should reference the reported provider constants rather than constructing IconData dynamically, and custom generated files must stay outside GlyphPact's owned output directory.

Flutter integration details

Does the generated Flutter code support icon tree shaking?

The generated provider follows the contract Flutter requires for it. It is emitted as an abstract final class annotated @staticIconProvider and has only static const members, including the private font-family strings and IconData values. Partial-alpha packs apply the same annotation to their static-const layered descriptor provider so directly referenced descriptors can be subset independently.

Whether subsetting runs is decided by the release build and can be disabled with --no-tree-shake-icons. The optional catalog companion has only static const members and carries the provider annotation so declaration-only catalogs in this or another package are ignored. Reachable map values remain visible: an unused catalog is removed, while a reachable byName map retains every base glyph it enumerates.

Flutter integration details

How many icons can one GlyphPact font hold?

The practical ceiling for a single font is 65,534 usable glyphs, which is an OpenType glyph-indexing limit that GlyphPact enforces as a per-build icon ceiling.

The allocation range is the tighter constraint in the default configuration. The BMP private use area, U+E000 through U+F8FF, provides 6,400 lifetime slots, and active icons and tombstones both consume them because codepoints are never recycled. Report schema v3 exposes codepointsRemaining and rangeUtilization, and builds warn at 80% utilization. For a larger catalogue, start a new stable font in a supplementary private use area with --start-codepoint 0xF0000, which provides 65,534 slots. Never change the start codepoint of an established lock; beyond one range, split the catalogue into independently versioned fonts.