---
title: 'Conditional Fields'
description: 'Reference and usage guidance for Conditional Fields.'
---

import { Callout } from 'nextra/components';

# Conditional Fields

How to hide fields or make them read-only based on other values in the record, or on the current user.

You can reduce the complexity of a form by controlling when fields appear, and by locking fields that should not be edited in certain situations. Use `hidden` to show or hide a field, and `readOnly` to make it non-editable. Both properties accept a static `true` or `false`, or a callback that returns a boolean.

This is commonly called **conditional fields**.

## Prerequisites

- A Flexkit schema you can edit. See [Schemas](/docs/schema) for an introduction to entities and attributes.
- Familiarity with JavaScript functions, since conditions are written as callbacks.

## Examples

### Hide based on a value in the current record

Only show the `subtitle` field if the product `name` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy):

```tsx
{
  name: 'subtitle',
  label: 'Subtitle',
  scope: 'local',
  dataType: 'string',
  inputType: 'text',
  hidden: ({ record }) => !record.name,
}
```

### Hide based on a sibling field

Show a compare-at price only when the product is on sale:

```tsx
{
  name: 'onSale',
  label: 'On sale',
  scope: 'global',
  dataType: 'boolean',
  inputType: 'switch',
  defaultValue: false,
}

{
  name: 'compareAtPrice',
  label: 'Compare-at price',
  scope: 'local',
  dataType: 'float',
  inputType: 'number',
  hidden: ({ record }) => !record.onSale,
  validation: (z) => z.number().min(0, { message: 'Must be >= 0' }),
}
```

### Hide based on a status value

Show restock fields only when inventory is depleted:

```tsx
{
  name: 'inventory',
  label: 'Inventory',
  scope: 'global',
  dataType: 'int',
  inputType: 'number',
  defaultValue: 0,
}

{
  name: 'restockNote',
  label: 'Restock note',
  scope: 'local',
  dataType: 'string',
  inputType: 'textarea',
  hidden: ({ record, value }) => !value && Number(record.inventory) > 0,
}
```

Use `value` when the condition should also consider the current field. In the example above, a restock note that already has a value stays visible even after inventory is replenished.

### Set read-only based on the current user's role

Keep the SKU editable for owners; show it as read-only for everyone else:

```tsx
{
  name: 'sku',
  label: 'SKU',
  scope: 'global',
  dataType: 'string',
  inputType: 'text',
  unique: true,
  readOnly: ({ currentUser }) => currentUser?.role !== 'owner',
}
```

### Set read-only based on another field

Lock the price after a product is published:

```tsx
{
  name: 'status',
  label: 'Status',
  scope: 'global',
  dataType: 'string',
  inputType: 'select',
  defaultValue: 'draft',
  options: {
    list: [
      { label: 'Draft', value: 'draft' },
      { label: 'Published', value: 'published' },
    ],
  },
}

{
  name: 'price',
  label: 'Price',
  scope: 'local',
  dataType: 'float',
  inputType: 'number',
  readOnly: ({ record }) => record.status === 'published',
}
```

<Callout type="warning">
  You cannot return a Promise from `hidden` or `readOnly` callbacks. Conditions must be synchronous.
</Callout>

<Callout type="info">
  Mention a condition in the field `options.comment` when it might surprise editors. For example, explain that the
  compare-at price appears only when **On sale** is enabled.
</Callout>

## Static vs callback visibility

- `hidden: true` hides the field in the form **and** the list/grid column.
- `hidden: ({ record }) => …` is form-only. List columns stay visible because a callback cannot be decided without a specific record.
- Hidden fields still submit their existing values. They are only removed from the form UI.
- Currently-read-only attributes are omitted from create and update mutations.
- Currently-hidden and currently-read-only fields are not validated, so a required field the editor cannot change cannot block save.

This is different from `entity.menu.hidden`, which hides the entity from the sidebar menu.

## Studio-only callbacks

Callbacks live in your TypeScript schema (`flexkit.config.tsx`) and run in Studio. They are stripped when the schema is serialized to JSON on deploy, the same as `validation`. Deployed JSON schemas can still use static `hidden: true` and `readOnly: true`.

## Reference

### Callback properties

The `hidden` and `readOnly` callbacks receive an object with the following properties:

#### `record` (`{ [attributeName: string]: unknown }`)

The current entity values being edited. Values are unwrapped from form wrappers, so write `record.status`, not `record.status.value`. Use optional chaining when a field may be empty, for example `record?.name`.

#### `value` (`unknown`)

The current field's value.

#### `currentUser` (`object | undefined`)

The signed-in user, or `undefined` when no user is loaded:

| Field          | Type       | Description                           |
| -------------- | ---------- | ------------------------------------- |
| `id`           | `string`   | User id                               |
| `email`        | `string`   | Email address                         |
| `display_name` | `string`   | Display name                          |
| `avatar_url`   | `string`   | Avatar URL                            |
| `role`         | `string`   | Role, for example `owner` or `viewer` |
| `spaces`       | `string[]` | Space codes the user can access       |

Attributes are a flat list on each entity. There are no nested object fields, so conditions always read sibling values from `record`.


---

[View full sitemap](/docs/sitemap.md)
