@hanzo/mod

Five platform look profiles (iOS, Android, macOS, Windows, Linux), theme mods as JSON, and web fonts, written as CSS custom properties on the page's root element and kept in this browser's localStorage. No server, no account sync, no mod loader.

@hanzo/mod is a small browser library with no dependencies. It does three things, all on the page it runs in:

  • Platform profiles. Five sets of font stacks, corner radii and glass values, named after an OS. It picks one from the user agent or keeps the one chosen, and writes it as CSS custom properties.
  • Theme mods. A mod is a JSON object: colours, fonts, a corner radius, glass values, custom CSS. It applies one, serializes it, parses and checks one, or derives a new one from another. Six are built in.
  • Fonts. It loads a web font by stylesheet or font-file URL and points the heading, body or mono face at a family.

Everything it does is a write to document.documentElement (CSS custom properties and a data-platform attribute), a <link> or <style> element in the page's head, or a localStorage key, and it dispatches two events on window (hanzo:mod:change, hanzo:mod:applied). It makes no request of its own: the only network traffic is the font or stylesheet URL you hand it. On a server (no window, no document) the functions that touch the page do nothing, getActivePlatform answers ios and getOrgMod answers null.

  • npm: @hanzo/mod, 0.1.0
  • Source: hanzoai/app, directory pkg/mod
  • License: Apache-2.0 OR MIT
  • Peer: react 19 or later is declared; no module imports it

@hanzo/app depends on it, and its Settings panel is the one place it is used today (below).

Install

pnpm add @hanzo/mod

Example

From the Settings panel in @hanzo/app (pkg/app/src/look.tsx), reduced to the calls:

import { setActivePlatform, BUILTIN_MODS, applyMod, importMod } from '@hanzo/mod'

// A platform: kept under localStorage hanzo.mod.platform, then written to <html>.
// 'auto' forgets the choice and follows the user agent.
setActivePlatform('macos')

// A built-in mod, or one pasted as JSON.
applyMod(BUILTIN_MODS[5])
applyMod(importMod(pasted))

A web font, loaded by its stylesheet and made the body face:

import { injectCustomFont, applyFontFamily } from '@hanzo/mod'

injectCustomFont({
  family: 'Inter',
  url: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;600',
  isStylesheet: true,
})
applyFontFamily('sans', 'Inter, sans-serif')

