rilaykit
Core concepts

Renderers

How to take full control over the HTML and layout of your forms.

Rilaykit is headless: every pixel comes from a renderer you own. Rendering has two layers — catalog renderers draw registered components, tools, and parts; structural components provide overridable layout (body, submit, flow navigation).

Catalog renderers

Attach a renderer when registering an entry, or later with .renderers(). Both are immutable and return a new instance.

lib/catalog.tsx
import { ril } from 'rilaykit';

export const catalog = ril.create().component('text', {
  name: 'Text Input',
  renderer: ({ id, props, field }) => (
    <div>
      <input
        id={id}
        value={(field?.value as string) ?? ''}
        onChange={(e) => field?.onChange(e.target.value)}
        onBlur={field?.onBlur}
        disabled={field?.disabled}
        aria-invalid={!!field?.error?.length}
        {...props}
      />
      {field?.touched &&
        field?.error?.map((e) => (
          <p role="alert" key={e.message}>
            {e.message}
          </p>
        ))}
    </div>
  ),
});

.renderers() attaches or overrides renderers on already-registered entries — useful when definitions live server-side and UI lives in a client file:

export const uiCatalog = catalog.renderers({
  components: { text: (ctx) => <TextInput {...ctx} /> },
  tools: { my_tool: (ctx) => <MyToolCard {...ctx} /> },
  parts: { text: ({ part }) => <Markdown>{part.text}</Markdown> },
});
BagRendersContext
componentsform fieldsComponentRenderContext{ id, props, field, conditions, children, meta }
toolsagent tool callsToolRenderContext{ toolCallId, state, input, output, resolve, … }
partschat message partsPartRenderContext{ part, meta }

ctx.field is the binding: value, onChange, onBlur, error, touched, disabled, isValidating.

An entry without a renderer is a valid blueprint (server-side builds, agent manifests) — validate() doesn't flag it; validateAsync() surfaces it as a warning. Rendering it through FormField throws, so every component a form displays needs one.

Structural components

Structural components ship minimal unstyled defaults tagged with data-* attributes for CSS. Pass a render-prop children to replace the markup entirely.

ComponentDefaultRender-prop context
FormBody<div data-form-body>, one <div data-form-row> per visible row{ rows: VisibleRow[] }
FormSubmit<button data-form-submit>{ submitting, submit }
FlowNext / FlowBack / FlowSkip<button> (FlowSkip hides when not skippable){ go, canGo, submitting, isLastStep, step }
FlowProgress<ol data-flow-progress> of visible steps{ steps, currentIndex, goTo }
import { Form, FormBody, FormField, FormList, FormSubmit } from 'rilaykit/react';

<Form of={myForm} onSubmit={handleSubmit}>
  <FormBody>
    {({ rows }) =>
      rows.map((row) =>
        row.kind === 'repeatable' ? (
          <FormList key={row.id} id={row.repeatable.id} />
        ) : (
          <div key={row.id} className="grid gap-4">
            {row.fields.map((f) => (
              <FormField key={f.id} id={f.id} />
            ))}
          </div>
        )
      )
    }
  </FormBody>
  <FormSubmit>
    {({ submitting, submit }) => (
      <button type="button" onClick={submit} disabled={submitting}>
        {submitting ? 'Submitting…' : 'Send'}
      </button>
    )}
  </FormSubmit>
</Form>;

On this page