A shadcn registry serving the Crowdin design tokens and a curated, pinned mirror of shadcn/ui components. Component source is pristine upstream shadcn; all Crowdin styling lives in the theme, style, and app-theme items.
Add the registry to your components.json:
{
"registries": {
"@crowdin": "https://crowdin.github.io/ui-registry/r/{name}.json"
}
}Install the Crowdin style (theme tokens + base CSS), then components:
npx shadcn@latest add @crowdin/style
npx shadcn@latest add @crowdin/button @crowdin/dialogThe registry serves the latest release - changes on main are not published until a release is made.
@crowdin/theme ships the canonical Crowdin design tokens as plain values - override them in your own CSS like any shadcn theme.
Apps that run inside the Crowdin iframe should also install @crowdin/app-theme. It re-points the tokens at the host --crowdin-* variables and keeps the canonical values as fallbacks so the app still looks right in local development outside the iframe:
npx shadcn@latest add @crowdin/style # standalone app
npx shadcn@latest add @crowdin/style @crowdin/app-theme # app embedded in CrowdinCSS variables never cross the iframe boundary, so @crowdin/app-theme also installs the host bridge that pulls the palette from the host: lib/crowdin-host.ts (framework-agnostic) and hooks/use-crowdin-theme.ts (React). Wire it once near your app root - it applies the host --crowdin-* variables to <html>, tracks host theme changes, and no-ops outside the iframe:
const mode = useCrowdinTheme() // "light" | "dark" - toggle your .dark class off thisThe app must load the Crowdin iframe SDK (https://cdn.crowdin.com/apps/dist/iframe.js) for the bridge to find the AP global; an async script tag is fine - the bridge probes for AP for ~10 seconds after mount before concluding it runs outside the iframe.
npx shadcn@latest add @crowdin/button --diff
npx shadcn@latest add @crowdin/button --overwriteThe registry works with the shadcn MCP server:
npx shadcn@latest mcp init --client claudepnpm install
pnpm test # unit tests for the sync helpers and theme/bridge invariants
pnpm sync # refresh registry/src/ from upstream shadcn (open the diff as a PR)
pnpm validate # schema-validate registry.json
pnpm build # emit r/*.json
pnpm smoke # install everything into a fixture app, typecheck, compile CSS
pnpm site:dev # run the component showcase locally (one-time: pnpm --dir site install)
pnpm site:build # typecheck and build the showcasetests/hosts/*.json pin the --crowdin-* variables each Crowdin host delivers to app iframes through AP.getCssVariables, trimmed to what the app-theme bridge reads plus the variables those values reference. The tests require every variable the bridge reads to be present on every host, and the theme literals to equal what crowdin.com sends - so when a host changes its variables, refresh the fixture and let the tests tell you what to update. To refresh one, open the host in a browser and copy window.cssVariables from the console: it is the same map the host sends to iframes.
registry/src/** is generated by pnpm sync - never edit it by hand. Crowdin styling changes go into the theme/style/app-theme items in registry.json. registry/crowdin/** is hand-written (the host-bridge sources shipped by app-theme) and survives sync.
To refresh the components from upstream shadcn, run the Sync upstream workflow from the Actions tab - it syncs, runs the full check suite, and opens a PR with the diff (only when something actually changed). New upstream components are opt-in: add the name to scripts/upstream-items.json first.
The showcase at crowdin.github.io/ui-registry is built from site/ and renders registry/src/** directly with the generated Crowdin theme - use its header toggles to preview dark mode and the captured host palettes from tests/hosts/.