For a stylesheet, the face is named by the stylesheet itself (Google's CSS above declares Inter); family only names the element injectCustomFont adds, so it can be replaced or removed.

Exported API

Platform profiles:

ExportWhat it does
PLATFORM_PROFILESFor ios, android, macos, windows and linux: a name, a font stack, a mono stack, card and pill radii, glass blur (px) and saturation (%), specularBorder and touchFeedback.
detectPlatform(userAgent?)Reads the user agent (default navigator.userAgent) and returns a PlatformId. An iPad that reports MacIntel with touch points counts as ios; a user agent that matches nothing returns ios.
getActivePlatform()The id kept under localStorage key hanzo.mod.platform, else detectPlatform().
setActivePlatform(id)Keeps id under hanzo.mod.platform, or removes the key for 'auto'; calls applyPlatformTokens; dispatches hanzo:mod:change on window.
applyPlatformTokens(id)Sets data-platform and --platform-font-sans, --platform-font-mono, --platform-radius-card, --platform-radius-pill, --glass-blur and --glass-saturate on <html>.

Theme mods:

ExportWhat it does
ModThemeThe type: id, name, and optional description, author, org, platform, fontHeading, fontBody, fontMono, customFontUrl, primary, accent, glassTint, glassBlur, radius, density, customCss.
BUILTIN_MODSSix mods: ios-liquid, android-material, macos-vibrant, windows-fluent, cyber-zen, nordic-frost.
applyMod(mod)Applies the mod's platform profile when it names one; sets --primary, --accent, --liquid-glass-bg (from glassTint), --glass-blur, --radius, --font-heading, --font-body, --font-mono for the fields the mod has; puts customCss in <style id="hanzo-custom-mod-css">, or removes that element when the mod has none; keeps the mod under hanzo.mod.active; dispatches hanzo:mod:applied.
exportMod(mod)The mod as indented JSON.
importMod(json)Parses JSON into a ModTheme. Throws Invalid Hanzo mod: missing theme name when there is no name. Cuts name and author to 64 characters and description to 256, drops a platform it does not know and a glassBlur or radius that is not a number, gives a missing id one made from the time, and passes every other field through as given.
forkMod(base, overrides)base with overrides on top, under a new id (<base id>-fork-<time>) and the name <base name> (Mod), unless the overrides set them.
saveOrgMod(orgId, mod)Writes the mod, with org set to orgId, to localStorage key hanzo.mod.org.<orgId> in this browser.
getOrgMod(orgId)Reads that key back, or null.

Fonts:

ExportWhat it does
CustomFontOptionsThe type: family, url, and optional format (woff2, woff, truetype, opentype), weights, isStylesheet.
injectCustomFont(options)A URL ending in .css, on fonts.googleapis.com, or passed with isStylesheet becomes a <link rel="stylesheet">. Any other URL becomes one @font-face rule per weight (default 400, 500, 600, 700, font-display: swap) in a <style>. Replaces the element an earlier call made for the same family.
removeCustomFont(family)Removes that element.
applyFontFamily(target, family)Sets --font-heading, --font-sans or --font-mono on <html>, for target heading, sans or mono.

What reads the properties

@hanzo/mod writes custom properties; a stylesheet decides what they change. In what hanzo.ai ships (@hanzo/design 0.5.35, @hanzo/ui 8.29.26, @hanzo/glass 0.1.0):

PropertyWritten byRead by
--font-sans, --font-mono, --radius, --primaryapplyFontFamily, applyMod@hanzo/design and @hanzo/ui
--accentapplyMod@hanzo/ui
--liquid-glass-bgapplyMod@hanzo/glass
data-platform, --platform-*, --glass-blur, --glass-saturate, --font-heading, --font-bodyapplyPlatformTokens, applyMod, applyFontFamilyNone of them. A host that wants these reads them in its own CSS.

So on hanzo.ai a platform profile alone changes nothing on screen: its radii and font stacks go to --platform-*, which no Hanzo stylesheet reads. A mod's own radius, colours and mono face do.

In hanzo.ai's Settings

Settings → General on hanzo.ai draws Look from @hanzo/app/look. Below the appearance controls (theme, type, accent, which @hanzo/appearance keeps with the person's IAM account), it has four @hanzo/mod controls:

  • Native Platform Look & Feel: Auto and the five platforms, through setActivePlatform.
  • Organization Theme Mods: one button per built-in mod, through applyMod. The choice is not saved for the organization: the panel does not call saveOrgMod.
  • Custom Typography: a family name is applied to the heading and body faces with applyFontFamily. A stylesheet URL is loaded, but the face applied is named CustomBrandFont, which a stylesheet such as Google Fonts' does not declare, so text falls back to the browser's generic sans-serif.
  • Fork & Share Theme Mods: pasted JSON goes through importMod and applyMod. Export JSON copies the first built-in mod (ios-liquid), not the one in effect.

A mod or a font applied there lasts until the page reloads. The chosen platform is kept, and the panel shows it as chosen on the next visit, but its properties are not written to the page again.

What it does not do

  • No server and no account sync. Every key is in this browser's localStorage. saveOrgMod does not reach another browser, another device, or another member of the organization.
  • No re-apply on load. applyMod keeps the mod under hanzo.mod.active, and nothing in the package reads that key back; the kept platform is not written to the page again either. A host that wants a choice to outlive a reload calls applyMod and applyPlatformTokens(getActivePlatform()) at start-up itself.
  • No reset. applyMod sets only the fields a mod has. A colour or radius an earlier mod set stays until the page reloads.
  • Not every field is applied. density, customFontUrl, description, author and org travel in a mod's JSON; applyMod reads none of them.
  • No sanitizing. importMod passes colours and customCss through as given, and applyMod puts customCss on the page as it is. CSS can load remote URLs, so apply only mods you trust.
  • No native look. The profiles are font stacks and numbers for web CSS. It draws no native controls, and there is no native mobile shell on @hanzo/app.
  • No mod loader. HIP-0528 (a draft) describes mods as packages: a manifest, a registry, a loader, and an API for them. None of that is in 0.1.0; this package is the three groups above and nothing else.
Was this page useful?
Last updated Oct 30, 2026