@anyknown/ui
The design system shared by every AnyKnown product, built with StyleX, React 19 and Base UI.
What changed in each release is in CHANGELOG.md; upgrading from 0.9 is covered step by step in MIGRATION.md.
Structure
src/tokens.stylex.ts: semantic tokens (color, font, type, space, corner, motion, shadow and more). Light is the default; dark follows the OS. The single source of truth.src/tokens.css: the same values as plain CSS variables (--ak-*), for consumers that don't use StyleX (desktop references them from Tailwind v4@theme). Import path:@anyknown/ui/tokens.css. There is one palette (neutral); light and dark follow the OS, anddata-themelocks one. Generated bypnpm gen:tokens-css; don't edit it by hand (src/tokens-css.test.tsfails when it is out of sync).src/themes.stylex.ts:light/darkthemes, for apps with a manual theme switch:<div {...stylex.props(...light)}>. Generated bypnpm gen:themes; don't edit it by hand (src/themes.test.tsfails when it is out of sync).src/brand.css: a bounded vocabulary of about twentyak-*classes for one-off pages with no bundler (reports, proposals, benchmarks). The build inlines the tokens and adds Google Fonts (scripts/brand-css.mjs), producingdist/brand.cssand https://ui.anyknown.com/brand.css. The rules are in DESIGN.md;pnpm checkfails when the document and the vocabulary disagree.src/scrollbar.css: custom scrollbars, applied globally. StyleX cannot write::-webkit-scrollbarpseudo-elements, so they live in a CSS file.src/components/: one folder per component:<Name>.tsx, an EnglishREADME.mdand, in most folders,<Name>.test.tsx. Every folder there is a component. The shared decisions are in components/README.md; the reasoning and the dead ends are in components/COMPONENTS.md.src/lib/: shared hooks and helpers:i18n.tsx(LocaleProvider,defineStrings,useStrings),store.ts(createStore,useStore),textLayout.ts(the optional Pretext engine), plus reduced motion, controllable state, clipboard, diff,styledandreset.control.docs/guides/: the guides: getting started, theming, state management, accessibility, internationalization and text layout.scripts/: generators and checks.palette.mjscomputes the colors,themes.mjsandtokens-css.mjsgeneratethemes.stylex.tsandtokens.css, andapi-docs.mjsreads the TypeScript source and writessite/generated/api.json(every component's props) andsite/generated/tokens.json(every token group) for the docs site.playground/: a demo app. Not part of the published package.site/: the docs site, deployed to Cloudflare Workers. Not part of the published package.
Docs site
- a page per component at
#/components/<name>, with its README, demos and a props table generated from the source; - the guides at
#/guide/<name>; - the tokens page at
#/tokens, with every token and its light and dark value; - every demo on one page at
#/demo;#/demo/<name>jumps to one. The playground uses the same section ids.
pnpm site:build builds the package, then the site, then llms.txt.
Development
pnpm test # vitest (jsdom + testing-library)
pnpm check # turbo run typecheck lint fmt:check design:check (parallel + cached)
pnpm typecheck # tsc over the library, the playground and the docs site
pnpm build # dist: ESM + d.ts + tokens.css + scrollbar.css + brand.css
pnpm verify:pack # build, pack, then resolve every exports-map entry from outside the package
pnpm gen:themes # regenerate src/themes.stylex.ts from the tokens
pnpm gen:tokens-css # regenerate src/tokens.css from the tokens
pnpm playground # http://localhost:5199, every component rendered from the built dist
pnpm site # http://localhost:5200, the docs site
pnpm site:test # jsdom smoke tests for the docs site
pnpm site:build # build the package and the docs site
pnpm site:deploy # build, then deploy to https://ui.anyknown.com (run wrangler login first)
check, test, build and the rest run through turborepo (single-package mode, configured in turbo.json). The cache is pushed to the self-hosted remote cache at https://turbo.anyknown.com (team anyknown-ui). To use the remote cache, export the token:
export TURBO_TOKEN=<token> # see turbo-cache-worker/GITHUB-SECRETS.md
Without it nothing breaks; turbo falls back to the local cache. The inputs of build, lint, typecheck and test in turbo.json deliberately exclude *.md, so editing docs doesn't rerun them. site:build and site:test use the default inputs, because the docs site reads those Markdown files.
Releasing
The package is published as ESM + d.ts. The StyleX calls stay in the output and the consumer's bundler compiles them (the standard approach for StyleX libraries).
Every release gets a CHANGELOG.md entry (Keep a Changelog: Breaking / Added / Changed / Deprecated / Fixed / Accessibility), and a breaking release also gets a section in MIGRATION.md with before/after code.
The version is set by a git tag. Push a v* tag and .github/workflows/release.yml runs the full gate, then publishes. If the tag doesn't match version in package.json, the workflow fails and nothing is published.
# 1. Bump version in package.json (semver)
# 2. Commit, tag and push
git commit -am "release: v0.2.0"
git tag v0.2.0
git push origin main --tags
CI runs typecheck / lint / fmt:check / test / site:test, then verify:pack, then npm publish. The first five go through the turbo remote cache, so most of them hit what already ran on the PR. verify:pack is not cached: its job is to pack a fresh tarball every time and resolve the exports map from outside the package.
Publishing uses npm trusted publishing (OIDC); the repo stores no npm credentials. The trusted publisher on npmjs.com ties @anyknown/ui to anyknown-com/ui / release.yml. CI uses npm publish, not pnpm publish, because pnpm's OIDC support isn't ready yet (pnpm#9812).
v0.1.0 was published by hand from a local machine: npm can only attach a trusted publisher to a package that already exists, so the first version had no way around it. Every version since goes through a tag.
The one thing CI can't check is the visuals. Before a release, open pnpm playground and look for visual or motion regressions, then turn on macOS "Reduce motion" and look again.
Infrastructure
| repo | https://github.com/anyknown-com/ui (public, MIT) |
| npm | @anyknown/ui, trusted publisher tied to anyknown-com/ui → release.yml |
| docs site | https://ui.anyknown.com (Cloudflare Workers) |
| design.md | https://ui.anyknown.com/design.md: DESIGN.md plus the generated token table and the brand.css vocabulary; the first entry in llms.txt |
| brand.css | https://ui.anyknown.com/brand.css: external agents making a one-off page just <link> this; also published as @anyknown/ui/brand.css |
| remote cache | turbo.anyknown.com, team anyknown-ui; the token is the repo secret TURBO_TOKEN, locally export TURBO_TOKEN=<token> (the worker reads it from a secret binding in anyknown-com/turbo-cache) |
Using it in an app
pnpm add @anyknown/ui @stylexjs/stylex
pnpm add -D @stylexjs/babel-plugin @stylexjs/postcss-plugin postcss @babel/core
StyleX needs two passes: Babel turns stylex.create into class names, and PostCSS writes the stylesheet. Both must run over node_modules/@anyknown/ui/dist/**/*.js as well as your code, with the same unstable_moduleResolution.rootDir. The entry CSS needs the bare @stylex; directive:
@import "@anyknown/ui/tokens.css";
@import "@anyknown/ui/scrollbar.css";
@stylex;
import { Button, Text } from "@anyknown/ui"
import { color, space } from "@anyknown/ui/tokens.stylex"
Mount <Toaster /> and <Dialogs /> once near the root, and wrap the app in <LocaleProvider locale="en"> for English built-in words (the default is zh-TW). Your own styles use the tokens, never raw colors.
The full Vite and PostCSS setup, fonts, Tailwind mapping and a first screen are in Getting started.
Visual direction: pencil sketch
One sheet of paper, no lines on it, nothing behind the words, and color only for status (OKLCH chroma 0 grays, ink text). Hierarchy comes from space, weight, size and one gray (textMuted). Only things you press carry a wash (ink at 6%, 11% on hover); only things that float (text fields, the composer, menus, sheets, dialogs, toasts, tooltips) get a raised surface with a shadow, one level at most. Blue signal means working, red failed, amber waiting for you, green done, and only on text or a 6px dot. There is no accent color: the primary action is a solid ink pill and links are ink with an underline. Corners are shape (a pill, for one-line controls), box (24px, for boxes that grow) and check (4px, the checkbox square). Headings and body text use Geist, timelines and data use Geist Mono, and Chinese uses Noto Sans TC (falling back to PingFang TC or Microsoft JhengHei when it isn't installed). The spec is DESIGN.md section 1. Colors are computed by scripts/palette.mjs from five 12-step OKLCH scales: to change a color, change the scale, rerun it and paste the output back; never hand-tune a value (see Theming). The earlier woven design language and its ledger palette are archived on the archive/fiber branch.
Apps install the fonts themselves:
pnpm add @fontsource-variable/geist @fontsource-variable/geist-mono @fontsource-variable/noto-sans-tc