Published as @antadesign/anta on npm. Portable UI component library. Works in React apps out of the box, in Preact via compat aliasing (react → preact/compat), and in custom runtimes via configure().
Two tiers per component:
src/elements/— Web components (custom elements). Attribute-driven, shadow DOM for rendering, plain CSS for token definitions. Files:a-{name}.ts,a-{name}.css. The external CSS file defines CSS variable tokens on the host, and the shadow DOM style references those variables. No visible attributes are set from JS on the host element — only shadow-internal styles are modified (e.g. setting--_percenton an internal element).src/components/— JSX wrappers. State management, CSS modules. Files:{Name}.tsx,{Name}.module.css. These handle logic and provide a typed component API. Useclsx(imported ascn) for class joining. Forwardstyleand extra props via spread to the underlying web component.
The tiers are decoupled — JSX wrappers emit <a-*> tags but never import element definitions. Binding is by tag name at runtime.
src/jsx-runtime.ts— Custom JSX runtime. Defaults toReact.createElement. Callconfigure(h)to swap.src/types.d.ts— CSS module type declarations andJSX.IntrinsicElementsfora-*custom elements.src/elements/index.ts— Barrel export that auto-registers all web components in browser contexts. Must only be imported client-side —HTMLElementdoes not exist in Node/SSR.src/index.ts— Barrel export for JSX components andconfigure().
Source lives in src/. Build output goes to dist/. dist/ is gitignored — pnpm install regenerates it via the prepare lifecycle script, and npm publish regenerates it via prepublishOnly so the published tarball is always fresh. The package is distributed only through npm; the GitHub-URL install path is not supported.
pnpm run build # Full build: JS + CSS + types
pnpm run build:js # esbuild: TSX/TS → dist/*.js
pnpm run build:css # Copy CSS files to dist (esbuild non-bundle mode doesn't output them)
pnpm run build:types # tsc: emit .d.ts declarations
pnpm run typecheck # Type check without emitImportant: esbuild runs in non-bundle mode (no --bundle flag). It compiles each entry point individually but does not process CSS imports — it leaves import "./a-progress.css" in the JS output without copying the CSS file. The build:css step copies CSS files manually. When adding a new component, add its CSS files to build:css.
Uses jsx: "react-jsx" with jsxImportSource: "@antadesign/anta" (automatic transform). The compiled output self-references the package via the exports map: import { jsx } from "@antadesign/anta/jsx-runtime".
.github/workflows/ci.yml runs on pull requests to main:
pnpm run build— build antapnpm run typecheck— type checkpnpm --filter anta-site build— build the docs site
Consumers can use anta via:
- npm:
"@antadesign/anta": "dev"(prerelease) or a specific version - Local link:
"@antadesign/anta": "link:/path/to/anta"(for development)
The exports map handles all subpath imports. Explicit entries exist for ., ./jsx-runtime, ./elements, ./elements/*, plus a "./*" wildcard fallback. main and types fields provide fallback for classic moduleResolution: "node".
react is a peer dependency — consumers provide it (or alias it to preact/compat).
@antadesign/anta/elements registers custom elements using HTMLElement, which does not exist in Node.js. In SSR contexts (Astro, Next.js), import it client-side only:
// In Astro: use a <script> tag (client-side by default)
<script>import '@antadesign/anta/elements'</script>
// In a Preact/React island: dynamic import inside useEffect
useEffect(() => { import('@antadesign/anta/elements') }, [])npm version prerelease --preid=dev # Bump: 0.1.1-dev.1 → 0.1.1-dev.2
npm publish --access public --tag dev # Publish prerelease under "dev" tagEach version string is immutable on npm — always bump before publishing.
CHANGELOG.md at the repo root documents changes to the @antadesign/anta package only — code shipped to npm consumers (anything under src/ and dist/, plus root files in the published tarball). The docs site under site/ is its own thing and does not belong in the changelog. New site pages, component-docs polish, demos, layout tweaks — all of that ships only on antadesign.dev and isn't a consumer-facing change.
When in doubt: would a consumer who installs this version see this change in their app? If no, leave it out of CHANGELOG.md. Use commit messages and PR descriptions for the docs-site narrative.
The site/ folder is the anta design system documentation website, built as a pnpm workspace member.
Stack: Astro 5 (static output) + Preact + MDX + astro-expressive-code (syntax highlighting, tokyo-night theme). Remark plugins: GFM, math, directive, definition-list, attributes. Rehype plugins: slug, autolink-headings, mathjax.
Structure:
site/src/layouts/DocsLayout.astro— Sidebar + main content shell. Imports@antadesign/anta/elementsvia client-side<script>.site/src/pages/—.astropages for static content,.mdxpages for component docs with code examplessite/src/components/— Preact island components (.tsx) for interactive demos, hydrated withclient:loadsite/src/styles/base.css— Minimal reset and typography (inline values, no CSS variables — will adopt anta's global tokens when available)
How it uses anta: "@antadesign/anta": "workspace:*" resolves to the local package. Anta must be built first (pnpm run build in root). Preact compat (@astrojs/preact({ compat: true })) aliases react → preact/compat, so anta's jsx-runtime works without calling configure().
cd site && pnpm run dev # Dev server
cd site && pnpm run build # Static buildAdding a component docs page: Create site/src/pages/components/{name}.mdx with layout: ../../layouts/DocsLayout.astro. For interactive demos, create a Preact island in site/src/components/ and include it with <Demo client:load />.
When naming components, props, CSS variables, internal class names, or suggesting patterns, reference established design systems: Material Design, shadcn/ui, Carbon Design System, and Shopify Polaris Web Components. Prefer terminology and conventions already used by these libraries over inventing new ones.
See FIGMA.md for rules when extracting tokens, components, or styles from the Anta Figma library. Key rule: always read the full variable list directly from the collection — don't infer the token set from get_variable_defs on a sample node, because tokens that aren't placed on the queried node won't appear, and you'll silently miss values.
To tune the alpha of any color (variable, currentColor, hex, etc.), always use color-mix(in oklch, <color> <percent>%, transparent). Mixing in oklch keeps the perceived hue/lightness stable, while the percent maps directly to the desired alpha (e.g. 50% → 0.5 alpha). This is the standard pattern in Anta — do not reach for rgba(...), hex-with-alpha (#rrggbbaa), or opacity on the parent when only the alpha of one color needs to change.
/* underline at half-strength of the link's color */
text-decoration-color: color-mix(in oklch, currentColor 50%, transparent);
/* token at 80% alpha */
border-color: color-mix(in oklch, var(--border-4) 80%, transparent);The same rule applies anywhere we lighten/darken/desaturate a color: prefer color-mix(in oklch, <color> <p>%, <other-color>) so all interpolation happens in a perceptually-uniform space.
- Declarative DOM — Web components are pure declarative. No element class — neither the constructor nor
attributeChangedCallbacknor any handler — may callsetAttribute, mutateclassName, set inlinestyle, or otherwise change anything on the host element that's visible in the DOM tree. The host's attributes and inline styles must come from the JSX wrapper (or from the consumer writing<a-…>directly). Only shadow-internal elements may be mutated from JS. - ARIA goes in JSX wrappers, not web components. All
role,aria-*,tabindex, etc., are added bysrc/components/<Name>.tsxas attribute pass-through, never byAXxxElement.constructor. This keeps the web component re-renderable from any reactive engine without state churning the DOM, and keeps the elements usable in non-React/Preact contexts where the consumer adds ARIA themselves. The single exception is when the wrapper passes a value through (e.g.aria-valuenow={value}); the wrapper is a thin JSX→DOM bridge and that's its job. - Don't add new web components without a strong reason. Each one occupies a global tag name. Prefer adding props to existing elements before introducing a new tag.
- Shadow DOM pattern — Web components use shadow DOM. The external CSS file (
a-{name}.css) styles the host element and handles light/dark mode via.darkancestor. The shadow DOM<style>declares only structural defaults on:host. - CSS variables for variant values — Use
--{component}-*variables for any property that changes across variants (tone, dark mode). In the base rule, declare all variables first, then leave an empty line before regular properties. - CSS variables for shadow internals — Use
--_prefix for shadow-internal-only variables set from JS (e.g.--_percent). - Dark mode — Use
.darkancestor class in the external CSS. - Default variant in union types — Include default value explicitly in the type union (e.g.
tone?: 'neutral' | 'info'). - CSS modules only on JSX wrappers, plain CSS for web components. Use
.containeras the top-level class in CSS modules. - Types — Use React global types (e.g.
React.CSSProperties) without importing React. Components must be compatible with both React and Preact. - Auto-registration —
elements/index.tsauto-registers all custom elements in browser contexts. - Component-token-first — Each component defines its own CSS custom properties. Global tokens will be added later.
- Docs-in-sync — When you rename a component prop, rename a
--{component}-*token, add a token, or remove one, updatesite/src/pages/components/{name}.mdxin the same change so the docs page tracks the source of truth. The same applies to default values, prop type unions, and to any consumer-facing API note inREADME.md. Drift between the source and the docs site is the single most common bug report — easier to keep them in lockstep than to chase the divergence later.
- Create
src/elements/a-{name}.ts— web component class +register_a_{name}()function - Create
src/elements/a-{name}.css— plain CSS using tag selector, attribute selectors for variants - Add to
src/elements/index.ts— import register function, re-export, add auto-register call - Create
src/components/{Name}.tsx— JSX wrapper, import CSS module - Create
src/components/{Name}.module.css— scoped styles for wrapper layout - Add to
src/index.ts— re-export the component - Add
a-{name}toJSX.IntrinsicElementsinsrc/types.d.ts - Add entry points to
build:jsscript inpackage.json - Add CSS files to
build:cssscript inpackage.json - Run
pnpm run buildto verify