jsonforms-shadcn-renderers
v0.2.0
Published
shadcn/ui-styled renderer set for JSONForms, with full control/layout coverage and overridable components
Downloads
92
Maintainers
Readme
jsonforms-shadcn-renderers
A JSONForms renderer set styled with shadcn/ui.
It's self-contained — built directly on Radix primitives (the default) or on
Base UI (via the /base-ui entry) and Tailwind, the same way npx shadcn
add would generate a component — so it needs no dependency on your project's local
components/ui files, and every component is individually overridable if you want to use your
own anyway.
Installation
npm install jsonforms-shadcn-renderers @jsonforms/core @jsonforms/reactTailwind setup (required)
This package ships no CSS of its own — every class it uses is a static Tailwind utility
baked into its compiled JS, styled entirely through your project's existing shadcn theme
tokens (--primary, --background, --radius, ...). Tailwind v4 excludes node_modules from
its automatic source scan, so you need one extra line in your global stylesheet telling it to
look inside this package:
@import 'tailwindcss';
@source '../node_modules/jsonforms-shadcn-renderers';(adjust the relative path to wherever your CSS file actually sits relative to node_modules).
Add the package to your content glob in tailwind.config.js, and make sure
tailwindcss-animate (or tw-animate-css) is installed for the animation utilities some
components (Accordion, Select, Popover) use:
module.exports = {
content: ['./src/**/*.{ts,tsx}', './node_modules/jsonforms-shadcn-renderers/dist/**/*.{js,cjs}'],
plugins: [require('tailwindcss-animate')],
};Basic usage
No provider, no config — pass the renderer and cell arrays straight into <JsonForms>:
import { JsonForms } from '@jsonforms/react';
import { shadcnRenderers, shadcnCells } from 'jsonforms-shadcn-renderers';
function MyForm() {
return (
<JsonForms
schema={schema}
uischema={uischema}
data={data}
renderers={shadcnRenderers}
cells={shadcnCells}
onChange={({ data, errors }) => console.log(data, errors)}
/>
);
}Using Base UI
The same renderers are also available built on Base UI (@base-ui/react)
instead of Radix. The public API is identical (shadcnRenderers, shadcnCells,
createShadcnRenderers, createShadcnCells, JsonFormsShadcnProvider, useUIComponents,
withComponentsOverride, and the types) — only the import path changes:
import { shadcnRenderers, shadcnCells } from 'jsonforms-shadcn-renderers/base-ui';The built-ins are shadcn's base-nova components. To override them, use the output of
npx shadcn add from a base-nova project as drop-ins, exactly like the Radix flavor:
import { JsonFormsShadcnProvider } from 'jsonforms-shadcn-renderers/base-ui';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
<JsonFormsShadcnProvider
components={{
Input,
select: {
Root: Select,
Trigger: SelectTrigger,
Value: SelectValue,
Content: SelectContent,
Item: SelectItem,
Group: SelectGroup,
Label: SelectLabel,
Separator: SelectSeparator,
},
}}
>
<MyForm />
</JsonFormsShadcnProvider>;Two differences from the Radix flavor:
- The multi-select
options: { format: 'combobox' }renders with Base UI Combobox + chips instead of cmdk. To override it, replace thecomboboxfamily (notcommand, which doesn't exist here). - There is no
popover.Anchor; the combobox family'suseAnchorhandles anchoring.
The Base UI compound families are select, radioGroup, popover, calendar, combobox,
table, tabs, accordion, and card; the simple slots are the same as in the Radix flavor.
No Radix at runtime. The /base-ui entry never imports Radix or cmdk, so bundlers don't
include them (they are still installed as dependencies of the package for the root entry). This
is enforced by npm run check:no-radix (part of check:package, so it runs before every
publish), by ESLint import restrictions, and by a module-graph test.
The Tailwind setup is the same — the one @source line covers both
entries.
Don't import both entries in one app: each has its own context, so a provider from one won't reach renderers from the other.
Supported controls, layouts, and combinators
| Schema / uischema shape | Renders as |
| ------------------------------------------------------------------------------------ | -------------------------------------------------- |
| type: 'string' | Text input |
| type: 'string', options: { multi: true } | Textarea |
| format: 'date' | Date picker (Popover + Calendar) |
| format: 'time' | Time input |
| format: 'date-time' | Date + time picker |
| type: 'number' / 'integer' | Number input |
| minimum + maximum + default, options: { slider: true } | Slider |
| type: 'boolean' | Checkbox |
| type: 'boolean', options: { toggle: true } | Switch |
| enum: [...] | Select |
| enum: [...], options: { format: 'radio' } | Radio group |
| oneOf: [{ const, title }, ...] | Select |
| oneOf: [{ const, title }, ...], options: { format: 'radio' } | Radio group |
| type: 'array', items.enum: [...] | Checkbox list |
| type: 'array', items.enum: [...], options: { format: 'combobox' } | Searchable multi-select combobox |
| type: 'array', items: { type: 'string' \| 'number' \| ... } | Add/remove list |
| type: 'array', items: { type: 'object' } | Table (add/remove rows) |
| type: 'array', items: { type: 'object' }, options: { layout: 'master-detail' } | Accordion, one item per entry |
| type: 'object' | Recursively rendered child layout |
| oneOf (object-level) | One-branch selector + the selected branch's fields |
| anyOf (object-level) | Same single-branch UX as oneOf (see note below) |
| allOf (object-level) | Every branch's fields rendered together |
| VerticalLayout / HorizontalLayout | Flex column / row |
| Group | Card with a title |
| Label | Heading |
| Categorization | Tabs |
| a rule (SHOW/HIDE/ENABLE/DISABLE) on any element | Respected automatically |
Validation is computed by JSONForms core itself (Ajv) — this package only displays the
resulting error messages, required-field markers, and description help text; there's no
validation logic of its own to configure.
A note on anyOf: JSON Schema's anyOf technically allows data to satisfy more than one
branch at once, but a form UI editing multiple overlapping branches simultaneously isn't
practically meaningful, so — matching how JSONForms' own official renderer sets handle it —
anyOf is treated the same as oneOf: pick one branch, edit it.
Cells vs. controls
Every control above also has a compact "cell" counterpart (shadcnCells) — the same field
without its label/description/error chrome, automatically used for the columns inside an
array-of-objects table instead of the full control.
Overriding components
By default every field is one of this package's own built-in components. You can override any
subset — either app-wide with a provider, or scoped to a single <JsonForms> instance without
one.
Simple slots (Input, Textarea, Label, Checkbox, Switch, Slider, Button,
Badge, Separator) can be overridden individually. Compound families (select,
radioGroup, popover, calendar, command, table, tabs, accordion, card) must be
replaced as a whole — Radix/cmdk/Base UI compound pieces share internal context, so mixing a
custom select.Trigger with the built-in select.Content breaks at runtime. (In the Base UI
flavor command is replaced by combobox; see Using Base UI.)
Because every built-in is generated the same way npx shadcn add would generate it for your
own project, your own literal @/components/ui/input.tsx (or a whole custom select family)
is a drop-in replacement with zero adapter code.
App-wide, via provider
import { JsonFormsShadcnProvider } from 'jsonforms-shadcn-renderers';
import { Input } from '@/components/ui/input';
function App() {
return (
<JsonFormsShadcnProvider components={{ Input }}>
<MyForm />
</JsonFormsShadcnProvider>
);
}Scoped, without a provider
import { createShadcnRenderers, createShadcnCells } from 'jsonforms-shadcn-renderers';
import { Input } from '@/components/ui/input';
const renderers = createShadcnRenderers({ Input });
const cells = createShadcnCells({ Input });
<JsonForms renderers={renderers} cells={cells} /* ... */ />;Live example
The apps/playground app in this repo is a real Vite + shadcn/ui
project with this package installed as a plain dependency — a page per control family, a
kitchen-sink page exercising everything at once, and an overrides page. Run it locally with
npm run dev from the repo root. apps/playground-base-ui is
the same thing for the Base UI flavor (jsonforms-shadcn-renderers/base-ui); run it with
npm run dev:base-ui.
Local development
From the repo root:
npm run dev:lib # build the renderer package in watch mode
npm run test # run its test suite
npm run typecheck # typecheck every workspace
npm run lint # lint every workspaceHow the vendored Base UI components are regenerated is described in
shadcn-base-ui/README.md.
License
MIT. Components in src/components/ui and src/components/base-ui are derived from shadcn/ui (MIT, © shadcn); see LICENSE.
