@emeryld/ui-schema-tsx
v0.1.6
Published
Standalone React DOM + TypeScript package for rendering and editing Zod v4 object schemas.
Downloads
709
Readme
@emeryld/ui-schema-tsx
Standalone React DOM + TypeScript package for rendering and editing Zod v4 object schemas.
Install
pnpm add @emeryld/ui-schema-tsx react react-dom zodCSS
import '@emeryld/ui-schema-tsx/styles.css'Basic usage
import { SchemaTSX, useSchemaState } from '@emeryld/ui-schema-tsx'
import { z } from 'zod'
const schema = z.object({
name: z.string(),
age: z.number(),
active: z.boolean(),
})
const { value, setValue, reset, errors } = useSchemaState(
schema,
{ age: 18 },
{
onValidationChange: ({ key, errors, valid }) => {
console.log(key, errors, valid)
},
},
)
<SchemaTSX
schema={schema}
value={value}
onChange={(nextValue) => setValue(nextValue)}
onFieldChange={(key, nextValue) => console.log(key, nextValue)}
onValueClick={({ rawString, key, value }) => console.log(rawString, key, value)}
onValidationChange={(result) => console.log(result)}
/>Derived schemas and memoization
SchemaTSX and useSchemaState are resilient to derived-schema identity churn (for example inline schema.omit(...)).
For better performance, still memoize derived schemas in consumers:
const formSchema = useMemo(() => schema.omit({ hidden: true }), [schema])Custom registry factory
import { createSchemaTSXFactory } from '@emeryld/ui-schema-tsx'
const schemaTSX = createSchemaTSXFactory({
variants: {
string: {
textInput: StringTextInput,
textarea: StringTextarea,
},
number: {
textInput: NumberTextInput,
slider: NumberSlider,
},
},
})
export const SchemaTSX = schemaTSX.SchemaTSXDisplay map
display={{
name: { variant: 'textarea', props: { label: 'Name' } },
age: { variant: 'slider', props: { minValue: 0, maxValue: 100 } },
status: { variant: 'select', props: {} },
}}Variant conventions
Use component variants to change UX behavior, and wrapper variants to style element containers.
- Wrapper variants define styling of elements inside a component (for example pills in a pill selector, or menu items in a dropdown).
- Component variants define the interaction pattern/UX (for example toggle vs checkbox, or dropdown vs pill selector).
- This separation is the expected API direction everywhere, even where older components still mix the two.
Examples of expected shape:
- Dropdown should not own style variants. It should expose optional:
boxWrapperVariant(default:card)menuItemWrapperVariant(default:flat)
- Button variants should represent button UX/shape (
circle,rectangular, etc.), while visual styles should come from wrapper variants and wrapper background color.
Enum options shape
Enum variants receive options as:
Array<{ value: string; label: string }>Value handling
Interactible controls use a controlled value contract:
valueonChange
All interactive variants/components should use this pattern consistently.
Optional and nullable markers
?optional−nullable*required- nullable takes precedence when both wrappers exist
- markers are shown in editable mode by default
Intersection and TrayFolder behavior
ZodIntersection is handled explicitly. Initial values are created per side and deep-merged for plain objects. Object intersections are rendered using a tray-folder style object variant, with fallback support exported as TrayFolder.
createInitialSchemaValue
import { createInitialSchemaValue } from '@emeryld/ui-schema-tsx'
const initialValue = createInitialSchemaValue(schema)It first tries schema.safeParse(undefined) (to respect defaults/prefaults/catch/transforms), then falls back to structural generation.
