S
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('you@example.com')
      .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 ipify)
  • fallback (optional) — Value to use if the fetch fails

Props

PropTypeDefaultDescription
type'hidden'-Field type (required)
resolverSerializableFieldResolver-Resolver configuration (required)
labelstring-Optional label (not rendered)

How It Works

  1. The Form component calls useHiddenFieldResolvers on mount
  2. The hook filters all fields with type: 'hidden'
  3. Each field’s resolver is executed via resolveValue() (async — supports IP fetch)
  4. Resolved values are set via react-hook-form’s setValue()
  5. 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

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