Back to Widget Patterns
v2.1Lead Capture

Inline Single Field Form

Single-field email, text, or tel input plus a submit button. The lowest-friction lead capture pattern. Includes a state machine: idle, submitting, success, error.

Live preview

Rendered with real component code. Each example demonstrates a documented variant.

Default email signup

With disclaimer

No spam. Unsubscribe anytime.

Phone variant

React props

Configure the component via props in React or via attributes in HTML.

PropTypeDefaultDescription
fieldType"email" | "text" | "tel""email"Input type
placeholderstringDepends on fieldTypePlaceholder text; defaults per fieldType if omitted
buttonLabelstring"Subscribe"Submit button text
onSubmit(value: string) => void | Promise<void>requiredCalled with the trimmed input value on submit; may be async
disclaimerTextstringundefinedOptional fine print rendered below the form row
successTextstring"Thanks!"Text shown after a successful submit
labelstringundefinedVisible label text for the input; an aria-label is applied automatically when omitted
classNamestringundefinedExtra class applied to the root form element

HTML usage

The HTML variant uses the same shared styles and class names. Drop into any stack.

<link rel="stylesheet" href="path/to/inline-single-field-form/styles.css">

<form class="isff" novalidate>
  <div class="isff__row">
    <div class="isff__field">
      <label for="my-input">Email address</label>
      <input
        id="my-input"
        class="isff__input"
        type="email"
        placeholder="you@company.com"
        autocomplete="email"
        required
      >
    </div>
    <button class="isff__button" type="submit">Subscribe</button>
  </div>

  <!-- Error: hidden until submission fails -->
  <p class="isff__error" id="my-error" hidden role="alert"></p>

  <!-- Success: hidden until submission succeeds -->
  <p class="isff__success" id="my-success" hidden aria-live="polite">
    Thanks!
  </p>

  <!-- Optional fine print -->
  <p class="isff__disclaimer">No spam. Unsubscribe anytime.</p>
</form>

<script>
(function () {
  var form = document.querySelector('.isff');
  var input = form.querySelector('.isff__input');
  var button = form.querySelector('.isff__button');
  var success = form.querySelector('.isff__success');
  var error = form.querySelector('.isff__error');

  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var value = input.value.trim();
    if (!value) return;

    input.disabled = true;
    button.disabled = true;
    error.hidden = true;

    // Place your fetch() call here.
    fetch('/api/subscribe', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: value }),
    })
      .then(function (res) {
        if (!res.ok) throw new Error('Request failed');
        success.hidden = false;
        form.reset();
      })
      .catch(function () {
        error.textContent = 'Something went wrong. Check your entry and try again.';
        error.hidden = false;
        input.disabled = false;
        button.disabled = false;
      });
  });
}());
</script>

Customization

Override these CSS custom properties to integrate the component into your brand.

.isff {
  --isff-input-border-color: var(--brand-border, #d1d5db);
  --isff-input-border-radius: 0.375rem;
  --isff-input-padding-y: 0.625rem;
  --isff-input-padding-x: 0.875rem;
  --isff-focus-ring-color: var(--brand-accent, #1e5fcf);
  --isff-button-bg: var(--brand-accent, #1e5fcf);
  --isff-button-bg-hover: color-mix(in srgb, var(--isff-button-bg) 88%, black);
  --isff-button-text: #ffffff;
  --isff-disclaimer-color: var(--brand-muted, #6b7280);
  --isff-success-color: var(--brand-success, #16a34a);
  --isff-error-color: var(--brand-error, #dc2626);
  --isff-gap: 0.5rem;
  --isff-font-size: 1rem;
  --isff-font-weight-button: 600;
  --isff-disclaimer-font-size: 0.8125rem;
}