Skip to content

Add Angular framework support to Ark UI#3888

Open
ryan-mahoney wants to merge 453 commits into
chakra-ui:mainfrom
ryan-mahoney:main
Open

Add Angular framework support to Ark UI#3888
ryan-mahoney wants to merge 453 commits into
chakra-ui:mainfrom
ryan-mahoney:main

Conversation

@ryan-mahoney
Copy link
Copy Markdown

Add Angular framework support (@ark-ui/angular)

First — thank you to the Chakra/Ark UI team. This work would not exist without
the careful design of Zag.js, the consistent component anatomies you've
established across React/Solid/Svelte/Vue, the headless API surface, and the
documentation site that made it possible to verify behavior against a known
reference.

This PR contributes a fifth framework target — Angular — modeled directly on
the existing framework packages and the patterns documented in CLAUDE.md /
.claude/docs. It is offered as a starting point: I expect the maintainers
will want to reshape parts of it (entry-point layout, adapter location,
naming) to match Ark's conventions, and I'm happy to follow that lead.


TL;DR

  • New packages/angular workspace publishing @ark-ui/angular, mirroring
    the React/Solid/Svelte/Vue packages in API parity and feature coverage.
  • 52 components + 14 utilities/providers wired to the existing Zag.js
    machines (same versions as the other framework packages — @zag-js/* 1.40.0).
  • 591 Angular example components + 62 Storybook stories, matching the
    story names and example identifiers used by the React reference so the
    website's example registry can resolve them by ID.
  • 79 spec files running on Vitest via @analogjs/vite-plugin-angular
    (zoneless), with coverage of the adapter, the form integration, and per-
    component behavior.
  • Website integration: framework selector now includes Angular, an
    llms-angular.txt route, example-registry plumbing, type-doc generation
    (scripts/src/generate-type-docs.angular.ts), and types/angular/*.json
    for the components covered so far.
  • MCP server (packages/mcp) lists Angular among supported frameworks.

Scope of changes

Area Files Notes
packages/angular/** ~1,735 New package, including dist scaffolding for ng-packagr secondary entry points
website/** (non-asset) ~100 Framework wiring, type docs, examples plumbing, llms route, doc updates
scripts/** ~16 Angular type-doc generator + example registry support
packages/mcp/** 2 Add Angular to supported frameworks
Root tooling (README, package.json, turbo wiring) a few Workspace registration

The work landed across ~326 conventional commits (feat(angular),
fix(angular), docs(angular), test(angular), chore(angular)) so the
history is fully bisectable. The branch can be reshaped into smaller staged
PRs on request — see "How to land this" below.


What's covered

Components (52)

accordion, angle-slider, avatar, carousel, checkbox, clipboard,
collapsible, color-picker, combobox, date-input, date-picker,
dialog, download-trigger, drawer, editable, field, fieldset,
file-upload, floating-panel, hover-card, image-cropper,
json-tree-view, listbox, marquee, menu, navigation-menu,
number-input, pagination, password-input, pin-input, popover,
progress, qr-code, radio-group, rating-group, scroll-area,
segment-group, select, signature-pad, slider, splitter, steps,
switch, tabs, tags-input, timer, toast, toggle, toggle-group,
tooltip, tour, tree-view.

Utilities / providers (14)

client-only, collection, focus-trap, format (byte/number/relative-
time/time), frame, highlight, portal, presence, swap,
download-trigger, plus the three providers — providers/environment,
providers/interaction, providers/locale. Each utility has parity examples
and a Storybook story matching the React reference.

Not yet covered

I matched the React surface as closely as possible but there are a couple of
React-only components I deliberately did not port pending a maintainer call:

  • time-picker (React has type docs but the component is still gated in
    React's surface area)
  • Anything added upstream after this branch's last sync against main

I'm happy to add these in a follow-up once direction is settled.


Architecture

The package follows the same headless / part-directive model used by the other
framework packages. The translation from "React components with a shared
useMachine hook" to "Angular directives with a shared injector-scoped
service" is the only place where the implementation meaningfully diverges from
its siblings. Three small files in src/_zag/ do this work:

src/_zag/use-machine.ts (~525 LOC)

A signal-based adapter that mirrors @zag-js/react's useMachine. Highlights:

  • Drives Zag's state graph via @zag-js/core helpers (createScope,
    findTransition, getExitEnterStates, hasTag, matchesState,
    resolveStateValue) — no custom state machine logic.
  • Uses Angular's reactivity primitives end-to-end:
    • signal() for the bindable store
    • computed() for derived props/api
    • effect() for context tracking, deps tracking, and late-arriving
      defaultValue hydration
    • inject(DestroyRef) for cleanup
  • Defers state.invoke(INIT_STATE) until afterNextRender(). This was the
    subtle one: machines like floating-panel / signature-pad resolve
    descendants in their entry actions via scope.getById, and the part
    directives' applyArkProps effects need to have flushed first. This
    mirrors the useSafeLayoutEffect + queueMicrotask pattern Zag uses on
    React.
  • createBindable honors the React convention that undefined means
    uncontrolled and null is a deliberate controlled value (Progress's
    indeterminate state depends on this).
  • Stable function wrappers for callbacks in context so identity changes
    don't churn the machine.

src/_zag/apply-ark-props.ts (~269 LOC)

The props().getRootProps() / getItemProps() etc. objects returned by Zag
are normalized React-style prop bags. This file applies them to a host
element via Renderer2:

  • DOM attributes vs. properties via isAttributeKey (role/id/tabindex/aria-/
    data-
    )
  • onChange → correct DOM event name based on tag/input type
  • className/class merged additively (preserves user-authored classes)
  • style with first-class CSS custom property support via setProperty
  • Bound event listeners cleaned up through DestroyRef

src/_zag/normalize-props.ts (5 LOC)

Identity normalizer; Zag's React-shaped prop bags are what we want.

Part-directive pattern

Each part is a standalone Angular directive that injects the root's context
via DI, reads its prop bag from the connected api, and forwards it through
applyArkProps. Example: [arkAccordion], [arkAccordionItem],
[arkAccordionItemTrigger], etc.

Forms integration (@angular/forms)

Form-bearing components (combobox, checkbox, listbox, number-input, pin-
input, radio-group, rating-group, select, switch, tags-input,
toggle, file-upload, slider, segment-group, password-input, editable, field,
fieldset) implement ControlValueAccessor through a shared
createArkCvaController helper. @angular/forms is declared an optional
peer dependency
so consumers who don't use Reactive/Template-driven forms
aren't forced to install it.

A small dev-time guard (scripts/check-forms-isolation.ts) enforces that
non-form entry points never import @angular/forms. It runs as part of
bun run typecheck. The matching check-forms-isolation.spec.ts keeps the
guard honest.


Public API shape

Component subpath exports follow the existing convention (@ark-ui/angular/ accordion, etc.). The package ships as an Angular Package Format (APF)
library via ng-packagr 20.x, producing FESM2022 bundles and .d.ts
declarations.

Two private subpaths exist for the adapter (./src/_zag,
./src/internal). scripts/hide-private-entrypoints.ts runs after build
and rewrites the published package.json so consumers can't import them.
This was the cleanest way I found to share types between secondary entry
points while keeping the consumer surface clean — I'm open to a different
arrangement if the maintainers prefer.

peerDependencies:

  • @angular/common: ^20.0.0
  • @angular/core: ^20.0.0
  • @angular/forms: ^20.0.0 (optional)

Angular 20 was chosen because it's the first release where signal-based
inputs (input(), model(), output()) and the zoneless renderer are
stable — both are used heavily in the adapter and component directives.


Examples & stories

Every component has at least the same parity examples as the React reference
(basic, controlled, disabled, multiple, etc.) plus framework-specific
variants where they made sense. Each example directory mirrors React's:

packages/angular/src/accordion/examples/
  basic.ts
  collapsible.ts
  context.ts
  controlled.ts
  disabled.ts
  horizontal.ts
  item-context.ts
  multiple.ts
  root-provider.ts

The story registry generator (scripts/src/generate-example-registry.ts) was
extended to pick up these examples so the website can render them via the
existing framework-switcher.

A small batch of commits (fix(angular): align <component> story with react parity) walks through every story and reconciles visual diffs against the
React reference — these were each verified in the running Storybook.


Tests

  • Adapter unit tests: _zag/use-machine.spec.ts, _zag/apply-ark- props.spec.ts, _zag/normalize-props.spec.ts
  • Internal helpers: internal/{id,split-props,render-strategy,context- carrier}.spec.ts
  • Forms: forms/control-value-accessor.spec.ts +
    scripts/check-forms-isolation.spec.ts
  • Per-component: <component>.spec.ts for every component (79 spec
    files total). These exercise the connected api, key interactions, and the
    state contract — not styling/visual.

Test runner: Vitest 4 with @analogjs/vite-plugin-angular and a zoneless
test setup. This matched the rest of the monorepo's preference for Vitest.


Website / docs / MCP

Surface Change
website/src/lib/frameworks.ts frameworks tuple now includes 'angular'
website/src/components/navigation/framework-select.tsx Adds Angular entry
website/src/app/(llms)/llms-angular.txt/route.ts New llms.txt route
website/src/lib/angular-example-registry.ts Angular-side of the example registry
website/src/lib/angular-prop-binding.ts Prop-binding helpers for docs renderer
website/src/lib/types-angular.test.ts Verifies type-doc generation output
website/src/content/types/angular/*.json Generated type docs (56 components/utilities so far)
website/src/content/pages/utilities/*.mdx Angular code samples added to each utility page
website/src/content/pages/overview/getting-started.mdx Angular install + usage section
website/src/content/pages/ai/llms.txt.mdx Angular included in framework list
packages/mcp/src/lib/types.ts angular added to supported framework union
scripts/src/generate-type-docs.angular.ts New generator that walks Angular directives and emits the same shape as the React generator

The type-doc generator is structured to match the React generator's output
format byte-for-byte for the same component, so the existing <PropsTable>
component renders Angular's props with no special-casing.


How to land this

This branch is large by necessity (a new framework target) but I've tried
to keep commits atomic. If staged landing is preferred, here is a natural
split:

  1. Adapter + scaffolding only (packages/angular/src/_zag, internal,
    workspace wiring, build/test/lint config). No components. Verifies the
    adapter in isolation.
  2. First component vertical slice (Avatar — chosen because it has both
    forms and non-forms parts and a small surface). Validates the
    end-to-end pattern.
  3. Component batches (existing branch already groups these into
    roughly batches 1–6 in the merged commits; can be replayed as separate
    PRs).
  4. Website integration (frameworks tuple, framework select, type
    docs generator + content).
  5. MCP support.

I'm happy to either rebase into that shape, or land this as-is and address
review feedback in follow-up commits — whichever fits the maintainers' usual
workflow.


Open questions for maintainers

A few decisions I made one way but would happily revisit:

  1. Adapter location. I put the Zag adapter in packages/angular/src/_zag
    rather than spinning up @zag-js/angular. That kept the change inside
    this repo and avoided coordinating with the Zag repo, but if you'd
    prefer the adapter live alongside @zag-js/react etc., I'll extract it.
  2. Subpath layout. Some components currently live at the package root
    (packages/angular/avatar/) and others under src/ — this happened
    because of how ng-packagr secondary entry points resolved during
    incremental development. I can normalize to a single layout.
  3. Forms as an optional peer. Matches Angular community convention but
    makes the form-bearing components slightly more involved to consume.
    Open to alternatives.
  4. Naming. I used [arkAccordion] etc. for directives; if Ark's
    maintainers prefer ark-accordion element selectors or a different
    prefix, that's a mechanical rename.

Local verification

bun install
bun run build           # builds all packages, including @ark-ui/angular
bun run test            # vitest across all packages
bun run typecheck       # includes Angular's forms-isolation check
bun run angular dev     # Storybook on :6006

The @ark-ui/angular package builds cleanly through ng-packagr to the APF
manifest in dist/; both the source and default export conditions
resolve correctly for workspace consumers and published artifacts.


Thanks again for considering this. The Ark UI architecture made the
framework port genuinely pleasant — most components were a near-mechanical
translation from the React equivalents, which is a credit to how well the
Zag/Ark boundary is drawn. Let me know how you'd like to proceed and I'll
follow up.

…ch-5-components

Implement Angular Batch 5 components
…lities

Complete Angular Batch 6 utilities
Align Angular stories with React parity
Add missing Angular components and align stories
@vercel
Copy link
Copy Markdown

vercel Bot commented May 20, 2026

@ryan-mahoney is attempting to deploy a commit to the Chakra UI Team on Vercel.

A member of the Team first needs to authorize it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant