Skip to main content
While JSON Schema defines the structure and validation of your data, UI Schema controls how the form is presented to users. It determines the layout, arrangement, and styling of form elements.

What is UI Schema?

UI Schema is an optional declarative description of how your form should be rendered. Without a UI Schema, JSON Forms generates a default vertical layout. With UI Schema, you gain fine-grained control over:
  • Element layout (vertical, horizontal, groups)
  • Element order and grouping
  • Element visibility and behavior
  • Custom options and styling
UI Schema is completely optional. If you don’t provide one, JSON Forms will automatically generate a simple vertical layout from your JSON Schema.

UI Schema Elements

All UI schema elements extend the base interface:
interface BaseUISchemaElement {
  type: string;              // Element type
  rule?: Rule;               // Conditional visibility/behavior
  options?: { [key: string]: any }; // Custom options
}

Control

A control binds to a data property using a scope:
{
  "type": "Control",
  "scope": "#/properties/name",
  "label": "Full Name"
}
interface ControlElement extends BaseUISchemaElement {
  type: 'Control';
  scope: string;  // JSON Pointer to data property
  label?: string | boolean | LabelDescription;
}
Scope syntax:
  • #/properties/name - Top-level property
  • #/properties/address/properties/city - Nested property
  • #/properties/items/items/properties/title - Array item property

Vertical Layout

Stacks elements vertically (top to bottom):
{
  "type": "VerticalLayout",
  "elements": [
    { "type": "Control", "scope": "#/properties/firstName" },
    { "type": "Control", "scope": "#/properties/lastName" }
  ]
}
interface VerticalLayout extends Layout {
  type: 'VerticalLayout';
  elements: UISchemaElement[];
}

Horizontal Layout

Arranges elements horizontally (left to right):
{
  "type": "HorizontalLayout",
  "elements": [
    { "type": "Control", "scope": "#/properties/firstName" },
    { "type": "Control", "scope": "#/properties/lastName" }
  ]
}
interface HorizontalLayout extends Layout {
  type: 'HorizontalLayout';
  elements: UISchemaElement[];
}

Group

Groups elements with an optional label:
{
  "type": "Group",
  "label": "Personal Information",
  "elements": [
    { "type": "Control", "scope": "#/properties/name" },
    { "type": "Control", "scope": "#/properties/age" }
  ]
}
interface GroupLayout extends Layout {
  type: 'Group';
  label?: string;
  elements: UISchemaElement[];
}

Label

Displays static text:
{
  "type": "Label",
  "text": "Please fill out all required fields"
}
interface LabelElement extends BaseUISchemaElement {
  type: 'Label';
  text: string;
}

Categorization

Creates tabbed or stepped navigation:
{
  "type": "Categorization",
  "elements": [
    {
      "type": "Category",
      "label": "Personal",
      "elements": [
        { "type": "Control", "scope": "#/properties/name" }
      ]
    },
    {
      "type": "Category",
      "label": "Address",
      "elements": [
        { "type": "Control", "scope": "#/properties/street" }
      ]
    }
  ]
}
interface Categorization extends BaseUISchemaElement {
  type: 'Categorization';
  label: string;
  elements: (Category | Categorization)[];
}

interface Category extends Layout {
  type: 'Category';
  label: string;
  elements: UISchemaElement[];
}

Scopes and Data Binding

Scopes use JSON Pointer syntax to reference schema properties:
// Schema
{
  "type": "object",
  "properties": {
    "name": { "type": "string" }
  }
}

// UI Schema
{
  "type": "Control",
  "scope": "#/properties/name"
}
JSON Forms automatically converts scopes to data paths internally using the toDataPath utility from packages/core/src/util/path.ts.

Rules

Rules allow conditional visibility and behavior:
interface Rule {
  effect: RuleEffect;     // HIDE, SHOW, ENABLE, DISABLE
  condition: Condition;   // When to apply the effect
}

enum RuleEffect {
  HIDE = 'HIDE',
  SHOW = 'SHOW',
  ENABLE = 'ENABLE',
  DISABLE = 'DISABLE'
}

Leaf Condition

