woff2
WOFF and WOFF2 font support for Flutter. Use .woff2 / .woff
web-font files directly in your Flutter app, the same way you would
use a .ttf or .otf — in Text(...), TextStyle(fontFamily: ...),
themes, and Material widgets.
Why this package?
Flutter's
FontLoaderonly accepts uncompressed SFNT (TTF/OTF). If you try to load a.woff2file viaFontLoader.addFont(...)and use it inText(...), the glyphs render as□ □ □(tofu) — the engine silently rejects the unsupported container. There's no built-in WOFF/WOFF2 decoder in Dart or Flutter.This package is the missing piece. It decodes WOFF1 and WOFF2 in pure Dart, then hands the resulting TTF/OTF bytes to Flutter's
FontLoaderfor you. Drop in your.woff2file, call one function, andText(..., style: TextStyle(fontFamily: 'YourFont'))just works.
Features
- 🅰️ One-call asset loader —
loadWoffFontFromAsset(...)takes afont-familyname and an asset path, and you're done. - 🗜️ Decodes WOFF1 and WOFF2 to SFNT (TTF/OTF) entirely in Dart.
WOFF1 uses
dart:io's zlib; WOFF2 uses Brotli decompression and implements the full W3Cglyf/loca/hmtxtransformations. - 🎨 Use in
Text,TextStyle,ThemeData— once registered, the font is a normal Flutter font family. Works with Material 3 themes, custom widgets, anything that takes afontFamilystring. - 📰
@font-faceCSS parser — extractfont-family,font-weight,font-style,src, andformatfrom a CSS string. Useful for SVG, HTML, and EPUB renderers that bundle styles with their content. - 📦 Batch-register multiple weights/styles of the same family
using
WoffFontRegistry. - 🌐 Embedded
data:URL fonts —data:font/woff2;base64,...works out of the box. - 🪶 No native plugins, no FFI for your app code. The package is
pure Dart (Brotli native lib is bundled by the
es_compressiondependency). - 🧪 Tested — 35 unit tests covering round-trip encoding for both WOFF1 (raw + zlib) and WOFF2 (Brotli, non-transformed), plus edge cases (malformed headers, truncated input, TTC collections).
Install
dependencies:
woff2: ^0.1.0
import 'package:woff2/woff2.dart';
Quick start
The most common case: you have a .woff2 in assets/fonts/, you want
to use it in a Text widget.
1. Declare the asset
pubspec.yaml:
flutter:
assets:
- assets/fonts/Inter.woff2
Note: do not put it under
flutter: fonts:— that key is for Flutter's built-in TTF/OTF loader, which won't accept WOFF/WOFF2. List it as a regularassets:entry instead.
2. Load the font at app start
import 'package:flutter/material.dart';
import 'package:woff2/woff2.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await loadWoffFontFromAsset(
fontFamily: 'Inter',
assetPath: 'assets/fonts/Inter.woff2',
);
runApp(const MyApp());
}
3. Use it like any Flutter font
Text(
'Hello, WOFF2!',
style: TextStyle(fontFamily: 'Inter', fontSize: 24),
)
That's it.
Loading from bytes (network, file system, RAM)
When you already have the font bytes — fetched over HTTP, read from
getApplicationDocumentsDirectory(), or built at runtime:
final response = await http.get(Uri.parse('https://example.com/font.woff2'));
await loadWoffFontFromBytes(
fontFamily: 'Remote',
bytes: response.bodyBytes,
);
Multiple weights and styles
Use WoffFontRegistry for batch registration, typically when the
variants come from a CSS stylesheet:
final registry = WoffFontRegistry();
await registry.registerFonts(
[
const CssFontFaceRule(
fontFamily: 'Inter',
fontWeight: '400',
src: 'fonts/Inter-Regular.woff2',
format: 'woff2',
),
const CssFontFaceRule(
fontFamily: 'Inter',
fontWeight: '700',
src: 'fonts/Inter-Bold.woff2',
format: 'woff2',
),
],
srcResolver: (src) async {
final data = await rootBundle.load('assets/$src');
return data.buffer.asUint8List();
},
);
if (registry.errors.isNotEmpty) {
debugPrint('Font errors: ${registry.errors}');
}
Parsing @font-face blocks from CSS
If you have CSS text — extracted from a <style> tag in an SVG, an
embedded HTML email, an EPUB stylesheet, etc. — pass it through
extractFontFaceRules:
const css = '''
@font-face {
font-family: 'Inter';
font-weight: 400;
src: url('fonts/Inter.woff2') format('woff2');
}
''';
final rules = extractFontFaceRules(css);
await registry.registerFonts(rules, srcResolver: ...);
The parser handles the awkward bits:
data:URLs with embedded semicolons (data:font/ttf;charset=utf-8;base64,...)- HTML-encoded quotes inside
font-family("My Font") - Keyword
font-weightvalues (normal,bold,lighter,bolder) - Single- and double-quoted strings
url(...)with and without quotes
Embedded data: URL fonts
Need to ship a font without a separate asset file? Use a data: URL
in your CssFontFaceRule:
const rule = CssFontFaceRule(
fontFamily: 'Inline',
src: 'data:font/woff2;base64,d09GMgABAAAAAAo...',
format: 'woff2',
);
await registry.registerFonts([rule]);
Low-level decoder
If you don't need the Flutter wiring — for example you're writing your own font pipeline or doing offline conversion — call the decoder directly:
import 'package:woff2/woff2.dart';
final (result, sfntBytes) = decodeFontIfWoff(bytes);
switch (result) {
case WoffDecodeResult.notWoff:
// Input was plain TTF/OTF — use `bytes` as-is.
case WoffDecodeResult.ok:
// `sfntBytes` is non-null, ready for any SFNT consumer.
case WoffDecodeResult.woff2Unsupported:
// WOFF2 TTC collection — see Limitations below.
case WoffDecodeResult.malformed:
// Bad input.
}
API reference
| Symbol | Purpose |
|---|---|
loadWoffFontFromAsset |
One-call helper: bundled .woff2 asset → TextStyle.fontFamily |
loadWoffFontFromBytes |
Same, but from raw bytes |
WoffFontRegistry |
Batch registration with weight/style variants |
CssFontFaceRule |
Value object for a parsed @font-face block |
extractFontFaceRules |
Pull all @font-face rules out of a CSS string |
WoffSrcResolver |
Callback used by the registry to fetch url(...) bytes |
decodeFontIfWoff |
Low-level: Uint8List → (WoffDecodeResult, Uint8List?) |
WoffDecodeResult |
notWoff, ok, woff2Unsupported, malformed |
Limitations
- WOFF2 collections (TTC) are not supported. A
.woff2whoseflavorfield is'ttcf'will be reported asWoffDecodeResult.malformed. TTC support requires reconstructing multiple sub-fonts from one sharedglyfstream and is a follow-up item. - Web platform: the underlying Brotli decoder (from
es_compression) is FFI-based, which means this package runs on Android, iOS, macOS, Linux, and Windows — but not Flutter Web. On the web, your browser already understands.woff2natively, so you don't need this package there; use the standard@font-faceCSS inweb/index.htmlinstead. - Variable fonts (with
fvar/gvartables) decode correctly on the SFNT level, but Flutter'sFontLoaderdoesn't currently expose the variable axes to widget styling. The font will load and use its default named instance.
Tested platforms
The decoder is tested with self-contained round-trip suites:
- WOFF1 with raw tables (
compLength == origLength) - WOFF1 with zlib-compressed tables
- WOFF2 with non-transformed tables (full glyf/loca/hmtx transformations are exercised by integration tests in the parent monorepo against real font files)
$ flutter test
00:01 +35: All tests passed!
Related packages
This package was extracted from
flutter_full_svg_support, a Flutter renderer for animated
SVGs that needed reliable WOFF/WOFF2 decoding to handle @font-face
rules inside SVG files. If you're rendering SVGs that ship their own
fonts, you already get this package transitively.
The same monorepo also ships quickjs_engine — a modern QuickJS-NG JavaScript runtime for Flutter — if you need bundled JS execution.
Contributing
Bugs and PRs welcome at
github.com/denisnadey/flutter_full_svg_support. The package
source lives at packages/woff2/ in that monorepo.
License
Apache License 2.0 — see LICENSE.
Keywords: Flutter WOFF, Flutter WOFF2, Flutter web fonts,
load WOFF2 in Flutter, decode WOFF2 Dart, Flutter custom fonts
WOFF2, @font-face Flutter, FontLoader WOFF, Flutter TTF from WOFF,
Brotli font decoder Dart.
Libraries
- woff2
- Public API of the
woff2package.