@awfixer/ui
v0.1.0
Published
An opinionated React 19 component library — a Tailwind CSS v4 styled surface built on **vendored, dependency-free primitives**. Every shipped component is available in multiple variants (styled / raw / animated), published through a granular `exports` map
Downloads
294
Readme
@awfixer/ui
An opinionated React 19 component library — a Tailwind CSS v4 styled surface
built on vendored, dependency-free primitives. Every shipped component is
available in multiple variants (styled / raw / animated), published through a
granular exports map so you only ship the code you import.
Status:
0.0.1— pre-release. The component catalog below is the stable, buildable surface. Several overlay primitives (dialog,popover,select,tooltip,dropdown-menu,context-menu,menubar,combobox,calendar, …) exist insrc/ui/andsrc/primatives/and are being migrated onto vendored sources, but are not yet wired into the build. See Roadmap.
Table of contents
- What this is
- Features
- Requirements
- Install
- Quick start
- Import variants
- Component catalog
- Theming
- Architecture
- Project structure
- Development
- Example app
- Roadmap
- License
What this is
@awfixer/ui is a three-tier React UI kit:
- Primitives (
src/primatives/) — unstyled, behavior-only, accessible components (focus management, keyboard nav, ARIA, animation state machines). These are vendored from Base UI-style sources and carry no styling opinions. - Styled surface (
src/ui/) — the primitives composed with Tailwind CSS v4 utilities, a vendoredcn(clsx + tailwind-merge) and a vendoredcva(class-variance-authority). This is the opinionated layer. - Entries (
src/entries/) — the public-facing build entrypoints that re-export each variant. These are whatpackage.jsonexportspoints at.
Lineage. The styled surface started life as the shadcn/ui new-york-v4
registry and was migrated (scripts/migrate.ts) off external packages
(radix-ui, class-variance-authority, lucide-react) onto vendored sources.
@vendor/cn, @vendor/cva, and @vendor/icons are inlined at build time, so
the published package has zero runtime dependency on them.
Features
- React 19 first.
"use client"directives, the new ref model, and modern JSX runtime throughout. - Zero-runtime vendor deps.
cn,cva, and the icon set are vendored and bundled into each entry — no transitive runtime bloat. - Three import variants per component where it makes sense:
styled,raw, andanimated. - Granular
exportsmap. Tree-shakeable per-component subpaths; no barrel that pulls the whole library. - Tailwind CSS v4 design-token surface (semantic colors, dark mode).
- Typed end to end. Declarations emitted with the native TypeScript compiler
(
tsgo/@typescript/native-preview), with dev path-aliases rewritten to relative specifiers so consumer types resolve without ourtsconfig.
Requirements
| Dependency | Role | Version |
| ----------------- | ------------------- | ----------- |
| react | peer dependency | ^19.0.0 |
| react-dom | peer dependency | ^19.0.0 |
| motion | peer dependency | ^12.0.0 |
| @base-ui/react | runtime dependency | 1.5.0 |
| @floating-ui/react-dom | runtime dependency | 2.1.8 |
| next-themes | runtime dependency | 0.4.6 |
| recharts | runtime dependency | 3.8.1 |
| Tailwind CSS | styling (consumer) | v4 |
The peer dependencies must be installed by the consumer. The runtime
dependencies are resolved from the package's own dependencies. Tailwind CSS
v4 must be set up in your app — the styled components emit Tailwind utility
classes; see Theming.
Install
# bun
bun add @awfixer/ui react react-dom motion
# pnpm
pnpm add @awfixer/ui react react-dom motion
# npm
npm install @awfixer/ui react react-dom motionQuick start
Import each component from its own subpath — there is no top-level barrel:
import { Button } from "@awfixer/ui/button";
import { Badge } from "@awfixer/ui/badge";
import { Separator } from "@awfixer/ui/separator";
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from "@awfixer/ui/accordion";
export function Example() {
return (
<div className="flex flex-col gap-4">
<div className="flex gap-3">
<Button>Click me</Button>
<Badge>New</Badge>
</div>
<Separator />
<Accordion type="single" collapsible>
<AccordionItem value="a">
<AccordionTrigger>Section</AccordionTrigger>
<AccordionContent>Content</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
}Import variants
Each component that wraps a primitive is published in up to three variants. Pick the one that matches how much opinion you want:
| Subpath | What you get | When to use |
| ----------------------------- | ---------------------------------------------- | ------------------------------------ |
| @awfixer/ui/<name> | Styled — Tailwind surface, opinionated | Default. Most apps. |
| @awfixer/ui/<name>/raw | Raw — unstyled primitive, namespace import | You want full control over styling. |
| @awfixer/ui/<name>/animated | Animated — motion-powered styled surface | You want enter/exit animations. |
rawandanimatedonly exist for components backed by a primitive. Theanimatedvariant currently ships foraccordiononly.
// Styled (default)
import { Accordion } from "@awfixer/ui/accordion";
// Raw primitive — unstyled, behavior only
import * as RawAccordion from "@awfixer/ui/accordion/raw";
<RawAccordion.Root type="single" collapsible>…</RawAccordion.Root>;
// Animated — motion-powered
import { Accordion as AnimatedAccordion } from "@awfixer/ui/accordion/animated";Button (and other cva-based components) also export their variant helper so
you can apply the same styles to arbitrary elements:
import { buttonVariants } from "@awfixer/ui/button";
<a className={buttonVariants({ variant: "outline", size: "sm" })}>Link as button</a>;Component catalog
35 components are wired into the stable build. Components backed by a primitive
support the /raw variant; accordion additionally supports /animated.
Interactive (raw + styled)
accordion (+ animated), aspect-ratio, avatar, checkbox, collapsible,
direction, label, navigation-menu, progress, radio-group,
scroll-area, separator, slider, switch, tabs, toggle,
toggle-group
Presentational (styled only)
alert, badge, breadcrumb, button, button-group, card, empty,
field, input, input-group, item, kbd, native-select, pagination,
skeleton, spinner, table, textarea
Variant helpers
cva-based components export a <name>Variants object (buttonVariants,
badgeVariants, …) and accept a variant / size prop plus asChild (via the
vendored Slot primitive) to render as a different element while keeping the
styles.
Theming
The styled surface is built against the standard shadcn/ui semantic design
tokens through Tailwind CSS v4 utilities (bg-primary, text-muted-foreground,
border-ring, bg-background, bg-accent, bg-destructive, bg-card,
bg-popover, ring-ring/50, border-border, …). You must define these tokens
in your app's stylesheet for the components to render with correct colors.
Minimum setup in your global CSS:
@import "tailwindcss";
@layer base {
:root {
--background: …;
--foreground: …;
--primary: …;
--primary-foreground: …;
/* secondary, muted, muted-foreground, accent, accent-foreground,
destructive, border, input, ring, card, popover, … */
}
.dark {
/* dark-mode overrides for the same tokens */
}
}Dark mode
Dark mode is driven by a class strategy on the root element. The library pairs
naturally with next-themes:
// app/components/theme-provider.tsx
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({ children }: { children: React.ReactNode }) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</NextThemesProvider>
);
}See example/ for a complete working setup.
Architecture
src/primatives/<name>/ behavior-only primitive ─┐
├─▶ src/ui/<name>.tsx styled surface
src/entries/<name>/ public entrypoints ─────┘ │
├── styled.ts → export * from "@/ui/<name>" │
├── index.ts → export * from "@/primatives/<name>" ▼
└── animated.ts → export * from "@/ui/<name>.animated" package.json exportsPath aliases are dev-only. In development, tsconfig.json maps @/* to
src/* and @vendor/{cn,cva,icons} to packages/*/src. The build pipeline
(see below) rewrites these to relative specifiers in the emitted .d.ts and
inlines the vendored sources into the bundled JS — so the published package
never leaks the aliases or the vendored packages as dependencies.
Vendored packages (packages/)
| Package | Provides |
| -------------- | ----------------------------------------------------- |
| @vendor/cn | cn, ClassValue — clsx + tailwind-merge, dep-free |
| @vendor/cva | cva, VariantProps — class-variance-authority |
| @vendor/icons| icon set with a createIcon factory |
Build pipeline (scripts/build.ts)
- Bundle JS per entry with
bun build(ESM,naming: "[name].mjs"). Externalizesreact,react-dom,motion,recharts,@base-ui/react,@floating-ui/react-dom,next-themes; inlines everything else. - Hoist
"use client"to the top of each bundled module (Bun inlines the directive mid-bundle; the build repairs it so bundlers honor it). - Emit
.d.tsdeclarations with the native TypeScript compiler (tsgo,tsconfig.build.json,emitDeclarationOnly). - Rewrite aliases in the emitted
.d.ts—@/…and@vendor/…become relative paths, and leaked bare-specifier library types are self-contained.
Project structure
awfixer-ui/
├── src/
│ ├── primatives/ # unstyled, behavior-only primitives (vendored Base UI)
│ ├── ui/ # styled Tailwind v4 surface
│ ├── entries/ # public build entrypoints (styled / raw / animated)
│ ├── lib/ # `cn` re-export
│ ├── hooks/ # shared hooks (e.g. use-mobile)
│ ├── utils/ # internal utilities + tests
│ └── types/ # ambient type declarations
├── packages/ # vendored, dependency-free: cn, cva, icons
├── scripts/ # build, codegen, and one-shot migration tooling
├── example/ # Next.js 16 consumer app (smoke test for the exports map)
├── types/ # ambient global type declarations
├── tsconfig.json # dev config (path aliases, strict)
├── tsconfig.build.json # emit config (excludes WIP heavy-dep primitives)
├── bunfig.toml # bun install config
└── package.json # exports map, peer/runtime depsDevelopment
The repo uses Bun as its runtime and task runner.
bun install # install dependencies
bun run build # bundle JS + emit .d.ts into dist/
bun run typecheck # tsgo --noEmit
bun test # run unit tests (bun test)
bun run lint # oxlint
bun run format # oxfmt --write
bun run format:check # oxfmt --checkCodegen & migration scripts (scripts/)
| Script | Purpose |
| ---------------------- | -------------------------------------------------------------------------------------- |
| build.ts | The publish build pipeline (JS bundle + .d.ts emit + alias rewrite). |
| gen-entries.ts | Regenerate src/entries/ files + the COMPONENTS map + exports JSON. Idempotent. |
| gen-exports.ts | Emit/refresh the exports block for package.json. |
| migrate.ts | One-shot codemod: rewrite src/ui imports off radix-ui/cva/lucide-react → vendored. |
| self-contain-types.ts| Prototype: vendor leaked bare-specifier lib types into the emitted .d.ts tree. |
| render-smoke.ts | Render smoke test for the styled surface. |
| rebrand.ts | Naming/branding sweep. |
After adding or renaming a component, re-run gen-entries.ts and wire the
generated COMPONENTS map and exports block into build.ts and
package.json respectively.
Example app
example/ is a minimal Next.js 16 app that consumes the
published package via its exports map, styled with Tailwind v4 and
next-themes. It exists as an integration smoke test.
cd example
bun install
bun run devIt also serves as a reference for the consumer-side setup (ThemeProvider,
globals.css, font variables).
Roadmap
This is 0.0.1. Active work, in rough priority order:
- Vendor remaining overlay dependencies (
aria-hidden,react-remove-scroll,@floating-uiinternals) to unblock the heavy primitives currently excluded from the build:dialog,menu,popover,select,popper,tooltip,dropdown-menu,context-menu,menubar,hover-card,one-time-password-field,password-toggle-field,alert-dialog,form,toolbar,toast,tabs(animated),radio-group,switch,checkbox,slider,toggle,toggle-group,scroll-area,navigation-menu. - Expand
/animatedvariants beyondaccordion. - Type-clean the WIP
src/ui/components (combobox,calendar,chart,sidebar,drawer,sheet,sonner,command, …) and wire them into entries.
The exports map in package.json is the source of truth for what is currently
importable.
License
Source-available — not open source. Licensed under the AWFixer Source Available License v0.4.
In summary (this is not legal advice — read the full license):
- You may view, run, and modify the Software for internal, personal, educational, and small-entity (≤10 employees / <$2M USD revenue) use, including limited production use.
- You may not use it to build or enable a Competitive Offering or Functionally Equivalent software, or publish benchmarks without written consent.
- You may not perform Prohibited AI Use — training, fine-tuning, embedding, or distilling the Software into an AI System — except narrowly scoped ephemeral AI assistance under the conditions in Section 4.
- You may not redistribute, publicly host, or offer the Software as a service without a separate commercial license.
- Change date: four (4) years after first availability, this version's license automatically converts to AGPL-3.0-or-later.
For commercial licensing, contact the Licensor.
