> ## Documentation Index
> Fetch the complete documentation index at: https://developers.reintersect.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sections and forms

> Expand a block in place to collect more input: toggle buttons open sections, sections hold forms, and forms submit to typed actions.

> Reintersect apps are in developer preview. Names and limits can still change before general
> release.

Blocks can't open dialogs or pages. When an action needs more input than a click, the block expands
in place: a toggle button opens a section, and the section holds a form. Opening a section, switching
tabs and typing into a form happen in the viewer's browser and never call your app.

## Sections

Declare section names on the block, then use the handles from the render context:

```tsx theme={null}
import { defineBlock } from "@reintersect/sdk/app";
import { Actions, Block, Button, Header, Section, Text } from "@reintersect/sdk/components";
import { z } from "zod";

export const Ticket = defineBlock({
  type: "ticket",
  title: "Ticket",
  props: z.object({ title: z.string(), body: z.string() }),
  state: z.object({}),
  initialState: {},
  sections: ["details"],
}).render((props, { sections }) => (
  <Block summary={props.title}>
    <Header>{props.title}</Header>
    <Section handle={sections.details} title="Details" collapsed>
      <Text muted>{props.body}</Text>
    </Section>
    <Actions>
      <Button type="toggle" label="Details" section={sections.details} />
    </Actions>
  </Block>
));
```

Handles are typed. A string such as `section="details"` is a type error, and so is a handle for a
section the block didn't declare. A section without a handle is a plain titled group and can't be
toggled. Section names are 1 to 48 characters from `A-Z`, `a-z`, `0-9`, `_` and `-`, at most 32 per
block. `reintersect check` reports a handle used in two sections and a handle from another block.

Each viewer opens and closes sections on their own; the open state isn't stored or shared. In
read-only views, collapsed sections stay collapsed and their toggle buttons are hidden.

### Where sections go

Sections sit at the block, tab or form level, one deep, so a section can't hold another section. A
section can hold `Columns`, `Tabs` and a `Form`. It can't hold an `Actions` row: the toggle button
and any other buttons go in the block's root `Actions`, which always comes last in the body. A
collapsed section needs a `title` or a toggle button that targets its handle, so viewers have a way
to open it.

## Forms

A form collects input for an action. Give the action an `input` schema:

```ts theme={null}
import { defineAction } from "@reintersect/sdk/app";
import { z } from "zod";

export const setDeadline = defineAction(
  "setDeadline",
  {
    description: "Set the launch deadline",
    input: z.object({
      deadline: z.string(),
      notifyOwner: z.boolean().optional(),
      reason: z.string().optional(),
    }),
    state: z.object({ deadline: z.string().optional() }),
  },
  async ({ input }) => {
    if (new Date(input.deadline) < new Date()) {
      return { fieldErrors: { deadline: "Pick a date in the future" } };
    }

    return { state: { deadline: input.deadline }, notice: "Deadline set" };
  },
);
```

Then render a `Form` with that action. Its child is a function that receives one typed field handle
per input property:

```tsx theme={null}
<Section handle={sections.settings} title="Deadline" collapsed>
  <Form action={actions.setDeadline} submitLabel="Save deadline">
    {(fields) => (
      <>
        <DateTimePicker field={fields.deadline} label="Deadline" mode="date" required />
        <Toggle field={fields.notifyOwner} label="Notify the owner" />
        <TextInput field={fields.reason} label="Reason" multiline />
      </>
    )}
  </Form>
</Section>
```

Field handles carry their value type, and each input component accepts only fields it can edit:

| Component | Field type |
| - | - |
| `TextInput` | `string` |
| `DateTimePicker` | `string` or `Date` |
| `Toggle` | `boolean` |
| `Select` | A string union, or `string` with search-backed options |
| `ChoiceGroup` | The values of its declared input |

Binding a boolean field to `DateTimePicker`, or misspelling `fields.deadlne`, is a type error.
Passing an action without an `input` schema to `Form` is also a type error.

Field names are unique across the whole block, so two forms can't both have a `reason` field. Fields
inside a form can't carry `audience`; put `audience` on the `Form` itself.

<Note>
  A plain `string` field also binds to `DateTimePicker`, because the type system can't see string
  formats. `reintersect check` reads the JSON Schema `format` and reports a field component that
  doesn't fit the field, or a form that leaves out a required field.
</Note>

### Submitting

`submitLabel` is required. The form ends with its own row holding only the submit button, labelled
with `submitLabel`, so there's no cancel button and no other buttons in it. When the block has no
root `Actions` and this is its only form, the submit button is the block's primary button; otherwise
it's secondary.

A form sits at the block, section or tab level, and forms don't nest. Inside a form you can use
sections and columns to arrange the fields.

The browser enforces `required` and `maxLength` before sending. The SDK then validates the values
against the action's `input` schema before your handler runs; values that don't match fail the
action with an error. For checks the schema can't express, return `fieldErrors`: the form keeps the
member's values and shows each message beside its field. On success, the form clears.

Unsent values live in the viewer's browser, keyed by field name, so they survive the block
re-rendering while the member types.

### Args and input together

An action can take both `args`, bound when the view renders, and `input`, typed by the member:

```tsx theme={null}
<Form action={actions.reply.with({ ticketId: props.ticketId })} submitLabel="Send reply">
  {(fields) => <TextInput field={fields.body} label="Reply" multiline required />}
</Form>
```

## Tabs

`Tabs` split a block into two to six views that each viewer switches between locally. Tabs sit at
the block or section level and can't be nested, and a tab can hold sections, columns and a form.
Tabs that don't fit move into a **More** menu. In read-only views and agent text, every tab is shown
one after the other under its label. See [`Tabs`](/components/interactive#tabs-and-tab).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.