Public mirror for @nebutra/fonts from Nebutra/Nebutra-Sailor.
This repository is generated from the Nebutra Sailor monorepo. Package releases are cut from the monorepo and mirrored here for discovery, standalone cloning, and contribution intake.
- Canonical source:
packages/design/fontsinNebutra/Nebutra-Sailor - Package registry: npm and GitHub Packages
- Contributions: open issues or PRs here; maintainers port accepted changes back into the monorepo source package
Status: WIP — not yet published to npm.
The Simplified-Chinese face is wired into every Next app that loads Geist (web,
landing, design-docs, sailor-docs, admin, forge, router, sleptons, typelens,
mail-preview). The theme / DESIGN.md registry (the ~16 Google faces) is still used
only by apps/web.
Self-hosted fonts for Nebutra: the CJK body face, plus an OSS font registry for themes and imported DESIGN.md font families.
The package has three entries:
@nebutra/fontsis client-safe and maps a CSS font-family stack to the registry CSS variable that should be prepended.@nebutra/fonts/next/cjkis server-only and declares the self-hosted Simplified-Chinese face vianext/font/local— no network at build time, which also keeps it working in a network-sandboxed dev server. This is the one every app needs.@nebutra/fonts/nextis server-only and declares the build-timenext/font/googleregistry faces plus the combined registry class name. It re-exports the CJK face, but importing it just for that would drag ~16 Google font downloads into the app — use./next/cjk.
pnpm add @nebutra/fontsApply registry font variables at the application root:
import { fontRegistryClassName } from "@nebutra/fonts/next";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={fontRegistryClassName}>
<body>{children}</body>
</html>
);
}Resolve theme or DESIGN.md stacks on the client-safe path:
import { withRegistryFont } from "@nebutra/fonts";
const stack = withRegistryFont("Space Grotesk, sans-serif");
// "var(--font-space-grotesk), Space Grotesk, sans-serif"Geist has no CJK coverage at all, so without a CJK face every Chinese character
falls back to whatever the OS supplies: PingFang on macOS, Microsoft YaHei on
Windows, something else again on Android. Chinese copy is a first-class surface
in this product (see docs/microcopy/), so the CJK face is self-hosted and
subset here.
Two lines in the root layout, beside the Geist loaders:
import { cjkFontClassName } from "@nebutra/fonts/next/cjk";
import { GeistMono } from "geist/font/mono";
import { GeistSans } from "geist/font/sans";
<html className={`${GeistSans.variable} ${GeistMono.variable} ${cjkFontClassName}`}>That defines --font-vivo-sans-sc. Nothing else is needed: the token stacks in
@nebutra/tokens (--font-sans, --font-cn, --font-display, --font-heading)
already reference the variable in the right position. The app also needs
"@nebutra/fonts" in dependencies and in transpilePackages (the package ships
TypeScript source).
Do not re-declare --font-sans and friends downstream. @nebutra/ui's
typography/fonts.css used to, and being the later import its Geist-only copies
won, so the CJK half of the token stack never reached --font-sans at all. That
duplication is gone; the file now only carries derived aliases.
font-family: var(--font-geist-sans), "Geist", var(--font-vivo-sans-sc), "Noto Sans SC", …;Geist comes first and keeps Latin and the numerals — its tabular figures and tighter x-height are what dense dashboard tables need, and it is the locked UI face. vivo Sans SC takes CJK. Both faces cover Latin, so the order is what decides: reversed, vivo Sans would take the Latin too, and its Latin is not as good as Geist's for UI.
Belt and braces: the generated @font-face rules carry a unicode-range with
no Latin, no ASCII and no general-punctuation codepoints in it, so a Latin-only
page can never trigger a CJK download even if a stack somewhere is written the
wrong way round. Geist Mono remains the code face.
Measured in Chromium against a running app (apps/typelens, dev and production
build), reading the fonts the engine actually used per text run via
CSS.getPlatformFontsForNode — not by eye and not from the declared stack:
| Probe | Face actually used |
|---|---|
Handgloves ABC |
Geist |
1234567890 |
Geist |
开始设置的一二三 @400 |
vivo Sans |
开始设置的一二三 @500 |
vivo Sans Medium |
开始设置的一二三 @600 |
vivo Sans Demibold |
开始设置的一二三 @700 |
vivo Sans Demibold (no synthesis — font-synthesis: none) |
。、!?() |
vivo Sans |
Nebutra 云毓 2026 |
Geist for Latin + digits, vivo Sans for 云, PingFang SC for 毓 |
The last row is the design working as intended: 毓 is a GB2312 level-2
character, outside the subset, so it falls through the stack instead of bloating
every page. A Latin-only route (/this-route-does-not-exist) requested
Geist_Variable and nothing else — zero CJK bytes. The production build emits
exactly three woff2 files (498,084 / 504,228 / 504,332 B, byte-identical to the
package) and preloads only the two Geist files.
Note that document.fonts.check("16px vivoSansCn", "A") returns true. That is
the vacuous-true case in the spec — no face in the family matches U+0041's
unicode-range, so "all matching faces are loaded" is trivially satisfied. It is
not evidence of Latin coverage; the platform-font table above is.
pnpm --filter @nebutra/fonts subset:cjk # idempotent; reports every byte size
pnpm --filter @nebutra/fonts subset:cjk -- --forceRequires python3 with fontTools and brotli (for --flavor=woff2).
woff2_compress is not needed. Outputs land in generated/:
| File | Weight | Size |
|---|---|---|
vivo-sans-sc-400.woff2 |
400 body | 498,084 B (486.4 KB) |
vivo-sans-sc-500.woff2 |
500 --font-weight-medium |
504,228 B (492.4 KB) |
vivo-sans-sc-600.woff2 |
600 --font-weight-heading |
504,332 B (492.5 KB) |
vivo-sans-cn.css |
@font-face rules |
1,813 B |
generated/index.ts |
face metadata for next/font/local |
1,398 B |
Sizes move by a few hundred bytes as Chinese copy lands — the catalog character
count is an input, not a constant. generated/subset-manifest.json records the
exact character count and byte size of the build actually on disk.
The design system's numeric slots are --font-weight-medium: 500 and
--font-weight-heading: 600 (packages/design/tokens/recipe.css), and the token
CSS writes literal font-weight in only four values: 500 (43×), 600 (35×),
400 (27×), 700 (10×). So 400 / 500 / 600 ship. 700 resolves to the 600 face by
normal CSS font matching, and because that matched face is itself ≥ 600 no
browser applies synthetic bold — which is precisely why the third face is
DemiBold 600 rather than Bold 700. Choosing 700 instead would leave the default
heading weight, the most common heading value in the system, a step out of place.
Skin-declared fractional weights (300 / 450 / 510) resolve into the same set.
Each weight costs ~490 KB, so shipping all nine static faces would be ~4.4 MB.
vivoSansSCVF.ttf is 42 MB. Subset to this exact character set it is still
1,070,240 B, because a variable CJK font carries per-weight deltas for every
glyph it keeps — one variable file costs more than all three static subsets on
the pages that only use one weight. vivo Sans SC L3 is not usable as a body
face: 60,339 characters but zero in the CJK basic block, it is a rare-plane
supplement.
Three inputs, unioned — 4,282 characters in the current build:
- The zh catalogs, by glob (1,586 chars) — every
zh*.jsonunder anymessages/orlocales/directory, walked at build time, so new Chinese copy is covered on the next run instead of drifting away from a hardcoded list. - CJK punctuation and fullwidth forms, wholesale (197 chars) — U+3000–303F,
U+FE30–FE4F, U+FF01–FF5E, U+FFE0–FFE6. Chinese text whose
、。!?()are set in a different face than its characters looks broken immediately: different baseline, different advance width. 35 of these are absent from the source face (〄〰〸₩and friends); the build reports them and they fall through the stack. - A floor of common characters: GB2312 level-1 (3,755 chars, 一级汉字). The
catalogs only cover our copy — product surfaces render user data, and one
character of a name or a city falling back to PingFang mid-sentence is worse
than not using the font at all. The conventional floor is the 通用规范汉字表
一级字表 (3,500 常用字); GB2312 level-1 is a superset of essentially that set
and, the deciding factor, is derivable in-process from the platform's own
GB2312 decoder — no 3,500-entry list to vendor, review or let rot. It covers
99.5% of running modern text. Level 2 (~3,000 further rare surname and place-name glyphs) is excluded: it would roughly double every file for characters that appear in a fraction of a percent of text, which is exactly what OS fallback is for.
The floor is what costs the bytes: the catalog set alone subsets to 191,108 B per weight, the full set to ~498,000 B.
Possible follow-up, measured but not implemented here: splitting each weight into a hot tier (catalogs + punctuation, 1,777 chars, 197,028 B) and an extended tier (the GB2312 remainder, 2,509 chars, 312,644 B) with complementary
unicode-ranges would drop the common path from ~1.47 MB to ~591 KB across three weights, and only fetch the extended tier when user data actually renders a character outside our own copy. It costs two files per weight and (on thenext/font/localpath) a per-tierdeclarationsentry to carry the range.
vendor/vivo-sans/ holds only the three static TTFs the pipeline consumes —
vivoSans-Regular.ttf, vivoSans-Medium.ttf, vivoSans-DemiBold.ttf (7.4 MB
each) — not all 33 faces from the licensed set, and not the 42 MB variable font.
The licence agreement is committed beside them as
vendor/vivo-sans/LICENCE-vivo-Sans.txt. VIVO_SANS_SOURCE_DIR points the
script at the licensed originals when re-vendoring.
本软件使用了 vivo Sans 字体。 This software uses the vivo Sans typeface.
Per clause 2.1 of the vivo Sans 字体知识产权许可协议 (committed at
vendor/vivo-sans/LICENCE-vivo-Sans.txt): 您应在软件中特别注明使用了 vivo Sans 字体.
vivo Sans is licensed from vivo Mobile Communication Co., Ltd. and is not
covered by this package's MIT licence, which applies to the code only. Any
redistribution of the generated .woff2 files carries the same obligation.
The registry includes Geist, Inter, Space Grotesk, Playfair Display, JetBrains Mono, Manrope, Sora, Work Sans, DM Sans, Plus Jakarta Sans, Outfit, Figtree, Montserrat, Lexend, Fira Code, Roboto Mono, and Source Code Pro.
next/font downloads and self-hosts Google fonts at build time. At runtime,
the browser requests fonts from the application origin only when an element
uses the corresponding CSS variable.
MIT