PingOne Recognize

User interface customization

Customizing user interface elements

Update the theme-options or themeOptions attributes with an object using the following structure:

Details
/** @public */
export interface ThemeOptions {
  colors: ThemeOptionsColors
  elements: ThemeOptionsElements
}
/** @public */
export interface ThemeOptionsColors {
  dark: ThemeOptionsColorsValue
  light: ThemeOptionsColorsValue
}
/** @public */
export interface ThemeOptionsColorsValue {
  primary: string
  onPrimary: string
  secondary: string
  onSecondary: string
  error: string
  onError: string
  secondaryContainer: string
  onSecondaryContainer: string
  surface: string
  onSurface: string
  surfaceVariant: string
  onSurfaceVariant: string
}
/** @public */
export interface ThemeOptionsElements {
  button: ThemeOptionsElementsButton
  camera: ThemeOptionsElementsCamera
  cameraCorners: ThemeOptionsElementsCameraCorners
  cameraInstructions: ThemeOptionsElementsCameraInstructions
  cameraSelect: ThemeOptionsElementsCameraSelect
  cameraTip: ThemeOptionsElementsCameraTip
  dialog: ThemeOptionsElementsDialog
  poweredBy: ThemeOptionsElementsPoweredBy
  qrcode: ThemeOptionsElementsQrcode
  root: ThemeOptionsElementsRoot
}
/** @public */
export interface ThemeOptionsElementsRoot {
  buttonCameraSelect: ThemeOptionsElementsRootButtonCameraSelect
  buttonCancel: ThemeOptionsElementsRootButtonCancel
  buttonClose: ThemeOptionsElementsRootButtonClose
  buttonFlash: ThemeOptionsElementsRootButtonFlash
  buttonPin: ThemeOptionsElementsRootButtonPin
  buttonsSwitchToMobileChoice: ThemeOptionsElementsRootButtonsSwitchToMobileChoice
  cameraBiometric: ThemeOptionsElementsRootCameraBiometric
  cameraTip: ThemeOptionsElementsRootCameraTip
  headline: ThemeOptionsElementsRootHeadline
  host: ThemeOptionsElementsRootHost
  poweredBy: ThemeOptionsElementsRootPoweredBy
  text: ThemeOptionsElementsRootText
  texts: ThemeOptionsElementsRootTexts
}
/** @public */
export interface ThemeOptionsElementsRootButtonCameraSelect {
  right: string
  top: string
}
/** @public */
export interface ThemeOptionsElementsRootButtonCancel {
  left: string
  top: string
}
/** @public */
export interface ThemeOptionsElementsRootButtonClose {
  left: string
  top: string
}
/** @public */
export interface ThemeOptionsElementsRootButtonFlash {
  right: string
  top: string
}
/** @public */
export interface ThemeOptionsElementsRootButtonPin {
  height: string
  width: string
}
/** @public */
export interface ThemeOptionsElementsRootButtonsSwitchToMobileChoice {
  gap: string
}
/** @public */
export interface ThemeOptionsElementsRootCameraBiometric {
  width: string
}
/** @public */
export interface ThemeOptionsElementsRootCameraTip {
  top: string
}
/** @public */
export interface ThemeOptionsElementsRootHeadline {
  fontSize: string
  fontWeight: string
  marginTop: string
}
/** @public */
export interface ThemeOptionsElementsRootHost {
  borderRadius: string
  gap: string
  padding: string
}
/** @public */
export interface ThemeOptionsElementsRootPoweredBy {
  bottom: string
}
/** @public */
export interface ThemeOptionsElementsRootText {
  fontSize: string
  fontWeight: string
}
/** @public */
export interface ThemeOptionsElementsRootTexts {
  gap: string
}
/** @public */
export interface ThemeOptionsElementsButton {
  host: ThemeOptionsElementsButtonHost
  size: ThemeOptionsElementsButtonSize
  variant: ThemeOptionsElementsButtonVariant
}
/** @public */
export interface ThemeOptionsElementsButtonHost {
  borderRadius: string
  fontSize: string
  fontWeight: string
  padding: string
}
/** @public */
export interface ThemeOptionsElementsButtonSizeSmallHost {
  fontSize: string
  padding: string
}
/** @public */
export interface ThemeOptionsElementsButtonSizeSmall {
  host: ThemeOptionsElementsButtonSizeSmallHost
}
/** @public */
export interface ThemeOptionsElementsButtonSize {
  small: ThemeOptionsElementsButtonSizeSmall
}
/** @public */
export interface ThemeOptionsElementsButtonVariantTextHostHover {
  opacity: string
}
/** @public */
export interface ThemeOptionsElementsButtonVariantTextHost {
  borderBottom: string
  hover: ThemeOptionsElementsButtonVariantTextHostHover
}
/** @public */
export interface ThemeOptionsElementsButtonVariantText {
  host: ThemeOptionsElementsButtonVariantTextHost
}
/** @public */
export interface ThemeOptionsElementsButtonVariant {
  text: ThemeOptionsElementsButtonVariantText
}
/** @public */
export interface ThemeOptionsElementsCamera {
  host: ThemeOptionsElementsCameraHost
}
/** @public */
export interface ThemeOptionsElementsCameraHost {
  after: ThemeOptionsElementsCameraHostAfter
  before: ThemeOptionsElementsCameraHostBefore
}
/** @public */
export interface ThemeOptionsElementsCameraHostAfter {
  background: string
  height: string
}
/** @public */
export interface ThemeOptionsElementsCameraHostBefore {
  background: string
  height: string
}
/** @public */
export interface ThemeOptionsElementsCameraCorners {
  svg: ThemeOptionsElementsCameraCornersSvg
}
/** @public */
export interface ThemeOptionsElementsCameraCornersSvg {
  strokeWidth: string
}
/** @public */
export interface ThemeOptionsElementsCameraInstructions {
  host: ThemeOptionsElementsCameraInstructionsHost
  li: ThemeOptionsElementsCameraInstructionsLi
  liText: ThemeOptionsElementsCameraInstructionsLiText
}
/** @public */
export interface ThemeOptionsElementsCameraInstructionsHost {
  gap: string
}
/** @public */
export interface ThemeOptionsElementsCameraInstructionsLi {
  borderRadius: string
  gap: string
  padding: string
}
/** @public */
export interface ThemeOptionsElementsCameraInstructionsLiText {
  fontSize: string
}
/** @public */
export interface ThemeOptionsElementsCameraSelect {
  labels: ThemeOptionsElementsCameraSelectLabels
  labelsHeadline: ThemeOptionsElementsCameraSelectLabelsHeadline
  labelsText: ThemeOptionsElementsCameraSelectLabelsText
  list: ThemeOptionsElementsCameraSelectList
  option: ThemeOptionsElementsCameraSelectOption
}
/** @public */
export interface ThemeOptionsElementsCameraSelectLabels {
  gap: string
  padding: string
}
/** @public */
export interface ThemeOptionsElementsCameraSelectLabelsHeadline {
  fontSize: string
  fontWeight: string
}
/** @public */
export interface ThemeOptionsElementsCameraSelectLabelsText {
  fontSize: string
  fontWeight: string
}
/** @public */
export interface ThemeOptionsElementsCameraSelectList {
  borderRadius: string
  margin: string
  padding: string
  top: string
}
/** @public */
export interface ThemeOptionsElementsCameraSelectOption {
  borderRadius: string
  marginTop: string
  padding: string
}
/** @public */
export interface ThemeOptionsElementsCameraTip {
  backdropFilter: string
  borderRadius: string
  fontSize: string
  fontWeight: string
  height: string
  padding: string
}
/** @public */
export interface ThemeOptionsElementsDialog {
  host: ThemeOptionsElementsDialogHost
}
/** @public */
export interface ThemeOptionsElementsDialogHost {
  border: string
  borderRadius: string
  boxShadow: string
}
/** @public */
export interface ThemeOptionsElementsFaceScanHost {
  borderRadius?: string
}
/** @public */
export interface ThemeOptionsElementsFaceScanCorners {
  strokeWidth: string
}
/** @public */
export interface ThemeOptionsElementsFaceScanScanLine {
  height: string
  animationDuration?: string
}
/** @public */
export interface ThemeOptionsElementsFaceScanWireframe {
  opacity: string
  animationDuration?: string
}
/** @public */
export interface ThemeOptionsElementsFaceScanTransitions {
  borderFade?: string
  wireframeFade?: string
}
/** @public */
export interface ThemeOptionsElementsPoweredBy {
  host: ThemeOptionsElementsPoweredByHost
  icon: ThemeOptionsElementsPoweredByIcon
  span: ThemeOptionsElementsPoweredBySpan
}
/** @public */
export interface ThemeOptionsElementsPoweredByHost {
  gap: string
  height: string
}
/** @public */
export interface ThemeOptionsElementsPoweredByIcon {
  height: string
  width: string
}
/** @public */
export interface ThemeOptionsElementsPoweredBySpan {
  fontSize: string
  fontWeight: string
  letterSpacing: string
}
/** @public */
export interface ThemeOptionsElementsQrcode {
  host: ThemeOptionsElementsQrcodeHost
}
/** @public */
export interface ThemeOptionsElementsQrcodeHost {
  borderRadius: string
  padding: string
}

