Astra UI

A published React component library with type-safe variants, polymorphic primitives, accessibility tested, automated CI/CD

RoleAuthor / Maintainer
TypeOpen Source Library
StackReact 18 / TypeScript / Tailwind
Distributionnpm · @astra-ui-lib/core
01Overview

Astra UI is a React component library I built to solve a specific problem: every team I've worked on rebuilds the same dozen primitives (buttons, inputs, dialogs, tooltips), but the variants always end up scattered across className chains, prop drilling, and inconsistent APIs. The library packages those primitives with a type-safe variant system, polymorphic component patterns, and accessibility tested in CI from day one.

It ships as @astra-ui-lib/core on npm with tree-shakeable ESM and CJS outputs. The repo is a Turborepo monorepo with the core library, a documentation site, and downstream consumer fixtures sharing the same TypeScript config. Every PR runs unit tests, type checks, and jest-axe accessibility assertions before merge. Releases are fully automated through Changesets. Open a PR with a changeset, merge it, and a GitHub Action publishes a new version with a generated changelog.

The bigger goal: turn a familiar engineering pattern (component libraries) into a reusable artifact, and use it as a place to push on the parts of frontend that are usually left undone: accessibility, type safety, semver discipline.

02Key Features
Type-safe Variants (CVA)
Class Variance Authority drives variant APIs. Variants are declared once, types are inferred, and invalid combinations are caught at compile time, with no string-template gymnastics.
Polymorphic Components
Every primitive accepts an as prop with full TypeScript inference. Render a Button as an <a> and the link-only props become available; render it as a <button> and they don't.
Accessibility Tested in CI
Every component has jest-axe assertions covering its rendered states. PRs that introduce a11y violations fail before review.
Tree-shakeable ESM + CJS
Rollup bundles per-component entry points so consumers only ship the code they import. Both ESM and CJS outputs with proper exports map for modern and legacy bundlers.
Automated Semver via Changesets
Contributors describe changes in changeset files. A GitHub Action collects them, computes the next version, generates a changelog, tags the release, and publishes to npm.
Tailwind-Native Theming
Components consume Tailwind tokens through CSS variables, so consumers can theme the library by overriding tokens in their own Tailwind config, with no separate theme provider.
03Variant API

Variants are the most common pain point in component libraries. CVA lets you declare them as data instead of conditional class logic, and the types fall out of the declaration:

const buttonStyles = cva(
  "inline-flex items-center justify-center font-medium",
  {
    variants: {
      intent: {
        primary: "bg-accent text-white",
        secondary: "border border-fg/10 text-fg",
        ghost: "hover:bg-fg/5 text-fg",
      },
      size: {
        sm: "h-8 px-3 text-sm",
        md: "h-10 px-4 text-base",
        lg: "h-12 px-6 text-lg",
      },
    },
    defaultVariants: { intent: "primary", size: "md" },
  }
);

// Usage: fully typed, IntelliSense-friendly:
<Button intent="ghost" size="sm">Cancel</Button>

The type of intent is the union of its keys, the type of size is the union of its keys, and a consumer who passes intent="invalid" gets a compile error. No runtime validation, no PropTypes, no string fall-through.

04Polymorphic Components

A common library bug: a Button renders as an <a> when you pass href, but TypeScript still thinks it's a button and offers type as a prop. The polymorphic pattern fixes this by making as a generic that drives the prop intersection:

<Button as="a" href="/login">Sign in</Button>
// `href` is required and typed as string
// `type` is NOT in autocomplete

<Button as="button" type="submit">Submit</Button>
// `type` is required and typed as button-type union
// `href` is NOT in autocomplete

Each primitive uses the same PolymorphicComponentProps helper, so the API is consistent across the library and the type narrowing works the same way everywhere.

05Build & Release Pipeline

The repo is a Turborepo monorepo. The core package is what ships to npm; the docs package is a separate Next.js site that imports the published library to pin documentation to a real released version. CI uses Turborepo's remote cache so unchanged packages skip work entirely on every PR.

Releases are driven by Changesets. A contributor adds a markdown changeset describing what changed and at what semver level (patch / minor / major). A GitHub Action watches the main branch, and when it sees pending changesets, it opens or updates a "Release PR" with the computed version bumps and changelog. Merging that PR runs another action that runs the Rollup build, publishes to npm with provenance, tags the release, and creates a GitHub Release with the generated notes.

The end state: a contributor only writes a changeset, never touches version numbers or changelogs, and releases happen on their own.

06Tech Stack
React 18UI framework
TypeScriptType safety
Tailwind CSSStyling tokens
CVAVariant system
TurborepoMonorepo + caching
RollupLibrary bundler
ChangesetsVersioning + changelog
GitHub ActionsCI / CD
jest-axea11y testing
VitestUnit testing
npmDistribution
tsup / RollupESM + CJS output