A fixed-length verification code shown as character slots, backed by one native input and one form value.
<tp-one-time-code-field>
One-time code field
Default density
Compact density
Restart from the initial state
Show the copyable source
Verify your login
Enter the code we sent to maya@northwind.io.
ResendVerify
Verify your login
Enter the code we sent to maya@northwind.io.
ResendVerify
<tp-card style="inline-size:100%;max-inline-size:26rem">
<h3 slot="header" style="margin:0">Verify your login</h3>
<p slot="description" style="margin:0">Enter the code we sent to maya@northwind.io.</p>
<tp-button slot="action" variant="outline" size="sm">Resend</tp-button>
<tp-field label="Verification code">
<tp-one-time-code-field length="6" name="code" group-lengths="[3,3]"></tp-one-time-code-field>
</tp-field>
<tp-button slot="footer">Verify</tp-button>
</tp-card>
Usage
The slots are presentation over a single text editor, so typing, deletion, selection, paste and SMS autofill (autocomplete="one-time-code" by default) behave like one input. Screen readers hear one field with one name, and the form receives one value. Wrap it in a Field for the label.
Character types
validation-type filters what can be entered: numeric, alphabetic or the default alphanumeric. Numeric codes also bring up the number keypad on touch devices.
onValueInvalid(detail) and tp-invalid-input report { attemptedValue, reason: 'input' | 'paste', sourceEvent } when normalization removes or changes attempted input. The normalizer removes whitespace, filters, applies normalizeValue, filters again, and truncates by Unicode code point.
Numeric
Default density
Compact density
Restart from the initial state
Show the copyable source
<tp-field label="SMS code" description="Enter the six digits from the text message.">
<tp-one-time-code-field length="6" name="sms-code" validation-type="numeric"></tp-one-time-code-field>
</tp-field>
Groups
group-lengths splits the slots into visual groups with a separator between them. The separator never enters the value or the accessibility tree.
Grouped
Default density
Compact density
Restart from the initial state
Show the copyable source
tp-complete fires once the last slot is filled, which is the moment to verify. Add auto-submit to submit the owning form instead of waiting for a button press.
Programmatic assignments and native resets do not simulate user completion. Completion precedes optional auto-submit.
Verify on completion
Default density
Compact density
Restart from the initial state
Show the copyable source
Set value to own the code, and accept each edit from tp-value-change by assigning event.detail.value. Your application can then clear or prefill the field directly, for example after requesting a new code.
A controlled owner must publish the accepted value synchronously. Rejection restores the editor, visible slots, selection and caret; it cannot complete or submit. Keep controlled versus uncontrolled ownership stable for the element’s lifetime.
Controlled value
Default density
Compact density
Restart from the initial state
Show the copyable source
Send a new code
Send a new code
<div id="otc-controlled" style="display:grid;gap:var(--tp-space-3);justify-items:start">
<tp-field label="Two-factor code">
<tp-one-time-code-field length="6" validation-type="numeric" group-lengths="[3,3]" value=""></tp-one-time-code-field>
</tp-field>
<div style="display:flex;gap:var(--tp-space-3);align-items:center">
<tp-button variant="outline" size="sm" data-resend>Send a new code</tp-button>
<output aria-live="polite" style="color:var(--tp-muted-foreground);font-size:var(--tp-text-sm)">0 of 6 digits</output>
</div>
</div>
<script type="module">
const root = document.querySelector('#otc-controlled');
const field = root.querySelector('tp-one-time-code-field');
const output = root.querySelector('output');
field.addEventListener('tp-value-change', (event) => {
if (event.defaultPrevented || event.detail.cancelled) return;
field.value = event.detail.value;
output.textContent = `${field.value.length} of 6 digits`;
});
root.querySelector('[data-resend]').addEventListener('click', () => {
field.value = '';
output.textContent = 'New code sent. The old one no longer works.';
field.focus();
});
</script>
States
Disabled and invalid
Default density
Compact density
Restart from the initial state
Show the copyable source
Disabled codes are left out of form data. An invalid Field paints every slot and announces its error once for the whole code.
API Reference
Every element also accepts motion-policy.
Attributes
AttributePropertyTypeDefaultDescriptionlengthlengthnumber | undefined—The number of characters in the code, and so the number of slots.inputmodeinputModestring—The on-screen keyboard to show; a numeric code brings up the number pad by default.default-valuedefaultValuestring | undefined—The code at first render and after a form reset, when the page does not own value.validation-typevalidationTypeCodeValidation'alphanumeric'Which characters the code accepts: digits, letters, both, or whatever characterPredicate
allows. Others are filtered out as they are typed or pasted.group-lengthsgroupLengthsreadonly number[][]Splits the slots into visual groups with a separator between them, as a JSON array of group
sizes that add up to length; the separator never enters the value.maskmaskbooleanfalseHides the entered characters on screen, for a PIN entered where others can see; the value stays
intact.autocompleteautocompletestring'one-time-code'The autofill hint of the native input; the default lets phones offer a code from a text
message.no-autofillnoAutofillbooleanfalseOpts the code editor out of host and extension autofill.auto-submitautoSubmitbooleanfalseSubmits the owning form as soon as the last slot is filled.labellabelstring'One-time code'The accessible name of the code when no Field or Label names it.placeholderplaceholderstring''A character shown in each empty slot.
Properties
PropertyTypeDefaultDescriptioncharacterPredicate((character: string) => boolean) | undefined—Decides whether one character is accepted when validationType is predicate.normalizeValue((value: string) => string) | undefined—Rewrites typed or pasted text before filtering, for example to uppercase or to strip dashes.effectiveNoAutofillread-onlyboolean—Whether autofill is off, by the field itself or by the Field or Form around it.onValueChange((event: TpValueChangeEvent<string>) => void) | undefined—Called with each proposed code before it commits; assign value to accept it or cancel the
event to reject it.onValueComplete((detail: CodeCompleteDetail) => void) | undefined—Called with the complete code when the last slot is filled.onValueInvalid((detail: CodeInvalidDetail) => void) | undefined—Called when typed or pasted text contained characters the code does not accept.completeread-onlyboolean—Whether every slot is filled.
Events
EventTypeDescriptiontp-value-changeTpValueChangeEvent<string>Cancelable proposal of a new code before it commits; detail.value, detail.previousValue and the detail.reason for the change.tp-field-valueCustomEvent<{ value: string; previousValue: string; reason: ChangeReason; }>After the code commits, with the same value, previousValue and reason; the enclosing Field listens for it.tp-invalid-inputCustomEvent<CodeInvalidDetail>When typed or pasted text contained characters the code does not accept; detail.attemptedValue is the text before filtering and detail.reason says how it arrived.tp-completeCustomEvent<{ value: string; sourceEvent: Event; }>When the last slot is filled; detail.value is the complete code.
CSS parts
PartDescription::part(one-time-code-field)The root that holds the character slots over the native input.::part(one-time-code-field-group)One group of character slots.::part(one-time-code-field-separator)The mark between two groups of slots; never part of the value.::part(one-time-code-field-slot)One character position; shows the active and filled states.
Related
Components, widgets and patterns that use this one.