Skip to content
GlyphPact
Menu

Flutter integration

Custom Flutter icons that keep their codepoints

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

From a directory of SVGs to a working Icon widget

  1. Install GlyphPact

    uv tool install glyphpact

    Installs the latest stable release from PyPI, currently v1.0.1. 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.0.1.

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,
  );
}

Tree shaking, precisely

The provider is emitted the way Flutter's icon subsetting requires: anabstract final class annotated@flutter.staticIconProvider, containing onlyconst IconData values whose codepoint, family, and package are compile-time constants.

That annotation is a promise the analyzer enforces, namely that noIconData in the class is constructed dynamically. Flutter needs that guarantee before it can drop unused glyphs from the font.

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. 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. Schema version 2.
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",
  "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.

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.0.1
      # 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.

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.

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. Two of the three outputs are framework-neutral: the compiled OpenType/CFF font is a standard .otf file that any OpenType consumer can use, and iconfont.lock.json is a documented JSON registry mapping each source file to its codepoint, which any language can read.

The third output is not neutral. The generated high-level binding is a Dart file containing const Flutter IconData constants, and the flags that shape it are Dart-specific. There is no generated binding for any other platform.

So a non-Flutter project can consume the font and read codepoints from the lock file, but it will be writing its own integration layer. GlyphPact does not currently ship a ready-made web, React, Android, or JavaScript integration.

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, holding only const IconData values whose codepoint, font family, and font package are compile-time constants.

That annotation exists so Flutter can statically verify no IconData is constructed dynamically, which is the condition its font-subsetting step needs. Whether subsetting actually runs is decided by your build: it applies to release builds and can be disabled with --no-tree-shake-icons. Passing non-constant data to Icon elsewhere in your app is what typically defeats it.

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. For a larger catalogue, start allocation in a supplementary private use area with --start-codepoint 0xF0000, which provides 65,534 slots. Beyond that, split the catalogue into several independently versioned fonts.