Skip to main content

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:

PropertyWhy
ariaLabelAccessible name of the input. Your <label for> cannot reach it. The default is English.
iframeTitleAccessible name of the iframe. Without it, the title is a technical id.
errorMessagesText announced when the field is invalid. Without it, a screen reader says "invalid" with no reason. See Accessible error messages.
descriptionHelp text read with the field, such as the expected expiry date format.
brandSelectorLabelCard 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, with role="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 lang to the language of those texts.
  • Set hostedForm.cardFormLabel to 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 title to threeDSChallenge in 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" and aria-modal="true", keep focus inside the modal, and wire the Escape key to the AbortSignal you pass in signal.
  • The challenge is removed after timeoutMs (10 minutes by default) without any message, and the promise resolves with status: 'timeout'. Tell the user the payment was not confirmed, in a role="status" region, and offer to try again. timeoutMs must be a positive number of at most 2147483647, otherwise the call throws.
Accessibility statement

List the bank's challenge pages as third-party content in your accessibility statement: neither you nor Purse controls them.