@city-of-baltimore/bromo-fonts
The font adapter for the Baltimore Design System. It registers the families
named by BROMO_FONT_CONTRACT so every product inherits one delivery
mechanism instead of each solving it.
npm install --save-exact @city-of-baltimore/bromo-fonts
// once, near your application entry, before anything renders.
// Pick exactly ONE of these two.
import "@city-of-baltimore/bromo-fonts/brand"; // City brand headings
// import "@city-of-baltimore/bromo-fonts/fonts.css"; // network-free
No @font-face or font URLs belong in application code. Optional native-language
labels add only the relevant script entry described below.
Which one? fonts.css requests faces only from the application origin and renders headings in Hanken
Grotesk. brand is a superset that adds the City's brand face,
Proxima Nova, ahead of it — at the cost of a request to Adobe on every page
load. Both are supported; the network-free one is not a degraded mode.
Why this is not part of Bromo
The public @city-of-baltimore/bromo package is MIT and network-free by
policy: it declares no @font-face, ships no font binary, and contacts no
font host. verify:package-policy and verify:pack-install enforce that
mechanically, at zero exceptions.
Bromo owns which families are correct. This adapter owns how they arrive. Keeping them apart is what lets delivery change — a new licence, a new host, self-hosting instead of a CDN — without touching the design system, and what guarantees a licensed binary can never land in an MIT tarball.
What it delivers
| Role | Family | Licence | Delivery | In which entry |
|---|---|---|---|---|
| Brand headings | proxima-nova 700/800 | Adobe web project (delivery-only) | Adobe CDN | brand only |
| Display and headings | Hanken Grotesk 700/800 | SIL OFL-1.1 | self-hosted, bundled at build time | both |
| Body and UI | Nunito Sans 400/700/800 | SIL OFL-1.1 | self-hosted, bundled at build time | both |
The two self-hosted families are redistributable, so their files ship inside the
@fontsource dependencies and are bundled into your application's own output.
This package ships no font binary of its own.
Latin subsets only. The default adapter imports latin and latin-ext,
not the full unicode-range set. Cyrillic and Vietnamese are outside that entry;
Storybook registers their pinned Nunito faces through its complete upstream
stylesheets. Optional script entries are listed below. Applications do not edit
fonts.css.
Proxima Nova cannot be bundled: the City holds it under an Adobe Fonts web project, a delivery-only licence that permits rendering from Adobe's CDN and does not grant self-hosting. It is the estate's one documented exception to the self-host rule, and it is bounded — headings only, opt-in, with the self-hosted Hanken Grotesk directly behind it so an Adobe outage costs one tier rather than all of them.
Only h1–h6, the brand lockup, and Alert titles use the heading stack;
everything else is the body face.
Optional native-language labels
Typography contract 6 adds these normal weight 700 entries while keeping the
base stacks and fonts.css declarations unchanged. They cover current native
labels; they do not claim complete translated workflows, arbitrary weights or
all regional forms.
| Import suffix | Named face | Explicit language attributes |
|---|---|---|
scripts/zh.css | Noto Sans SC | zh or zh-Hans (including zh-Hans-*) |
scripts/ko.css | Noto Sans KR | ko and regional subtags |
scripts/ar.css | Noto Sans Arabic | ar and regional subtags |
scripts/hi.css | Noto Sans Devanagari | hi and regional subtags |
// In the existing lazy language-assistance feature, not every application's entry.
import "@city-of-baltimore/bromo-fonts/scripts/ar.css";
<Tag label={<>Arabic · <bdi lang="ar" dir="auto">العربية</bdi></>} />
Keep the existing base font import. The adapter styles explicitly language-tagged
text; it neither guesses a language from characters nor rewrites a global font
stack. Put lang on the native text only when an English descriptor accompanies
it. LanguageAccessPanel also accepts { label, lang, dir? } items, retaining
legacy strings and label-based selection/callbacks. Its structured labels use
bdi, defaulting to dir="auto". Labels must remain unique.
The bare zh choice is the SC face for today's shared 中文 label. It does not
claim Traditional Chinese coverage policy; zh-Hant does not select this entry.
Use this 700-only delivery for governed labels, not 400-weight translated body
copy. An expanded weight or language requirement needs an explicit contract change.
Each script entry needs its Fontsource family, which the adapter declares as an optional peer at the exact version its CSS was generated from. Install the families for the entries you import, and only those:
| Import suffix | Install |
|---|---|
scripts/zh.css | @fontsource/[email protected] |
scripts/ko.css | @fontsource/[email protected] |
scripts/ar.css | @fontsource/[email protected] |
scripts/hi.css | @fontsource/[email protected] |
npm install --save-exact @fontsource/[email protected]
An application that imports no script entry installs none of them (about 4.8 MB for the adapter and its base faces, against about 133 MB with all four). A missing family fails the build — the bundler cannot resolve the face — rather than painting an OS fallback.
All faces come from exact Fontsource 5.3.0 packages. The generator preserves upstream Unicode ranges, complete script shards and unmodified shaping data; it selects WOFF2 only. It never creates a font from screenshot characters. Only required Unicode shards should download.
The package includes upstream notices in licenses/. Preserve the relevant OFL
notices when deploying bundled font assets. The adapter code's MIT license does
not replace the fonts' OFL license. See the typography guide for cost estimates
and remaining browser/device acceptance; registered faces alone do not prove
which face painted a glyph.
CSP
fonts.css and the optional script entries use same-origin assets:
font-src 'self';
Apps on this entry can remove https://use.typekit.net,
https://fonts.googleapis.com, and https://fonts.gstatic.com from their
style-src and font-src directives entirely.
brand attaches Adobe's stylesheet at runtime rather than importing it, so
it needs Adobe reachable for both the stylesheet and the faces:
style-src 'self' https://use.typekit.net;
font-src 'self' https://use.typekit.net https://p.typekit.net;
Nothing else changes — fonts.googleapis.com and fonts.gstatic.com stay out
of both.
Why the brand entry is JavaScript
Because a stylesheet cannot stay off the critical rendering path, and that is the only property that matters here.
The optional brand entry attaches Adobe's stylesheet with media="print",
then changes it to all on load through addEventListener. This keeps the
stylesheet off the initial render-blocking path and preserves a same-origin
self-hosted fallback. It does not change Adobe's font descriptor or guarantee
that a later swap is imperceptible. The approved heading weights remain 700
and 800; project configuration and licensed delivery stay with the adapter's
owner.
Licences
This package is MIT. The fonts it delivers keep their own licence; the self-hosted faces permit the redistribution this adapter performs:
-
Nunito Sans — SIL Open Font License 1.1, via
@fontsource/nunito-sans. -
Hanken Grotesk — SIL Open Font License 1.1, via
@fontsource/hanken-grotesk. -
Noto Sans SC, Noto Sans KR, Noto Sans Arabic and Noto Sans Devanagari — SIL Open Font License 1.1, through the corresponding exact Fontsource packages.
On Proxima Nova
Proxima Nova is delivered only by the separate, deliberate brand opt-in entry.
It is never in fonts.css, optional script entries or Bromo's visual baseline
environment. Its existing delivery-only Adobe license does not permit vendoring.
If the City acquires a self-hosting license, coordinate the typography contract
and adapter change and retire the remote exception; do not silently replace the
network-free display family.