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
import { FormBuilder } from '@saastro/forms';
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
.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
.addField('visitor_ip', (f) =>
f.type('hidden').resolver({
$resolver: 'ip',
endpoint: 'https://api.ipify.org?format=json',
fallback: 'unknown',
}),
)
JSON Configuration
{
"type": "hidden",
"resolver": { "$resolver": "timestamp" }
}
{
"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 readfallback(optional) — Value to use if the parameter is not present
ip accepts:
endpoint(optional) — Custom API URL (defaults tohttps://api.ipify.org?format=json). The endpoint must return JSON with anipkey.fallback(optional) — Value to use if the fetch fails or the response has noipkey
Props
| Prop | Type | Default | Description |
|---|---|---|---|
type | 'hidden' | - | Field type (required) |
resolver | SerializableFieldResolver | - | Resolver configuration (required) |
label | string | - | Optional label (not rendered) |
How It Works
- The form engine calls
useHiddenFieldResolverson mount (automatic when you use<Form>) - The hook filters all fields with
type: 'hidden' - All resolvers run in parallel via
resolveValue()(async — supports the IP fetch) - Resolved values are set via
react-hook-form’ssetValue()(without marking the form dirty) - 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 and the Hidden Fields guide.
No Validation
Hidden fields use z.any() internally and skip validation entirely. Their value comes from the resolver, not from user input.
Use Cases
- Analytics tracking — Capture UTM parameters, referrer, page URL
- Timestamps — Record when the form was loaded or submitted
- Device fingerprinting — User agent, hostname, IP address
- Lead attribution — Source tracking for marketing funnels
- Form context — Which page the form appeared on
Complete Example
import { FormBuilder } from '@saastro/forms';
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: "[email protected]",
// 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 — Conceptual guide with advanced patterns
- useHiddenFieldResolvers — The public hook that resolves hidden field values
- Submit & Actions — Field mapping can inject resolved values too
- Plugins — Plugins like DataBowl can inject resolver values into the submitted payload via field mapping, without declaring hidden fields in the form