Rajveer Singh✕
Open Source · Published on npm

A React UI
Library That Builds
Itself From JSON

v0.50.2 · live on npmMIT1 runtime dependencynpmjs.com/package/orynn ↗
Introduction

orynn is a strongly-typed React UI library I designed, built and published on my own — a JSON-driven form engine wrapped in a shadcn-aligned component set of 71 accessible components, all sharing one semantic token layer.

It exists because of a pattern I kept hitting in enterprise work: every internal tool needs the same forty forms, and every team re-implements them by hand. orynn inverts that. You describe a form as a config object — fields, validation, conditional visibility, column layout — and the library renders it, validates it, and hands you the values back, fully typed.

Around that engine sits everything the rest of an app needs: dialogs, data tables, command palettes, date and colour pickers, toasts. One CSS file, one runtime dependency, ESM and CJS builds, and 573 tests keeping it honest across 53 releases.

$ npm i orynn
React ≥ 18 is the only required peer. One import for the stylesheet, one provider at the root, and nothing else to configure.
Orynn DataTable with sorting, row selection, status badges, totals and pagination
DataTable — sorting, selection, badges, totals, pagination and export, all driven by a single column config.
71
Components
573
Tests Passing
26.8K
Lines of Source
53
Releases Shipped
1
Runtime Dependency

One Config,
One Form

The engine

A FieldsetConfig is a plain object — serialisable, storable, sendable from a backend. The type is a discriminated union on type, so a dropdown cannot compile without options and an unknown field type is a build error, not a runtime surprise.

billing-form.tsx
import { Fieldset, type FieldsetConfig } from "orynn";
import "orynn/styles.css";

const config: FieldsetConfig = {
  legend: "Billing details",
  columns: 2,
  fields: [
    { name: "company", type: "input", label: "Company",
      validation: { required: true, minLength: 2 } },

    { name: "country", type: "dropdown", label: "Country",
      validation: { required: true },
      options: [
        { label: "India", value: "IN" },
        { label: "United Arab Emirates", value: "AE" },
      ] },

    // Rendered and validated only while the condition holds.
    { name: "gstin", type: "input", label: "GSTIN",
      when: { field: "country", eq: "IN" },
      validation: { required: true, pattern: "^[0-9A-Z]{15}$" } },
  ],
};

<Fieldset config={config} onChange={(values) => save(values)} />;
01
Conditional fields
when takes a declarative target ({ field, eq, in, truthy }) or a predicate over all values. Hidden fields stop validating — no phantom errors blocking a submit.
02
Pluggable validation
Built-in rules cover the common cases; anything implementing Standard Schema — Zod, Valibot, ArkType — drops in as a resolver without the library depending on any of them.
03
Fluid column layout
columns is a ceiling, not a fixed grid: fields reflow down to a minimum track width, with per-field spans overriding the fieldset when a control needs room.
04
Headless when you need it
useFieldsetState exposes the reducer behind the renderer, and a field registry lets you register your own control types against the same config schema.

The Component
Surface

01 — Form controls
FieldsetFormFieldInputTextareaNumberFieldDropdownDatePickerColorPickerCascaderTreeSelectPinInputTagsInputMentionsCheckboxCheckboxGroupRadioRadioGroupSwitchSliderRatingFileUpload
02 — Overlays & commands
DialogAlertDialogDrawerPopoverTooltipHoverCardDropdownMenuContextMenuMenubarCommandPopconfirmTour
03 — Navigation
TabsBreadcrumbPaginationStepperAnchorNavigationMenuBackTopAffix
04 — Data display
DataTableTableDescriptionsTimelineTreeViewTransferStatCarouselImageQRCodeAvatarBadgeMarquee
05 — Feedback
AlertToastProgressSpinnerSkeletonResultEmptyWatermark
06 — Layout & primitives
CardSeparatorAccordionCollapsibleScrollAreaAspectRatioSplitterToggleToggleGroupKbdLabel

One Token Layer,
Every Component

Theming

Every component reads from the same semantic OKLCH tokens — background, foreground, muted, border, primary, ring. Presets recolour the primary and the focus ring only; backgrounds stay neutral, so a theme switch never turns an app into a colour wash.

OrynnProvider has no styling runtime at all. It resolves your config into --orynn-* custom properties and sets them. Your own className always wins over the library's.

default
Neutral, ships as the base
slate
Cooler greys
teal
The docs site default
violet
High-chroma action
rose
Warm accent
emerald
Confirmation-led UI
main.tsx
// The provider writes CSS variables. That is the entire styling runtime.
<OrynnProvider
  theme={{
    primary: "#4f46e5",      // any CSS colour — hex, oklch(), hsl()
    radius: "md",            // one knob; sm/md/lg/xl derive with calc()
    density: "comfortable",  // compact | comfortable | spacious
    colorScheme: "dark",     // also stamps the .dark class
  }}
