OTP input
The field a patient types a WhatsApp, SMS, or email verification code into.
mq-otp-input renders one box per character of a one-time code. It ships from
@mediquo/ui as a custom element and as a React component.
Install
// Custom element — the import registers <mq-otp-input>
import "@mediquo/ui/components/otp-input";// React
import { OtpInput } from "@mediquo/ui/react/otp-input";It renders the control and nothing else
No label, no hint, no error text. Those belong to the field wrapper the
integrator already has — in the patient portal, FieldLabel,
FieldDescription and FieldError from features/shared/ui/components/field.
That split is deliberate. If every form primitive rendered its own label and
error text, each new one would grow another copy of the same markup and the
same spacing decisions. invalid here is a state, not a message: it turns
the boxes red and sets aria-invalid, and the integrator says what went wrong.
<Field>
<FieldLabel htmlFor="code">Código de verificación</FieldLabel>
<OtpInput label="Código de verificación" invalid={isCodeError} />
{isCodeError ? <FieldError>El código no es correcto.</FieldError> : null}
</Field>label is the one string it does take, because the real input lives in a shadow
root where an outer <label for> cannot reach it. It sets aria-label and
renders no text.
How it is built
The visible boxes are decoration. Underneath them sits a single real
<input> carrying the whole code, transparent and stretched over the boxes.
That is a deliberate choice, not an implementation detail:
- One input per digit announces as six unrelated text fields. One input announces as one labelled field, which is what it is.
autocomplete="one-time-code"only autofills a field that holds the whole code, so iOS and Android can offer the code straight from the SMS.- Pasting, caret movement,
Backspace,Home, andEndare the browser's native text-box behaviour rather than reimplemented key handling.
Accessibility
labelis the accessible name and there is no visible<label>, so it is effectively required — the component warns in the console when it is missing.- The boxes are
aria-hidden; only the real input is exposed. invalidsetsaria-invalid. Pair it with your field wrapper's error text so the failure is never communicated by colour alone.requiredsetsaria-required.- The caret blink and the box transitions are dropped under
prefers-reduced-motion: reduce.
Sizing
Sizes and look follow the patient portal's verify-code screen: no border, a
filled box, a 1px ring for the active and invalid states, and boxes that stretch
to fill their container and grow at the md breakpoint (60px tall below it,
80px from it).
Because the boxes stretch, the width comes from the parent — give it the width you want the row to have.
Styling
The component uses --mq-* tokens with literal fallbacks, so it renders
correctly with or without @mediquo/ui/tokens.css loaded. These custom
properties are the supported override points:
mq-otp-input {
--mq-otp-slot-height: 3.75rem;
--mq-otp-slot-height-md: 5rem;
--mq-otp-slot-max-width: 2.75rem;
--mq-otp-slot-max-width-md: 65px;
--mq-otp-slot-radius: 0.75rem;
--mq-otp-slot-background: #f8f7fa;
--mq-otp-slot-active-ring-color: #6500cc;
--mq-otp-slot-invalid-ring-color: #ff014d;
--mq-otp-slot-selected-background: #f5e5ff;
--mq-otp-slot-font-size: 1.25rem;
--mq-otp-slot-font-size-md: 1.5rem;
--mq-otp-gap: 0.25rem;
--mq-otp-separator-width: 0.5rem;
--mq-otp-separator-color: #d1ccdc;
--mq-otp-caret-color: #0a0a23;
--mq-otp-caret-height: 1.75rem;
}part is exposed for root, input, group, slot, separator, and
caret; filled and active boxes also carry slot-filled and slot-active.
Behaviour worth knowing
- Paste replaces the whole code. A pasted string is the code, so it does not splice in at the caret. Separators and spaces are stripped.
- Typing over a character replaces it instead of pushing the rest along and dropping the last one.
- Clicking pins the caret after the last character, the way a phone's native code field does, because the real glyphs are collapsed and a click cannot land the caret anywhere meaningful.
maskis visual only. The value is still readable by assistive tech, which is correct for a verification code — it is not a password.