Installation

A CLI copies components into your repo and brings whatever they depend on. Nothing is generated, nothing is hidden, and there is no package wrapping the code you end up with.

Quick start

Three commands
bash
npm i -D astralyx-ui
npx astralyx-ui init
npx astralyx-ui add button

init writes a components.json, drops in the two helpers every component needs, and installs the theme. add copies a component and everything it imports.

Wire up the theme

init writes the token layer to src/styles/astralyx.css. Import it from your Tailwind entry:

src/index.css
css
@import 'tailwindcss';
@import './styles/astralyx.css';

That file carries the colour tokens, the surface ladder and the keyframes components animate with. Without it everything renders unstyled.

Adding components

Dependencies come along without being asked. data-grid pulls table, checkbox and empty; anything needing an npm package gets it installed.

Working with the registry
bash
npx astralyx-ui add data-grid dialog toast   # several at once
npx astralyx-ui add --all                    # every component
npx astralyx-ui list                         # the catalogue, by category
npx astralyx-ui info data-grid               # what it would bring with it
npx astralyx-ui add button --dry-run         # look before you leap
Files are never replaced by accident
add skips anything that already exists and says so. Once you have edited a component it is yours — pass --overwrite only when you actually mean to lose those changes.

Where things go

components.json holds both the folders files are written to and the aliases their imports use, so you can keep components anywhere and the imports follow.

components.json
json
{
  "tsx": true,
  "tailwind": { "css": "src/index.css" },
  "aliases": {
    "ui": "@/components/ui",
    "primitives": "@/components/primitives",
    "lib": "@/lib"
  },
  "paths": {
    "ui": "src/components/ui",
    "primitives": "src/components/primitives",
    "lib": "src/lib",
    "styles": "src/styles"
  }
}

Your bundler needs to resolve whatever alias you choose. For Vite and TypeScript that is one entry each:

vite.config.ts
tsx
import { fileURLToPath, URL } from 'node:url'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: {
    alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },
  },
})

What gets installed

The CLI itself has no runtime dependencies. These are what the components need, and only the ones you actually use are added:

PackageNeeded by
cnEvery component. Backs the cn() helper that resolves conflicting Tailwind classes by argument order.
class-variance-authorityAnything with variants — buttons, badges, inputs.
lucide-reactAnything with an icon.
shikiCodeBlock, QueryEditor and the other highlighted surfaces.
react-hook-formForm only.
Do not skip the cn() wrapper
Several components rely on it to resolve conflicts by argument order — a size’s radius overriding the base one, a variant cancelling a default. Dropping it breaks those silently, because both classes then apply and stylesheet order decides.

Then use it

Use it
tsx
import { Button } from '@/components/ui/button'

<Button color="blue" size="lg">Deploy</Button>

The file is in your repo now. Edit it, rename it, delete half of it — nothing upstream will argue.