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
SVG sourcesOpenType fontlock fileconst 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.
Install GlyphPact
uv tool install glyphpactInstalls the latest stable release from PyPI, currently v1.1.0. Already installed? Run
uv tool upgrade glyphpact. Release notes.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.
glyphpact assets/icons \ --output lib/generated/app_icons \ --name AppIcons
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 the
pubspec.yamlthat declares them, and thefamilymust equal the configured font family.flutter: fonts: - family: AppIcons fonts: - asset: lib/generated/app_icons/fonts/AppIcons.otfUse the generated constants
Generated
IconDatacarries no label. Supply a localizedsemanticLabelfor meaningful icons, use a labelledIconButton, or exclude decorative ones from semantics.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.
// 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.
{
"catalog": true
}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.
# 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.
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:
{
"icons": {
"status/verified_layers.svg": {
"partialAlpha": {
"mode": "layers",
"fallback": "silhouette"
}
}
}
}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.otfThe generated Dart adds a layered widget and layer descriptors, and the plain IconData remains available as the fallback you explicitly chose:
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.
{
"$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.
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.
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
- Flutter adoption guideApp fonts, package fonts, layered icons, CI, and accessibility notes.
- Supported SVG profileThe versioned contract for exactly which SVG features compile.
- Bulk SVG to Flutter iconsRecursive folders, audits, automation, and large-pack boundaries.
- How codepoint stability worksWhy assignments move in other tools, and what the lock file does about it.
- Compared with FlutterIcon.comAn honest look at the hosted alternative, and where it wins.