Accessibility (Web SDK)
Card fields render inside cross-origin iframes. That keeps card data off your page, but it also means
your page cannot reach into the fields: a <label for>, an aria-describedby or a CSS rule on your
page never reaches the <input> inside the frame. This guide lists what the SDK handles inside the
frames, and what your page has to handle around them, for WCAG 2.2 AA and RGAA 4.
What the SDK handles
In both modes, inside each field frame:
- Every input is marked required (
aria-required="true"). - Every input has an accessible name. Without
ariaLabel, it defaults to an English label such as "Card number". - A field is announced invalid only after the user leaves it, or after a rejected
submit(). It is never announced invalid on first render or while the user is still typing. - The error message stays in the input's description while the field is invalid, focus included, so a screen reader reads it when the user comes back to the field.
- On a rejected
submit(), every invalid field reveals its error, including fields the user never visited. - The frame document uses the language set by
lang, so screen readers pronounce texts correctly. - The co-badge brand list drawn in the card number field has an accessible name and a visible focus.
Hosted fields mode
With mode: 'hosted_fields' you own everything around the frames: labels, layout, visible errors
and the focus indicator.
Name, describe and translate each field
Pass these per field, in the language of your checkout:
| Property | Why |
|---|---|
ariaLabel | Accessible name of the input. Your <label for> cannot reach it. The default is English. |
iframeTitle | Accessible name of the iframe. Without it, the title is a technical id. |
errorMessages | Text announced when the field is invalid. Without it, a screen reader says "invalid" with no reason. See Accessible error messages. |
description | Help text read with the field, such as the expected expiry date format. |
brandSelectorLabel | Card number only: name of the co-badge brand list. Defaults to "Card brand". |
Set lang at the top level of the config to the language of these texts. It defaults to your
page's <html lang>, then to 'en'.
const secureForm = await sf.initSecureFields({
tenantId: '${VAULT_TENANT_ID}',
config: {
mode: 'hosted_fields',
lang: 'fr',
fields: {
expDate: {
target: 'exp-date-container',
placeholder: 'MM/AA',
ariaLabel: "Date d'expiration",
iframeTitle: "Champ sécurisé : date d'expiration",
description: 'Au format MM/AA, comme sur votre carte',
errorMessages: {
empty: "La date d'expiration est obligatoire",
format: "La date d'expiration est invalide",
expired: 'La carte a expiré',
},
},
// ...other fields
},
},
});
Keep a visible label on your page next to each container, with the same text as ariaLabel.
Draw the focus indicator
The input inside the frame has no outline, and styles only accepts colour and font properties. Draw
the focus indicator on the container instead, from the focus and blur events:
const containers = {
cardNumber: 'card-number-container',
expDate: 'exp-date-container',
cvv: 'cvv-container',
holderName: 'holder-name-container',
};
secureForm.on('focus', ({ fieldName }) => {
document.getElementById(containers[fieldName])?.classList.add('is-focused');
});
secureForm.on('blur', ({ fieldName }) => {
document.getElementById(containers[fieldName])?.classList.remove('is-focused');
});
.secure-field.is-focused {
outline: 2px solid #0050b3;
outline-offset: 2px;
}
The indicator needs a contrast of at least 3:1 against the page background.
Show errors
- Render a visible message next to each field, from the same text as
errorMessages, withrole="alert"or in a live region. - Do not rely on colour alone: add text or an icon to the message.
- In this mode the SDK does not move focus after a rejected
submit(). Move it yourself, for example to an error summary at the top of the form.
Check contrast
The fields have a transparent background by default, so their text sits on your page's background.
Check that color and placeholderColor reach 4.5:1 against it.
Hosted form mode
With mode: 'hosted_form' the SDK draws the labels, help text, errors and a focus border on the box
around each field. It also moves focus to the first invalid field after a rejected submit().
- Translate every text in
hostedForm. The defaults are in English only. - Set
langto the language of those texts. - Set
hostedForm.cardFormLabelto name the group of card fields. It defaults to "Card details". - If you change the theme, check the contrast of text, borders and the focus border.
3D Secure challenge
The challenge page belongs to the cardholder's bank. Your page owns the frame around it.
- Pass
titletothreeDSChallengein your checkout's language. It defaults to "3D Secure authentication". - Give the container a visible heading, for example "Confirm your payment".
- When the challenge ends, the SDK moves focus back to the element that had it before the challenge.
If that element is gone, focus goes to the container, and the SDK adds
tabindex="-1"to it. - Add a
role="status"region to announce loading, success and expiry. - If you show the challenge in a modal: use
role="dialog"andaria-modal="true", keep focus inside the modal, and wire the Escape key to theAbortSignalyou pass insignal. - The challenge is removed after
timeoutMs(10 minutes by default) without any message, and the promise resolves withstatus: 'timeout'. Tell the user the payment was not confirmed, in arole="status"region, and offer to try again.timeoutMsmust be a positive number of at most 2147483647, otherwise the call throws.
List the bank's challenge pages as third-party content in your accessibility statement: neither you nor Purse controls them.