# Field: Hidden (category: special)

URL: https://docs.forms.saastro.io/docs/hidden

<FieldPreview
  name="hidden"
  description="Submit the form to see how hidden fields resolve values automatically"
/>

## Overview

The hidden field captures metadata without any visible UI. Each hidden field has a **resolver** that computes its value when the form mounts. Use it for tracking, analytics, and contextual data that the user doesn't need to see or edit.

Hidden fields:

- Render nothing in the form UI
- Resolve their value asynchronously at form init via `useHiddenFieldResolvers`
- Skip validation (use `z.any()` internally)
- Are included in the submitted data like any other field

## Usage

### Basic Timestamp

```tsx

const config = FormBuilder.create('contact')
  .addField('email', (f) => f.type('email').label('Email').required())
  .addField('submitted_at', (f) => f.type('hidden').resolver({ $resolver: 'timestamp' }))
  .addStep('main', ['email', 'submitted_at'])
  .build();
```

### UTM Tracking

```tsx
.addField('utm_source', (f) =>
  f.type('hidden').resolver({
    $resolver: 'urlParam',
    param: 'utm_source',
    fallback: 'direct',
  }),
)
.addField('utm_medium', (f) =>
  f.type('hidden').resolver({
    $resolver: 'urlParam',
    param: 'utm_medium',
    fallback: 'none',
  }),
)
```

### IP Address

```tsx
.addField('visitor_ip', (f) =>
  f.type('hidden').resolver({
    $resolver: 'ip',
    endpoint: 'https://api.ipify.org?format=json',
    fallback: 'unknown',
  }),
)
```

### JSON Configuration

```json
{
  "type": "hidden",
  "resolver": { "$resolver": "timestamp" }
}
```

```json
{
  "type": "hidden",
  "resolver": {
    "$resolver": "urlParam",
    "param": "utm_source",
    "fallback": "direct"
  }
}
```

## Built-in Resolvers

| Resolver    | Output              | Async | Description                                      |
| ----------- | ------------------- | ----- | ------------------------------------------------ |
| `timestamp` | `string` (ISO 8601) | No    | Current date/time via `new Date().toISOString()` |
| `hostname`  | `string`            | No    | `window.location.hostname`                       |
| `pageUrl`   | `string`            | No    | `window.location.href`                           |
| `referrer`  | `string`            | No    | `document.referrer`                              |
| `userAgent` | `string`            | No    | `navigator.userAgent`                            |
| `ip`        | `string`            | Yes   | Fetches visitor IP from an external API          |
| `urlParam`  | `string`            | No    | Reads a URL query parameter                      |

### Resolver Options

**`urlParam`** accepts:

- `param` (required) — The query parameter name to read
- `fallback` (optional) — Value to use if the parameter is not present

**`ip`** accepts:

- `endpoint` (optional) — Custom API URL (defaults to `https://api.ipify.org?format=json`). The endpoint must return JSON with an `ip` key.
- `fallback` (optional) — Value to use if the fetch fails or the response has no `ip` key

## Props

| Prop       | Type                        | Default | Description                       |
| ---------- | --------------------------- | ------- | --------------------------------- |
| `type`     | `'hidden'`                  | -       | Field type (required)             |
| `resolver` | `SerializableFieldResolver` | -       | Resolver configuration (required) |
| `label`    | `string`                    | -       | Optional label (not rendered)     |

## How It Works

1. The form engine calls `useHiddenFieldResolvers` on mount (automatic when you use `<Form>`)
2. The hook filters all fields with `type: 'hidden'`
3. All resolvers run in parallel via `resolveValue()` (async — supports the IP fetch)
4. Resolved values are set via `react-hook-form`'s `setValue()` (without marking the form dirty)
5. On submit, hidden field values are included in the form data like any other field

If the form unmounts before async resolvers finish, the results are discarded.

`useHiddenFieldResolvers` is a public hook — you only need to call it yourself when building a fully custom form on top of react-hook-form. See the [useHiddenFieldResolvers reference](/docs/use-hidden-field-resolvers) and the [Hidden Fields guide](/docs/hidden-fields).

## No Validation

Hidden fields use `z.any()` internally and skip validation entirely. Their value comes from the resolver, not from user input.

## Use Cases

1. **Analytics tracking** — Capture UTM parameters, referrer, page URL
2. **Timestamps** — Record when the form was loaded or submitted
3. **Device fingerprinting** — User agent, hostname, IP address
4. **Lead attribution** — Source tracking for marketing funnels
5. **Form context** — Which page the form appeared on

## Complete Example

```tsx

const config = FormBuilder.create('lead-capture')
  .addField('name', (f) => f.type('text').label('Name').required())
  .addField('email', (f) => f.type('email').label('Email').required())
  // Hidden tracking fields
  .addField('submitted_at', (f) => f.type('hidden').resolver({ $resolver: 'timestamp' }))
  .addField('page_url', (f) => f.type('hidden').resolver({ $resolver: 'pageUrl' }))
  .addField('referrer', (f) => f.type('hidden').resolver({ $resolver: 'referrer' }))
  .addField('utm_source', (f) =>
    f.type('hidden').resolver({
      $resolver: 'urlParam',
      param: 'utm_source',
      fallback: 'organic',
    }),
  )
  .addField('visitor_ip', (f) => f.type('hidden').resolver({ $resolver: 'ip' }))
  .addStep('main', [
    'name',
    'email',
    'submitted_at',
    'page_url',
    'referrer',
    'utm_source',
    'visitor_ip',
  ])
  .build();

// Submitted data includes all resolved hidden values:
// {
//   name: "John",
//   email: "john@example.com",
//   submitted_at: "2026-02-25T12:00:00.000Z",
//   page_url: "https://example.com/contact?utm_source=google",
//   referrer: "https://google.com",
//   utm_source: "google",
//   visitor_ip: "203.0.113.42"
// }
```

## Related

- [Hidden Fields Guide](/docs/hidden-fields) — Conceptual guide with advanced patterns
- [useHiddenFieldResolvers](/docs/use-hidden-field-resolvers) — The public hook that resolves hidden field values
- [Submit & Actions](/docs/submit) — Field mapping can inject resolved values too
- [Plugins](/docs/plugins) — Plugins like DataBowl can inject resolver values into the submitted payload via field mapping, without declaring hidden fields in the form