Interface structure

The theme options structure includes two main properties, colors and elements.

The colors block supports dark mode settings by providing dark and light objects, which each define color settings for the corresponding display mode.

The following diagram shows how each sub-property applies to the user interface.

Annotated screenshot of the authentication error screen showing how color theme properties map to UI elements. The card background maps to the surface property. The card border maps to surfaceVariant. The error animation icon (a red circle with an X) is labeled as the Lottie slot. The heading 'Something went wrong' maps to onSurface, the primary body text color. The error description text maps to onSurfaceVariant, the secondary body text color. The Retry button background maps to primary, the button and spinner color. The button label text maps to onPrimary, the button text color.

The properties of the elements object affect the corresponding UI elements. Each property corresponds to a specific user interface element except for the ae property, which refers to the abstraction layer behind the authentication (a) and enroll (e) operations.

Style changes applied to an element property affect the corresponding user interface element.

Default style values

The following example shows the default style values for PingOne Recognize:

Details
{
  colors: {
    dark: {
      primary: '#1833B8',
      onPrimary: '#FFFFFF',
      secondary: '#FFD900',
      onSecondary: '#1A1A1A',
      error: '#BA3B1B',
      onError: '#FFFFFF',
      secondaryContainer: '#2B2B2B',
      onSecondaryContainer: '#F8F8F8',
      surface: '#14161C',
      onSurface: '#F8F8F8',
      surfaceVariant: '#2B2B2B',
      onSurfaceVariant: '#808080'
    },
    light: {
      primary: '#151E74',
      onPrimary: '#FFFFFF',
      secondary: '#FFDE33',
      onSecondary: '#1A1A1A',
      error: '#BA3B1B',
      onError: '#FFFFFF',
      secondaryContainer: '#F5F5F5',
      onSecondaryContainer: '#1A1A1A',
      surface: '#FFFFFF',
      onSurface: '#1A1A1A',
      surfaceVariant: '#F5F5F5',
      onSurfaceVariant: '#808080'
    }
  },
  elements: {
    button: {
      host: {
        borderRadius: '8px',
        fontSize: '14px',
        fontWeight: '500',
        padding: '16px'
      },
      size: {
        small: {
          host: {
            fontSize: '12px',
            padding: '6px 8px'
          }
        }
      },
      variant: {
        text: {
          host: {
            borderBottom: '1px solid',
            hover: {
              opacity: '0.1'
            }
          }
        }
      }
    },
    camera: {
      host: {
        after: {
          background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.25) 0%, rgba(0, 0, 0, 0) 100%)',
          height: '25%'
        },
        before: {
          background: 'linear-gradient(180deg, rgba(0, 0, 0, 0.25) 0%, rgba(0, 0, 0, 0) 100%)',
          height: '25%'
        }
      }
    },
    cameraCorners: {
      svg: {
        strokeWidth: '3px'
      }
    },
    cameraInstructions: {
      host: {
        gap: '8px'
      },
      li: {
        borderRadius: '8px',
        gap: '16px',
        padding: '16px'
      },
      liText: {
        fontSize: '14px'
      }
    },
    cameraSelect: {
      labels: {
        gap: '12px',
        padding: '16px 0 calc(16px - 8px) 0'
      },
      labelsHeadline: {
        fontSize: '24px',
        fontWeight: '500'
      },
      labelsText: {
        fontSize: '16px',
        fontWeight: '500'
      },
      list: {
        borderRadius: '16px',
        margin: '16px',
        padding: '16px',
        top: '32px !important'
      },
      option: {
        borderRadius: '8px',
        marginTop: '8px',
        padding: '16px'
      }
    },
    cameraTip: {
      backdropFilter: 'blur(4px)',
      borderRadius: '4px',
      fontSize: '10px',
      fontWeight: '700',
      height: '24px',
      padding: '0px 8px'
    },
    dialog: {
      host: {
        border: '1px solid',
        borderRadius: '16px',
        boxShadow: '0px 4px 16px rgba(0, 0, 0, 0.25)'
      }
    },
    poweredBy: {
      host: {
        gap: '6px',
        height: '13px'
      },
      icon: {
        height: '13px',
        width: '139px'
      },
      span: {
        fontSize: '9px',
        fontWeight: '500',
        letterSpacing: '2px'
      }
    },
    qrcode: {
      host: {
        borderRadius: '8px',
        padding: '4px'
      }
    },
    root: {
      buttonCameraSelect: {
        right: 'calc(16px + 18px + 16px)',
        top: '18px'
      },
      buttonCancel: {
        left: '16px',
        top: '16px'
      },
      buttonClose: {
        left: '16px',
        top: '16px'
      },
      buttonFlash: {
        right: '18px',
        top: '18px'
      },
      buttonPin: {
        height: '16px',
        width: '16px'
      },
      buttonsSwitchToMobileChoice: {
        gap: '4px'
      },
      cameraBiometric: {
        width: '100%'
      },
      cameraTip: {
        top: '14px'
      },
      headline: {
        fontSize: '24px',
        fontWeight: '600',
        marginTop: '32px'
      },
      host: {
        borderRadius: '16px',
        gap: '32px',
        padding: '16px 16px 40px 16px'
      },
      poweredBy: {
        bottom: '12px'
      },
      text: {
        fontSize: '16px',
        fontWeight: '400'
      },
      texts: {
        gap: '8px'
      }
    }
  }
}

