Skip to main content
skill CX Codex

mantine-form — Claude Code skill: build forms with @mantine/form

A skill for building forms with @mantine/form: validation rules, zod and valibot schemas, nested fields and arrays. Useful when your agent writes Mantine forms.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add mantinedev/skills --skill mantine-form -a claude-code
Open on GitHub

What mantine-form does

The skill covers useForm from @mantine/form and is written for Mantine 9.x. It walks through the order of work: set up the form, wire inputs with getInputProps, handle submission with onSubmit. Validation gets its own section — a rules object, Standard Schema libraries (zod, valibot, arktype) through the built-in schemaResolver, functions for cross-field logic, async checks.

There is a mode table. Uncontrolled is the recommended mode, but the default is 'controlled', so you set it explicitly. In uncontrolled mode every input needs key={form.key('path')} — without it the value does not update after form.setFieldValue, form.setValues or form.reset. Checkboxes and switches need { type: 'checkbox' }, and components without an error prop (Slider, Rating, SegmentedControl) need { withError: false } plus your own error output.

The skill also spells out what rerenders in uncontrolled mode: typing does not rerender the form, and form.getValues() during render is not updated by typing — use form.useWatchValue(path) for that. The file is cut off, so the full API list in references/api.md and patterns like multi-step forms or loading initial values from a server are worth opening through the links inside the skill. Fields validate on submit by default — turn on blur validation and an error that appears on click can shift the layout so the click misses the submit button.

What it helps with

  • set up a form with useForm and field validation
  • hook a zod schema into a form without a resolver package
  • handle nested object and array fields
  • share form state across components
  • pick between controlled and uncontrolled mode

Example prompts for the agent

  • Build a signup form with @mantine/form in uncontrolled mode, validating email and password
  • Add a zod schema to this form through schemaResolver with { sync: true }
  • Render a members list with insertListItem and validate on blur using FORM_INDEX

How to install mantine-form

1Skills CLI (Vercel)

npx skills add mantinedev/skills --skill mantine-form -a claude-code

2Manually: put the files in place

~/.claude/skills/mantine-form/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/mantine-form/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about mantine-form

How do I install mantine-form in Claude Code?

Run npx skills add mantinedev/skills --skill mantine-form -a claude-code. The skill lands in ~/.claude/skills/mantine-form/SKILL.md.

What does the mantine-form skill do?

It tells the agent how to build forms with @mantine/form: useForm setup, validation rules, schemas, nested fields, modes and rerender behaviour.

Do I need a separate package for schema validation?

No, schemaResolver ships with @mantine/form. For synchronous schemas pass { sync: true } so form.validate() returns a plain result instead of a promise.

Source text

SKILL.md · MIT
---
name: mantine-form
description: >
  Build forms using @mantine/form. Use this skill when: (1) setting up a form with useForm,
  (2) adding validation rules, schema validation (zod, valibot, arktype) or async validation,
  (3) working with nested object or array fields, (4) sharing form state across components
  with createFormContext, (5) choosing between controlled and uncontrolled mode, (6) reading
  form values during render with form.useWatchValue, (7) using the standalone useField hook, or
  (8) any task involving useForm, getInputProps, onSubmit, insertListItem, or form validation.
---

# Mantine Form Skill

Written for Mantine 9.x.

## Core Workflow

### 1. Set up the form

Uncontrolled mode is the recommended mode for all forms. The default is `'controlled'`, so set it explicitly.

```tsx
const form = useForm({
  mode: 'uncontrolled',
  initialValues: {
    email: '',
    age: 0,
  },
  validate: {
    email: isEmail('Invalid email'),
    age: isInRange({ min: 18 }, 'Must be at least 18'),
  },
});
```

### 2. Wire inputs with getInputProps

In uncontrolled mode every input needs `key={form.key('path')}`. Without it the input does not
update after `form.setFieldValue`, `form.setValues` or `form.reset`.

```tsx
<TextInput label="Email" key={form.key('email')} {...form.getInputProps('email')} />
<NumberInput label="Age" key={form.key('age')} {...form.getInputProps('age')} />
```

Checkboxes and switches need `{ type: 'checkbox' }`:

```tsx
<Checkbox
  label="I agree"
  key={form.key('agreed')}
  {...form.getInputProps('agreed', { type: 'checkbox' })}
/>
```

`Select`, `NumberInput`, `Radio.Group`, `Checkbox.Group` and other Mantine inputs with a `value` /
`onChange` pair work with plain `getInputProps` and a `key`:

```tsx
<Radio.Group label="Delivery" key={form.key('delivery')} {...form.getInputProps('delivery')}>
  <Radio value="standard" label="Standard" />
  <Radio value="express" label="Express" />
</Radio.Group>
```

For standalone radios without `Radio.Group`, use `form.getInputProps('color', { type: 'radio', value: 'red' })`
and add `key={form.key('color')}` to each radio.

Components without an `error` prop (`Slider`, `RangeSlider`, `Rating`, `SegmentedControl`, `Chip.Group`)
need `{ withError: false }`, and you render the error yourself:

