Special Field

Hidden

Invisible field that resolves a dynamic value at runtime — timestamps, URLs, IP addresses, UTM params, and more.

stable
hidden resolver tracking metadata

Submit the form to see how hidden fields resolve values automatically

Submit the form to see the resolved hidden field values (timestamp, page URL, referrer, UTM source).

/**
 * Hidden Field Demo - Shows how hidden fields resolve values at runtime
 */

import { Form, FormBuilder } from '@saastro/forms';
import { useState } from 'react';

import { FormProvider } from '@/components/FormProvider';
import { TooltipProvider } from '@/components/ui/tooltip';

const config = FormBuilder.create('hidden-demo')
  .layout('manual')
  .columns(12)
  .addField('name', (f) =>
    f
      .type('text')
      .label('Your Name')
      .placeholder('Enter your name')
      .required()
      .columns({ default: 12 }),
  )
  .addField('email', (f) =>
    f
      .type('email')
      .label('Email')
      .placeholder('[email protected]')
      .required()
      .columns({ default: 12 }),
  )
  .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: 'direct' }),
  )
  .addStep('main', ['name', 'email', 'submitted_at', 'page_url', 'referrer', 'utm_source'])
  .build();

export default function HiddenDemo() {
  const [submittedData, setSubmittedData] = useState<Record<string, unknown> | null>(null);

  const handleSubmit = (data: Record<string, unknown>) => {
    setSubmittedData(data);
  };

  return (
    <TooltipProvider>
      <FormProvider>
        <div className="space-y-4">
          <Form config={config} onSubmit={handleSubmit} className="space-y-4" />

          {submittedData && (
            <div className="rounded-md border p-4 bg-muted/50">
              <p className="text-sm font-medium mb-2">
                Submitted data (includes resolved hidden fields):
              </p>
              <pre className="text-xs overflow-auto bg-background p-3 rounded border">
                {JSON.stringify(submittedData, null, 2)}
              </pre>
            </div>
          )}

          {!submittedData && (
            <p className="text-xs text-muted-foreground">
              Submit the form to see the resolved hidden field values (timestamp, page URL,
              referrer, UTM source).
            </p>
          )}
        </div>
      </FormProvider>
    </TooltipProvider>
  );
}

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

ResolverOutputAsyncDescription
timestampstring (ISO 8601)NoCurrent date/time via new Date().toISOString()
hostnamestringNowindow.location.hostname
pageUrlstringNowindow.location.href
referrerstringNodocument.referrer
userAgentstringNonavigator.userAgent
ipstringYesFetches visitor IP from an external API
urlParamstringNoReads 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

PropTypeDefaultDescription
type'hidden'-Field type (required)
resolverSerializableFieldResolver-Resolver configuration (required)
labelstring-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 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

  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

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"
// }
  • 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