Example: Setting colors

The following example shows how to change the primary and secondary colors by updating the themeOptions property of the PingOne Recognize authorization object (auth):

Details
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Auth</title>
    <style>
      * {
        box-sizing: border-box;
      }

      body {
        align-items: center;
        display: flex;
        justify-content: center;
        margin: 0;
        min-height: 100vh;
        padding: 8px;
      }

      kl-auth {
        border: 1px solid lightgray;
      }
    </style>
  </head>
  <body>
    <kl-auth
      customer="CUSTOMER_NAME"
      enable-camera-instructions
      key="IMAGE_ENCRYPTION_PUBLIC_KEY"
      lang="en"
      size="375"
      theme="light"
      transaction-data='DATA_FROM_CUSTOMER_SERVER_TO_BE_SIGNED'
      username="USERNAME"
      service-url="KEYLESS_AUTHENTICATION_SERVICE_URL"
    ></kl-auth>
    <script src="./node_modules/@keyless/sdk-web-components/index.js" type="module"></script>
    <script>
      const auth = document.querySelector('kl-auth')

      auth.themeOptions = {
        colors: {
          light: {
            primary: '#000',
            onPrimary: '#fff',
            surface: '#fff',
            onSurface: '#000'
          }
        }
      }
    </script>
  </body>