Based on a specific value:
{
  "type": "Control",
  "scope": "#/properties/vegetarianOptions",
  "rule": {
    "effect": "SHOW",
    "condition": {
      "type": "LEAF",
      "scope": "#/properties/vegetarian",
      "expectedValue": true
    }
  }
}
interface LeafCondition extends BaseCondition {
  type: 'LEAF';
  scope: string;
  expectedValue: any;
}

Schema-Based Condition

Based on JSON Schema validation:
{
  "type": "Control",
  "scope": "#/properties/postalCode",
  "rule": {
    "effect": "ENABLE",
    "condition": {
      "scope": "#/properties/country",
      "schema": {
        "const": "US"
      }
    }
  }
}
interface SchemaBasedCondition extends BaseCondition {
  scope: string;
  schema: JsonSchema;
  failWhenUndefined?: boolean;
}

Composable Conditions

Combine multiple conditions:
{
  "rule": {
    "effect": "SHOW",
    "condition": {
      "type": "OR",
      "conditions": [
        {
          "type": "LEAF",
          "scope": "#/properties/isStudent",
          "expectedValue": true
        },
        {
          "type": "LEAF",
          "scope": "#/properties/age",
          "expectedValue": 65
        }
      ]
    }
  }
}
interface OrCondition extends ComposableCondition {
  type: 'OR';
  conditions: Condition[];
}

interface AndCondition extends ComposableCondition {
  type: 'AND';
  conditions: Condition[];
}

Options

Options provide renderer-specific customization:
{
  "type": "Control",
  "scope": "#/properties/comments",
  "options": {
    "multi": true,
    "rows": 5
  }
}
Common options:
  • multi: true - Multi-line text input
  • format: "date" - Override format
  • slider: true - Render number as slider
  • detail - Custom detail view for arrays

Complete Example

Here’s a real-world example combining multiple concepts:
{
  "type": "VerticalLayout",
  "elements": [
    {
      "type": "HorizontalLayout",
      "elements": [
        {
          "type": "Control",
          "scope": "#/properties/name"
        },
        {
          "type": "Control",
          "scope": "#/properties/personalData/properties/age"
        }
      ]
    },
    {
      "type": "Group",
      "label": "Additional Information",
      "elements": [
        {
          "type": "HorizontalLayout",
          "elements": [
            {
              "type": "Control",
              "scope": "#/properties/birthDate"
            },
            {
              "type": "Control",
              "scope": "#/properties/nationality"
            }
          ]
        }
      ]
    },
    {
      "type": "Control",
      "scope": "#/properties/vegetarian"
    },
    {
      "type": "Control",
      "scope": "#/properties/vegetarianOptions",
      "rule": {
        "effect": "SHOW",
        "condition": {
          "type": "LEAF",
          "scope": "#/properties/vegetarian",
          "expectedValue": true
        }
      }
    }
  ]
}

TypeScript Interfaces

From packages/core/src/models/uischema.ts:
type UISchemaElement =
  | BaseUISchemaElement
  | ControlElement
  | Layout
  | LabelElement
  | GroupLayout
  | Category
  | Categorization
  | VerticalLayout
  | HorizontalLayout;

interface Scopable {
  scope?: string;
}

interface Scoped extends Scopable {
  scope: string;  // Required
}

interface Labelable<T = string> {
  label?: string | T;
}

Internationalization

UI Schema supports i18n keys:
{
  "type": "Control",
  "scope": "#/properties/name",
  "i18n": "person.name"
}
JSON Forms will look for translations like:
  • person.name.label
  • person.name.description
See the i18n documentation for details.

Best Practices

Begin with just a JSON Schema and let JSON Forms generate the default layout. Only add UI Schema when you need custom layouts.
Avoid deeply nested layouts. Use Groups to organize related fields instead of complex nesting.
Complex rule logic can make forms hard to maintain. Consider breaking complex forms into multiple steps.
Use options to customize renderer behavior without creating custom renderers.

Next Steps

Renderers

Learn how renderers interpret UI Schema elements

Data Binding

Understand how scopes connect to your data

Build docs developers (and LLMs) love