A cross-platform design system for the World ecosystem. Nucleus publishes design tokens (colors, typography) and a small set of components as platform-native packages for Android, iOS, and web.
Tokens are authored as JSON in tokens/definitions/. A TypeScript build pipeline reads the JSON through a validating loader, runs format-specific generators, and writes platform-native source files into the repo. The generated files are committed; CI verifies they are in sync with the source JSON on every PR.
graph TD
A[tokens/definitions/*.json] --> B[loaders<br/>tokens/formats/loaders.ts]
B --> C[per-platform format functions<br/>tokens/formats/*-android, *-ios, *-web]
C -->|Handlebars templates| D[android/nucleus/src/main]
C -->|Handlebars templates| E[ios/Sources]
C -->|strings + asset copy| F[build/web]
Design principles
- JSON is the single source of truth. Every platform output is derived from the same JSON; nothing is hand-edited downstream.
- Source layers are explicit in token paths. Color tokens live under
primitive.color.*andsemantic.color.*. Font tokens are split into afamiliesblock (font files) and atokensblock (typography styles). - Platform outputs are standalone. No app-specific dependencies. Android gets Compose
Color/NucleusFontStylevalues; iOS gets aNucleusColorstruct and aNucleusFontstruct; web gets CSS custom properties + JSON. - Generated files are committed. CI runs
npm run buildthengit diff --exit-codeto ensure the source and generated outputs stay in sync.
npm ci
npm run buildGenerated files appear in:
| Platform | Path | Contents |
|---|---|---|
| Android | android/nucleus/src/main/java/com/worldcoin/nucleus/tokens/ |
Kotlin objects (NucleusPrimitiveColors, NucleusSemanticColorsLight/Dark, NucleusFonts) shipped in the android/nucleus Maven artifact |
| Android | android/nucleus/src/main/res/font/ |
Bundled font files (.ttf) |
| iOS | ios/Sources/NucleusColors/ |
NucleusColor struct + generated NucleusColor+Primitives.swift and NucleusColor+Semantics.swift (SPM library NucleusColors) |
| iOS | ios/Sources/NucleusFonts/ |
NucleusFont struct + generated NucleusFont+Defaults.swift and bundled font resources (SPM library NucleusFonts) |
| Web | build/web/ |
CSS custom properties (nucleus-*.css), JSON token files, font files, package.json for npm publishing |
The repo-root VERSION file is the canonical version. npm run build stamps it into the web package.json; the Android library reads it from build.gradle.kts; SPM consumes the SPM tag.
Colors are layered:
- Primitive (
primitive.color.*) — raw palette values (e.g.grey.500,red.700). Use only when no semantic token fits. - Semantic (
semantic.color.*) — purpose-named tokens that adapt to theme (e.g.surface.primary,text.primary). Prefer these in product code so light/dark swap automatically.
Source: tokens/definitions/color/primitive.json, tokens/definitions/color/semantic.light.json, tokens/definitions/color/semantic.dark.json.
Source: tokens/definitions/font/fonts.json.
Typography is organized into six categories, each with one or more sizes (1 = largest within the category).
| Category | Prefix | Sizes used today | Intended use |
|---|---|---|---|
| Display | d |
d1 |
Hero / marketing-scale text. |
| Numeric | n |
n1–n5 |
Large numeric readouts (balances, scores, counters) — semibold weight, tighter tracking. |
| Heading | h |
h1–h4 |
Section and view titles. Decreasing prominence from h1 to h4. |
| Subtitle | s |
s1–s4 |
Secondary headings or emphasized labels. Medium weight. |
| Label | l |
l1–l3 |
Buttons, chips, captions, badge text. Semibold weight. |
| Body | b |
b1–b4 |
Paragraph and content text. Lighter weight (325), looser line height (1.3). |
Picking a token. Start from the category (what role does this text play?), then choose the size that matches the visual hierarchy of the screen. Don't mix categories at the same hierarchy level on one screen.
Each token carries: family (font file), size (pt / sp / px), weight (variable axis value, typically 300–800), letterSpacing (em multiplier), lineHeight (em multiplier), and an optional dynamicTypeStyle for iOS Dynamic Type scaling.
TODO: the category names (Display, Numeric, Heading, etc.) are inferred from prefix conventions; double check with design on canonical names.
Android
Add the GitHub Packages Maven repository to settings.gradle.kts:
maven {
url = uri("https://maven.pkg.github.com/worldcoin/nucleus")
credentials {
username = System.getenv("GITHUB_USER")
password = System.getenv("GITHUB_TOKEN")
}
}Then add the dependency:
implementation("com.worldcoin:nucleus:<version>")Colors
import com.worldcoin.nucleus.tokens.NucleusPrimitiveColors
import com.worldcoin.nucleus.tokens.NucleusSemanticColorsLight
val grey = NucleusPrimitiveColors.grey900
val surface = NucleusSemanticColorsLight.surfacePrimaryTypography
import androidx.compose.material3.Text
import androidx.compose.ui.text.TextStyle
import com.worldcoin.nucleus.tokens.NucleusFonts
val style = with(NucleusFonts.b1) {
TextStyle(
fontFamily = fontFamily,
fontSize = fontSize,
fontWeight = fontWeight,
letterSpacing = letterSpacing,
lineHeight = lineHeight,
)
}
Text("Hello", style = style)NucleusFonts.<token>.fontFamily is wired to the bundled font resource — no R.font.* lookup needed.
iOS
Add the SPM dependency in your Package.swift:
.package(url: "https://github.com/worldcoin/nucleus.git", branch: "generated/ios")Or pin to a specific release tag (e.g. vX.Y.Z-ios). Then add the libraries you need as target dependencies:
.target(
name: "YourTarget",
dependencies: [
.product(name: "NucleusColors", package: "nucleus"),
.product(name: "NucleusFonts", package: "nucleus"),
]
)Colors
import NucleusColors
import SwiftUI
Text("Hello")
.foregroundStyle(NucleusColor.surfacePrimary.color) // adapts to light/dark
.background(NucleusColor.grey900.color) // primitiveTypography
import NucleusFonts
import SwiftUI
// Register bundled fonts once at app launch.
NucleusFont.registerFonts()
Text("Hello")
.font(NucleusFont.b1.asFont())NucleusFont supports variable weight axis, monospaced digits, and Dynamic Type. See ios/Sources/NucleusFonts/NucleusFont.swift for the full API.
Web
Add a .npmrc to your project:
@worldcoin:registry=https://npm.pkg.github.com
Then install:
npm install @worldcoin/nucleusColors
@import "@worldcoin/nucleus/nucleus-primitive-colors.css";
@import "@worldcoin/nucleus/nucleus-semantic-colors-light.css";
.card {
color: var(--nucleus-text-primary);
background: var(--nucleus-surface-primary);
border: 1px solid var(--nucleus-grey-200);
}Typography
@import "@worldcoin/nucleus/nucleus-fonts.css";
@font-face {
font-family: "WorldProMVPLH-Regular";
src: url("@worldcoin/nucleus/fonts/WorldProMVP.ttf") format("truetype");
}
.headline {
font-family: var(--nucleus-font-h1-family);
font-size: var(--nucleus-font-h1-size);
font-weight: var(--nucleus-font-h1-weight);
letter-spacing: var(--nucleus-font-h1-letter-spacing);
line-height: var(--nucleus-font-h1-line-height);
}JSON tokens (for JS/TS)
import colors from "@worldcoin/nucleus/nucleus-primitive-colors.json";
import fonts from "@worldcoin/nucleus/nucleus-fonts.json";- Edit the relevant JSON file in
tokens/definitions/. The loader validates the shape at build time and throws a clear error if a field is missing or malformed. - Run
npm run buildto regenerate platform sources and verify output. - Commit both the source change and the regenerated files (CI enforces this with
git diff --exit-code). - Open a PR with a release label (
patch,minor, ormajor).
For new typography tokens: add the font file to tokens/definitions/font/, declare it in the families block of fonts.json, then reference it from the token. The Android resource name is derived by snake-casing the filename.
Release automation is split across two workflows:
.github/workflows/prepare-release.yml— prepares release PRs.github/workflows/publish-release.yml— tags and publishes merged release PRs
The prepare workflow runs in two modes:
- Push to
mainafter merging a PR with a release label (major,minor,patch) — the workflow derives the bump from the merged PR, creates arelease/v*branch, and opens a release PR. - Manual dispatch — choose the bump type from the Actions UI to create the same release PR flow without a source PR label.
If the computed tag already exists, prepare-release.yml skips instead of opening a duplicate.
- Release PR creation —
prepare-release.ymldetermines the next version, updatesVERSION,package.json,package-lock.json, andCHANGELOG.md, then opens arelease/v*PR. - Release PR merge — merging that PR back into
maintriggerspublish-release.yml. - Tag + build — the merged release commit is tagged as
v*, thennpm run buildruns and uploadsandroid-tokens,ios-tokens, andweb-tokens. - publish-mvn — publishes Android library to GitHub Packages.
- publish-spm — commits generated iOS files to the
generated/iosbranch, tags asv*-ios. - publish-npm — publishes web package to GitHub Packages npm registry.
publish-release.yml only publishes the first time it creates the v* tag. Reruns after that tag exists skip the publish jobs.
The verification workflow (.github/workflows/verify.yml) runs format:check, lint, typecheck, build, and the codegen-in-sync check on every push to main and every PR.
examples/android/— Android demo withlocalandpackageflavorsexamples/ios/— iOS demo withLocalandPackageschemesexamples/web/— Next.js demo withlocalandpackagetoken sources