</html>

Slots

The PingOne Recognize Web SDK uses <slot> elements to customize the following components:

  • <kl-auth>

  • <kl-auth-dialog>

  • <kl-enroll>

  • <kl-enroll-dialog>

Each component supports the following slots:

button-cancel

The circle button with the left arrow icon on the top left of the component. When clicked, it reconnects to the WebSocket and restarts the process from the beginning.

button-close

The circle button with the X icon on the top right of the component. when clicked, it closes the WebSocket connection and activates the close event used by the dialog components to close the dialog.

lottie-spinner

The spinner animation.

lottie-done

The checkmark animation.

lottie-error

The error animation.

texts

The title and description block.

camera-instructions

The list of camera instructions that the user should follow to perform a successful authentication or enrollment.

button-camera-permissions

The button that checks and potentially requests the camera permission to the user.

camera-tip

The text tip on the top of the component when the cameras are on that suggests to the user how to better frame themself, only shown if camera checks are enabled.

camera-select

The select that is shown when the user is on a desktop device and has multiple cameras, grants the user the capability of picking a different camera than the default one.

button-flash

The circle button with the flash icon on the top right of the component, its action is to force a white background on the screen.

camera-backdrop

The blurry camera stream that stays behind the main camera stream, its purpose is mainly for design.

