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 to ipify)fallback(optional) — Value to use if the fetch fails
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
Formcomponent callsuseHiddenFieldResolverson mount - The hook filters all fields with
type: 'hidden' - Each field’s resolver is executed via
resolveValue()(async — supports IP fetch) - Resolved values are set via
react-hook-form’ssetValue() - On submit, hidden field values are included in the form data like any other field
// Internal: useHiddenFieldResolvers
useEffect(() => {
const hiddenFields = Object.entries(fields).filter(([, cfg]) => cfg.type === 'hidden');
const resolve = async () => {
const results = await Promise.all(
hiddenFields.map(async ([name, cfg]) => ({
name,
value: await resolveValue(cfg.resolver),
})),
);
for (const { name, value } of results) {
methods.setValue(name, value, { shouldDirty: false });
}
};
resolve();
}, [fields, methods]);
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 { Form, 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: "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
- Submit & Actions — Field mapping can inject resolved values too
- Plugins — The DataBowl plugin adds hidden tracking fields automatically
- Hidden Fields Guide — Conceptual guide with advanced patterns