```tsx
<Slider key={form.key('stock')} {...form.getInputProps('stock', { withError: false })} />
{form.errors.stock && <Text c="red" size="sm">{form.errors.stock}</Text>}
```

### 3. Handle submission

```tsx
<form onSubmit={form.onSubmit((values) => console.log(values))}>
  ...
  <Button type="submit">Submit</Button>
</form>
```

`onSubmit` only calls the handler when validation passes. If the handler returns a promise,
`form.submitting` is `true` until it settles. To handle failures:

```tsx
form.onSubmit(
  (values, event) => save(values),
  (errors, values, event) => console.log('Validation failed', errors) // { 'user.email': 'Invalid email' }
);
```

## Validation

### Rules object (most common)

```tsx
validate: {
  name: isNotEmpty('Required'),
  email: isEmail('Invalid email'),
  password: hasLength({ min: 8 }, 'Min 8 chars'),
  confirmPassword: matchesField('password', 'Passwords do not match'),
}
```

### Schema (zod, valibot, arktype and other Standard Schema libraries)

```tsx
import { z } from 'zod/v4';
import { schemaResolver, useForm } from '@mantine/form';

const schema = z.object({
  email: z.email({ error: 'Invalid email' }),
  age: z.number().min(18, { error: 'Must be at least 18' }),
});

const form = useForm({
  mode: 'uncontrolled',
  initialValues: { email: '', age: 0 },
  validate: schemaResolver(schema, { sync: true }),
});
```

`schemaResolver` is built in: no resolver package is needed. Pass `{ sync: true }` for
synchronous schemas so that `form.validate()` returns a plain result instead of a `Promise`.

### Function (for cross-field logic)

```tsx
validate: (values) => ({
  endDate: values.endDate < values.startDate ? 'End must be after start' : null,
});
```

### When to validate

By default fields are validated on submit only.

```tsx
validateInputOnChange: true,            // also validate every field when it changes
validateInputOnChange: ['email'],       // only the listed fields
validateInputOnBlur: ['email'],         // same options, on blur
validateInputOnBlur: [`members.${FORM_INDEX}.email`], // list items, FORM_INDEX is exported from @mantine/form
```

With validation on blur, an error that appears when the user presses the submit button shifts the
layout, and the click can miss the button. Keep the submit button in a place that does not move
when errors appear (for example a footer with fixed position), or validate on change instead.

## Modes

|                                  | `'uncontrolled'` (recommended)      | `'controlled'` (default) |
| -------------------------------- | ----------------------------------- | ------------------------ |
| Values storage                   | Ref                                 | React state              |
| `form.values`                    | Not updated, use `form.getValues()` | Updated on every change  |
| Re-renders on value change       | No                                  | Yes                      |
| Input props                      | `defaultValue` + `onChange`         | `value` + `onChange`     |
| `key={form.key(path)}` on inputs | Required                            | Not needed               |

## What rerenders in uncontrolled mode

- Typing in an input does not rerender the form.
- `form.getValues()` during render is not updated by typing. To show or hide part of the form
  based on a value, use `form.useWatchValue(path)`. Nested paths work: `form.useWatchValue('members.0.role')`.
- `setFieldValue`, `setValues`, `insertListItem`, `removeListItem`, `reorderListItem`, `reset` and
  `initialize` do rerender, so rendering a list from `form.getValues().items.map(...)` is correct.
- `form.errors`, `form.isDirty()`, `form.isDirty('path')`, `form.isTouched()`, `form.submitting` and
  `form.validating` can be used during render in both modes: the form rerenders when they change.

```tsx
const shipsInternationally = form.useWatchValue('shipsInternationally');
```

## References

For anything beyond a basic form, read both references before writing code:

- **[`references/patterns.md`](references/patterns.md)** — read for: reusing one form for different records (edit dialogs), focusing the first invalid field, more than one submit button, changing a value while the user types, nested objects, array fields, lists inside lists and `formRootRule`, async validation (rules and async schemas), conditional fields, conditional validation, multi-step forms, loading initial values from a server, saving and setting a new baseline, controlling a form from outside its component, custom inputs, form context across components, `transformValues`, standalone `useField`, server errors after submission
- **[`references/api.md`](references/api.md)** — read for everything else. It is the only place that lists every `useForm` option and return member (`watch`, `onValuesChange`, `enhanceGetInputProps`, `resetField`, `clearFieldError`, `touchTrigger` and others), plus `useField`, `createFormContext`, `createFormActions`, `schemaResolver`, built-in validators and types

## Looking things up

If the references do not cover what you need, do not guess:

- If the Mantine MCP server (`@mantine/mcp-server`) is connected, use `search_docs` and `get_item_doc`
- Otherwise fetch `https://mantine.dev/llms.txt` and open the linked form pages

© mantinedev · MIT · mantinedev/skills

Similar skills

Everything for Frontend and UI →

From the same set3

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor