Skip to content
Kinetixui

Contributing

KinetixUI is one design source, one implementation per platform. React (@kinetixui/ui) is the source of truth; Jetpack Compose (packages/ui-compose), SwiftUI (packages/ui-swiftui) and Flutter (packages/ui-flutter) each carry a 1:1 port of the same component surface on the same token contract. Angular (packages/ui-angular) is a fifth implementation, still in preview and rolling out in waves — see Angular.

The platform rule#

A new component or block is not "done" until it exists on all four catalogue-complete platforms — React, SwiftUI, Jetpack Compose and Flutter — or is a documented, deliberate non-port on the ones where it has no native idiom. Each port:

  1. Mirrors the React source 1:1 — the same variants, sizes, states and prop names, adjusted only for platform idiom (onPressed / onChanged callbacks instead of DOM events, value + ValueChanged instead of controlled inputs, etc.).
  2. Resolves the same semantic tokens — never a hard-coded colour. Every port reads its KinetixTheme / KinetixColors / @Environment(\.kinetixColors), which come from the shared token pipeline.
  3. Carries a doc comment at the top of the file naming its packages/ui/src/components/*.tsx counterpart and stating exactly what wasn't carried over and why.
  4. Compiles in CI — see the per-platform workflow below. A port that only exists locally doesn't count.

Every platform tab must have an answer#

A component page shows a tab for all five platforms, and a tab is never left blank or filled with something invented. Where a component is not implemented on a platform, components.manifest.json carries a platformGuidance entry saying what the tab shows instead:

KindWhat the tab showsCounts as parity?
(implemented)the component's own exampleYes — this is what platforms means
native-equivalentthe platform's own idiom, compiledNo
compositionother KinetixUI components, composedNo
plannednothing — a statement and the delivery waveNo

pnpm check:manifest fails if any (component, platform) pair has neither an implementation nor guidance, if a guidance entry names a platform the component is actually on, or if a non-planned entry has no reason. pnpm check:platform-code fails in the other direction: guidance that promises a snippet and has none, and a snippet sitting under a planned gap.

Guidance is deliberately never counted as coverage. The per-platform numbers on this site mean one thing — a real implementation — and that is the only reason they are worth printing.

The standing non-ports#

These components are not ported to any native platform, by design. Each one carries composition or native-equivalent guidance per platform in components.manifest.json, so this table cannot drift from what actually ships:

ComponentWhyUse instead
Formreact-hook-form glue with no native analogueKinetixField (+ Label / Description / Message)
NavigationMenuhover-triggered desktop mega-menu, no touch idiomplatform navigation (NavigationSplitView, NavigationRail, …)
Comboboxa recipe (Popover + Command), not a standalone componentKinetixPopover + a filtered list, or KinetixSelect
NativeSelectwraps the browser's own <select> — a web-only escape hatch from Select's custom popoverKinetixSelect, which already wraps each native platform's own picker mechanism (Material3 DropdownMenu, SwiftUI Menu, Flutter MenuAnchor)
AvatarGroupre-wraps its children (React.Children.toArray) — not an idiomatic pattern on any native layout systema Row/HStack/Row of KinetixAvatars with negative spacing (spacedBy((-8).dp) / spacing: -8 / a Transform-shifted Stack)
DirectionProviderthreads text direction through Radix on the web; SwiftUI (environment(\.layoutDirection)), Compose (LocalLayoutDirection) and Flutter (Directionality) already carry layout direction in the framework itselfthe framework's own directionality value — there is nothing to wrap
Tourtargets an arbitrary already-rendered element by CSS selector — no native platform has an equivalent live-tree query, only opt-in position reporting (Modifier.onGloballyPositioned + a shared registry, a PreferenceKey, a GlobalKey)a sequence of KinetixPopover/KinetixDropdownMenu steps, each anchored to the element it explains
KanbanBoardbuilt on @dnd-kit's accessible multi-container drag-and-drop — no equivalent dependency exists in this repo's native packages, and hand-rolling pointer + touch + keyboard DnD with collision detection and live reordering from scratch on three more platforms is a much bigger lift than porting the component's own logiceach platform's own idiomatic drag primitive (Compose drag gestures + LazyColumn, SwiftUI .draggable/.dropDestination, Flutter Draggable/DragTarget), composed by hand for the specific board

Anything else that can't be ported cleanly is scoped down and documented, not silently skipped — e.g. KinetixSheet is bottom-only, KinetixSidebar drops rail mode, KinetixChart is bar-only on Flutter.

Component status#

KinetixUI tracks three different things, and they are not the same. Printing one where a reader would take it for another is how stable came to mean nothing:

Question it answersWhere it lives
Component lifecycleIs this component's API settled?status in components.manifest.json
Package maturityIs this platform's package a finished product?platformDefinitions[].maturity
Implementation verificationWhat do automated tests prove about this implementation?verification.json, derived from the tests

None is derived from another. @kinetixui/ui is a stable, published package whose catalogue's verification is beta, containing components whose lifecycle is stable — three true statements that one word cannot make. See Supported platforms for the other two; this section is about lifecycle only.

What lifecycle Beta means#

The component ships and is CI-compiled like everything else, but something about its product or API is unresolved — and that reason is recorded in the manifest as lifecycleReason. pnpm check:manifest fails on a beta component without one, and on a stable component that still carries one.

Beta is not a waiting room. It used to be: the rule was "beta for the release cycle it lands in, stable once it's shipped a cycle with no reported issues," which meant twelve components introduced in 0.12.0 were still beta at 0.22.1. That made the label a record of an unrun chore rather than a statement about the API. Time in beta is a reason to review a component; it is never the reason to promote one.

What lifecycle Stable means#

A component graduates when all of these hold:

  1. its purpose is clear and its anatomy is settled;
  2. its public API is coherent and follows the shared conventions — value / defaultValue / onValueChange, disabled, size, variant;
  3. it works controlled and uncontrolled where it carries a value;
  4. the API is unlikely to need a redesign;
  5. keyboard behaviour is defined where applicable;
  6. the accessibility contract is intentional — roles, names, states, focus;
  7. RTL behaviour is understood where relevant;
  8. empty, error, loading and disabled states are defined where relevant;
  9. the docs let someone use it without reading the source;
  10. no known P0/P1 product or API defect remains;
  11. the cross-platform adaptation strategy is understood and documented honestly.

Lifecycle Stable deliberately does not require verification Stable, visual regression on every platform, published native packages, or an Angular implementation. A component can be lifecycle stable while its SwiftUI port is verified only to experimental: one statement is about the API, the other about evidence.

Breaking changes before 1.0#

While a component is Beta, a bad API is corrected rather than preserved — directly, with a Changeset explaining why the old shape was unsuitable. That is what Beta is for. Carrying a compatibility alias to 1.0 to avoid a rename costs more than the rename. Once a component is Stable, a breaking change needs a deprecation path.

deprecated is reserved for a component being phased out; nothing has been deprecated yet.

Status and platform coverage live in one place, components.manifest.json at the repo root. pnpm gen:manifest derives component-status.json and platform-parity.json from it (the web app, the CLI registry and kinetixui parity read those), and pnpm check:manifest in CI fails if a component is missing, a status is invalid, a platform gap has no platformNote or no platformGuidance, or the generated files are stale. The data drives the /components gallery badges/filter and the <ComponentMeta> spec strip on each doc page.

Promotion is a judgement against the gate above, made when the blocker in a component's lifecycleReason is actually resolved — not a release-day chore. pnpm report:beta lists every beta component with its lifecycle blocker and, separately, its verification gaps.

Workflow for a new component#

  1. Design tokens first. If the component needs a token that doesn't exist, add it to tokens/ (DTCG), run pnpm build:tokens, and commit the regenerated packages/tokens/dist/**. The CI "generated files in sync" gate fails otherwise. If a native library needs the new semantic colour, it flows through automatically — the android-compose-theme / ios-swiftui-theme / flutter-color-scheme blocks run on both light and dark passes.
  2. React — build it in packages/ui/src/components/<name>.tsx, add a Storybook story and a demo. pnpm build:registry picks it up for the CLI automatically. Add the component to components.manifest.json (status beta, since = the version it will ship in, its platforms) and run pnpm gen:manifest — CI fails without it.
  3. Jetpack Compose — packages/ui-compose/ui/src/main/kotlin/com/kinetixui/ui/<Name>.kt. Vendored tokens: pnpm build:tokens && pnpm vendor:compose.
  4. SwiftUI — packages/ui-swiftui/Sources/KinetixUI/<Name>.swift. Vendored tokens: pnpm build:tokens && pnpm vendor:swiftui.
  5. Flutter — packages/ui-flutter/lib/src/<name>.dart, plus an export line in lib/kinetix_ui.dart. Vendored tokens: pnpm build:tokens && pnpm vendor:flutter.
  6. Docs — add the component to /docs/compose, /docs/swiftui and /docs/flutter's "what's covered" lists and to /docs/changelog.

CI — the compiler feedback loop#

None of the native toolchains are assumed to be on a contributor's machine. Each platform has a path-filtered workflow that is the sole compile check for that library — additive, never touching ci.yml / release.yml:

PlatformWorkflowRunnerCheck
React / tokensci.ymlubuntuvitest, build:registry, generated-files-in-sync, build:web
Jetpack Composenative-compose.ymlubuntugradle :ui:assembleDebug + :ui:lintDebug
SwiftUInative-swiftui.ymlmacosswift build (.macOS(.v13) target, no simulator)
Flutternative-flutter.ymlubuntuflutter pub get, flutter analyze, flutter test (widget smoke suite)

All four are path-filtered to the platform's package plus tokens/** and style-dictionary/**, so a token change re-checks every native library.

Cutting a release (the changelog page)#

Changesets opens a "version packages" PR that bumps the core npm packages — @kinetixui/tokens, @kinetixui/ui and @kinetixui/cli — together. (@kinetixui/angular is its own release cohort and versions separately; a release of one does not move the other.) That PR cannot merge until /docs/changelog has an entry for the new version — pnpm check:releases (in ci.yml) fails when the top entry in apps/web/src/lib/releases.ts isn't the version the packages are at. This exists because the page once sat at 0.6.0 for eleven releases while the packages went to 0.17.0.

  1. On the release PR's branch, add an entry at the top of RELEASES: version, date, a one-line summary, the typed changes (new / improved / fixed / security / accessibility / breaking, each with an area), and limitations for anything the release deliberately doesn't do.
  2. Set breaking explicitly — [] means "none, and I checked"; the check fails if it's missing. Add a migration note when a change needs action from users.
  3. List new components under newComponents; the page adds platform badges from platform-parity.json, so don't write coverage by hand.
  4. The per-package "changed / unchanged" indicators are generated from the package changelogs (node scripts/gen-release-meta.mjs) — run it if the check says they're stale.

Not every version has a GitHub Release page: versions between v0.5.0 and the release-automation change carry only a per-package tag, so the page links the tag unless githubReleaseUrl is set. Releases created from now on are made by scripts/release/github-releases.mjs during the release itself. If you publish one by hand, generate its notes from the same curated entry — nothing is written twice:

pnpm release:notes 0.21.0            # prints Markdown to paste into the GitHub Release
pnpm release:notes 0.21.0 --out notes.md

Then add githubReleaseUrl to that entry (never guess it — check:releases requires it to be a github.com/zedalleys/kinetixui/releases/tag/… link naming the same version) and the changelog shows "View release on GitHub" instead of "Tag on GitHub". The script only reads releases.ts and the generated package metadata; it never talks to GitHub.

The changelog page itself is searchable and its filter and search live in the URL, so any view can be shared: /docs/changelog?filter=accessibility, ?q=DataGrid, ?filter=components&q=grid.

Publishing the native ports#

React (@kinetixui/tokens / @kinetixui/ui / @kinetixui/cli) publishes to npm via Changesets on merge to main. The three native ports are consumed from a repo checkout today; the manual release paths are workflow_dispatch workflows, credential-guarded so they're inert until you add the secret:

PortWorkflowMechanismSecret
Flutterpublish-flutter.ymlflutter pub publish to pub.dev (dry-run by default)PUB_DEV_CREDENTIALS — the contents of ~/.config/dart/pub-credentials.json after dart pub login
SwiftUIpublish-swiftui.ymlgit subtree split of packages/ui-swiftui → a thin mirror repo (SwiftPM can't resolve a package in a monorepo subdirectory)SWIFTUI_MIRROR_REPO — an authenticated push URL, https://x-access-token:<PAT>@github.com/<you>/<mirror>.git
Jetpack Composepublish-compose.ymlgradle publish to Maven Central via the Central Portal's OSSRH-compatible staging API (dry-run = unsigned publishToMavenLocal); the staged deployment is then released by hand at central.sonatype.comMAVEN_CENTRAL_USERNAME / MAVEN_CENTRAL_PASSWORD (a Portal user token) and SIGNING_KEY / SIGNING_KEY_PASSWORD (an ASCII-armored GPG key — gpg --armor --export-secret-keys)

Blocks#

Blocks (the composed examples on /blocks) go further than the component rule: a published block must exist on all five platforms — React, Angular, SwiftUI, Jetpack Compose and Flutter. A block is a composition of Kinetix* primitives, so a block that works on the web is expressible from each platform's own primitives; and a reader who copies the React tab has no way to tell that the Compose tab was never written. A block on two platforms is not a smaller version of the idea, it is a different claim.

pnpm check:block-source enforces this: a block with status beta, stable or deprecated must list every platform. The one escape hatch is status draft, which may be missing platforms and which the website never publishes — gen-block-examples.mjs leaves drafts out of the generated snippets entirely, so work in progress can live in the manifest without the catalogue advertising a hole.

Block code is not written into the website. Every snippet the site shows is extracted from a real source file, and a block may only advertise a platform it has one for.

Composition, not translation. Where a platform has a better idiom, the block uses it and the file says why: the table toolbar is a list on SwiftUI because three columns on a phone stop being readable at larger text sizes, and the settings list is a semantic <ul> on Angular because the list component is in a later wave and inventing a kx-list is the one thing this architecture forbids.

Adding a block implementation#

  1. Write the real source. One file, in that platform's home:

    PlatformWhere
    Reactapps/web/src/examples/blocks/<slug>.tsx
    Angularpackages/ui-angular/src/examples/blocks/<slug>.ts
    SwiftUIpackages/ui-swiftui/Tests/KinetixUITests/Blocks/<Name>BlockTests.swift
    Composepackages/ui-compose/ui/src/test/kotlin/com/kinetixui/ui/blocks/<Name>BlockTest.kt
    Flutterpackages/ui-flutter/test/blocks/<name>_block_test.dart

    Wrap the part a reader should copy in // kx-block:start / // kx-block:end. Imports and any test around it stay outside the markers — one file, so the example and the thing the compiler checks can never be two different pieces of code.

  2. Declare it in blocks.manifest.json under that block's sources, with the explicit path. Platform names are validated against components.manifest.json's platformDefinitions; there is no second platform list.

  3. Regenerate: pnpm gen:blocks.

  4. Check: pnpm check:block-source (every claim has a file) and pnpm check:blocks (the generated snippets match the files).

  5. CI runs both, and the native workflows compile the native sources — the file paths above sit inside packages/ui-*, which is what those workflows watch.

A React block is also its own live preview: blockPreviews imports the fixture, so the code on the page and the thing rendered above it are the same component.

Coverage is explicit: a platform missing from a block's sources is not supported for that block, and nothing infers otherwise. For a published block that is a failure rather than a fact — see the five-platform rule above. block-parity.json carries the derived counts, and the /blocks page reads them rather than printing a number anyone typed.