camera-biometric

The main camera stream, useful for the user to adjust their camera quality in realtime.

buttons-stm-choice

The switch to mobile buttons, the primary button leads to a qrcode that lets the user continue the flow on their phone. The secondary button lets the user continue the flow on their current device.

stm-qrcode

The QR code that allows the user to continue the flow on their phone.

button-retry

The retry button, reconnects to the WebSocket and restarts the whole flow from zero.

powered-by

The powered by PingOne Recognize element, always visible when enabled at the bottom of the component.

Example: Changing the spinner

This example changes the appearance of the spinner in the authorization component. It defines a custom CSS style rule (#custom-spinner) and then assigns it to the spinner slot of the <kl-auth> element.

Details
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Auth</title>
    <style>
      * {
        box-sizing: border-box;
      }

      body {
        align-items: center;
        display: flex;
        justify-content: center;
        margin: 0;
        min-height: 100vh;
        padding: 8px;
      }

      kl-auth {
        border: 1px solid lightgray;
      }

      #custom-spinner {
        width: 48px;
        height: 48px;
        border: 5px solid black;
        border-bottom-color: transparent;
        border-radius: 50%;
        display: inline-block;
        box-sizing: border-box;
        animation: rotation 1s linear infinite;
      }

      @keyframes rotation {
        0% {
            transform: rotate(0deg);
        }
        100% {
            transform: rotate(360deg);
        }
      }
    </style>
  </head>
  <body>
    <kl-auth
      customer="CUSTOMER_NAME"
      enable-camera-instructions
      key="IMAGE_ENCRYPTION_PUBLIC_KEY"
      lang="en"
      size="375"
      theme="light"
      transaction-data='DATA_FROM_CUSTOMER_SERVER_TO_BE_SIGNED'
      username="USERNAME"
      service-url="KEYLESS_AUTHENTICATION_SERVICE_URL"
    >
      <div id="custom-spinner" slot="spinner"></div>
    </kl-auth>
    <script src="./node_modules/@keyless/sdk-web-components/index.js" type="module"></script>
  </body>
</html>

Example: Retry when liveness fails

This example shows how to display components in specific situations. Here:

  • The Retry button is initially hidden by assigning a custom CSS rule to a custom button named custom-button-retry.

  • The button is assigned to the button-retry slot of the <kl-auth> elements.

  • Events are added to the button to control visibility when the button is clicked and when the liveness check fails.