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
Phone variant
React props
Configure the component via props in React or via attributes in HTML.
| Prop | Type | Default | Description |
|---|---|---|---|
| fieldType | "email" | "text" | "tel" | "email" | Input type |
| placeholder | string | Depends on fieldType | Placeholder text; defaults per fieldType if omitted |
| buttonLabel | string | "Subscribe" | Submit button text |
| onSubmit | (value: string) => void | Promise<void> | required | Called with the trimmed input value on submit; may be async |
| disclaimerText | string | undefined | Optional fine print rendered below the form row |
| successText | string | "Thanks!" | Text shown after a successful submit |
| label | string | undefined | Visible label text for the input; an aria-label is applied automatically when omitted |
| className | string | undefined | Extra 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;
}