>
  <App />
</OrynnProvider>
6 colour presets3 densities6 radius keywordsLight / dark by classCustom primary & ringFont & base sizeRaw var escape hatch

Hard Problems
I Solved

Challenge 01

A Theme System That Doesn't Multiply the CSS

The Problem: The naive approach to presets is to fork the palette per theme. With 71 components and six presets, times light and dark, times three densities, the stylesheet and the surface area you have to test both explode.

My Solution: One semantic token layer in OKLCH that every component consumes, and presets that move only the primary and ring channels. Radius is a single --orynn-radius knob with the sm/md/lg/xl scale derived through calc(). When a consumer passes an arbitrary primary colour, the library parses its lightness — understanding hex, oklch() and hsl() — and derives a readable foreground automatically instead of asking them for one.

Challenge 02

Typing a Form That Doesn't Exist Until Runtime

The Problem: JSON-driven UI usually means giving up type safety. A config is data; the compiler has nothing to check against, so mistakes surface as a blank field in production.

My Solution: FieldConfig is a discriminated union keyed on the field type, so each variant carries exactly its own props — a dropdown without options fails to compile, an unregistered field type is rejected by the union, and switch narrowing works inside custom renderers. The rules are locked down with expect-type assertions that run as a compile-time test suite alongside the runtime one.

Challenge 03

Writing a QR Encoder That Actually Scans

The Problem: The QRCode component encodes from scratch rather than pulling in a dependency — and a QR code that renders is not the same as a QR code that decodes. Three bugs shipped before a phone camera caught them: the Reed–Solomon streaming division folded in the generator polynomial from the wrong index, alignment patterns whose centre sits on row 6 were being skipped by the timing pattern on version 7 and above, and the mask scorer was missing the 1:1:3:1:1 finder-like penalty rule.

My Solution: Fixed the generator index, reordered alignment ahead of timing, and implemented the missing penalty rule — then wrote a round-trip test that reads the matrix back with an independent decoder and asserts the error-correction syndromes come out clean. Regressions of that class can no longer reach a release.

Challenge 04

Publishing Dual ESM / CJS Without Breaking Consumers

The Problem: A library this size is judged on the install. Wrong exports maps, a single .d.ts served to both module systems, or a missing sideEffects hint and consumers get broken types, duplicate runtimes, or all 71 components in a bundle that imports one button.

My Solution: tsup emits a real dual build — index.js, index.cjs, and separate .d.ts / .d.cts — behind a conditional exports map with the stylesheet on its own subpath. sideEffects is narrowed to CSS so bundlers can tree-shake freely, and publint plus are-the-types-wrong gate every release, so packaging correctness is verified rather than assumed.

Ship it as a design system for humans and for the agents rewriting their UI.

AI tooling

orynn ships its own agent skill. One installer — npx @rajveer-singh-18/orynn-skill — detects the coding agent in a repo and drops the design-system rules where that agent looks for them: a skill package for Claude, a rule file for Cursor or Windsurf, an instructions file for Copilot.

The skill carries the token reference, the layout conventions and a redesign brief template, so an agent restyling an existing app maps markup onto real orynn components and tokens instead of inventing CSS. The docs site generates the brief for you.

Engineering
& Packaging

Language
TypeScript 5.7 (strict), expect-type
Public types are a tested artefact, not a by-product
Runtime
React ≥ 18 (peer), radix-ui
The single dependency — focus traps, portals, ARIA wiring
Styling
Hand-authored CSS, semantic OKLCH tokens
One stylesheet, no CSS-in-JS, no consumer build step
Form engine
Custom reducer, field registry, condition resolver
Config → state → validation → render
Validation
Built-in rules + Standard Schema resolver
Zod / Valibot / ArkType plug in; none are dependencies
Build
tsup (esbuild), dual ESM + CJS, sourcemaps
index.js, index.cjs, .d.ts and .d.cts
Tests
Vitest, Testing Library, user-event, jsdom
573 tests across 86 files, including dark-mode coverage
Quality gates
Biome, publint, are-the-types-wrong
Lint, format and package correctness on every release
Distribution
npm — orynn, MIT, Changesets
53 tagged releases with a hand-written changelog
AI tooling
@rajveer-singh-18/orynn-skill
Agent-installable design-system rules for Claude, Cursor, Windsurf, Copilot
Role
Sole author — design, engineering, docs, release
Author
Rajveer Singh
Documentation
Version
0.50.2 · 53 releases
Licence
MIT
Core
TypeScript, React 18+, radix-ui
Tooling
tsup, Vitest, Biome, Changesets, publint, attw
Surface
71 components · 73 documentation pages
Status
Published and maintained — 2026