UI Customization

The web components have extensive theming and customization capabilities, you can fine tune all the colors and tweak most of the relevant CSS styles of every element, and if that’s not enough you can completely replace the single elements through slots, making the components truly 100% customizable.

The theme can be customized through the theme-options or themeOptions attribute, it accepts an object with the following structure:

interface ThemeOptions {
  colors: {
    dark: {
      primary: string
      onPrimary: string
      secondary: string
      onSecondary: string
      secondaryContainer: string
      onSecondaryContainer: string
      surface: string
      onSurface: string
      surfaceVariant: string
      onSurfaceVariant: string
    }
    light: {
      primary: string
      onPrimary: string
      secondary: string
      onSecondary: string
      secondaryContainer: string
      onSecondaryContainer: string
      surface: string
      onSurface: string
      surfaceVariant: string
      onSurfaceVariant: string
    }
  }
  elements: {
    ae: {
      button: {
        '[enable-powered-by]': {
          marginBottom: string
        }
      }
      buttonCancel: {
        left: string
        top: string
      }
      buttonCameraSelect: {
        right: string
        top: string
      }
      buttonClose: {
        left: string
        top: string
      }
      buttonFlash: {
        right: string
        top: string
      }
      buttonPin: {
        backdropFilter: string
        borderRadius: string
        height: string
        width: string
      }
      cameraBiometric: {
        width: string
      }
      cameraTip: {
        backdropFilter: string
        borderRadius: string
        fontSize: string
        fontWeight: string
        gap: string
        height: string
        padding: string
        top: string
      }
      headline: {
        fontSize: string
        fontWeight: string
        marginTop: string
      }
      host: {
        borderRadius: string
        gap: string
        padding: string
      }
      poweredBy: {
        bottom: string
      }
      text: {
        fontSize: string
        fontWeight: string
      }
      texts: {
        gap: string
      }
    }
    aeDialog: {
      host: {
        border: string
        borderRadius: string
        boxShadow: string
      }
    }
    button: {
      host: {
        borderRadius: string
        fontSize: string
        fontWeight: string
        padding: string
      }
    }
    cameraBackdrop: {
      host: {
        filter: string
        transform: string
      }
    }
    cameraBiometric: {
      host: {
        border: string
        borderRadius: string
      }
    }
    cameraInstructions: {
      host: {
        gap: string
      }
      li: {
        borderRadius: string
        gap: string
        padding: string
      }
      liText: {
        fontSize: string
      }
    }
    cameraSelect: {
      labels: {
        gap: string
        padding: string
      }
      labelsHeadline: {
        fontSize: string
        fontWeight: string
      }
      labelsText: {
        fontSize: string
        fontWeight: string
      }
      list: {
        borderRadius: string
        margin: string
        padding: string
        top: string
      }
      option: {
        borderRadius: string
        marginTop: string
        padding: string
      }
    }
    spinner: {
      path: {
        strokeLineCap: string
        strokeWidth: string
      }
    }
    poweredBy: {
      host: {
        gap: string
      }
      img: {
        height: string
      }
      span: {
        fontSize: string
      }
    }
  }
}

Let’s quickly explain how this interface is structured, the two main blocks are colors and elements, as for colors it is quite simple to grasp, two palettes are exposed which are dark and light, making the components able to support dark and light themes.

To better understand the semantics behind the name of the colors please refer to this illustration:

Regarding the elements block it grants you the capability of changing the styles of all elements, the names are quite straightforward with just one exception: ae, this name is a codename that we reserve for the abstraction layer behind the Auth and Enroll elements, you can probably already tell that the a stand for Auth and the e stands for Enroll.

The host key in each element block refers to the root of that element, meaning that the style is applied directly to the target element.

The other names are quite easy to associate to each element on screen, if unsure please try to tweak the values and test if the expected style change was applied, otherwise please ask Keyless for further info.

Finally, here’s the default values that Keyless uses:

{
  colors: {
    dark: {
      primary: '#BBC3FF',
      onPrimary: '#232C61',
      secondary: '#C4C5DD',
      onSecondary: '#2D2F42',
      secondaryContainer: '#434559',
      onSecondaryContainer: '#E0E1F9',
      surface: '#131318',
      onSurface: '#E4E1E9',
      surfaceVariant: '#46464F',
      onSurfaceVariant: '#C7C5D0'
    },
    light: {
      primary: '#525A92',
      onPrimary: '#FFFFFF',
      secondary: '#5B5D72',
      onSecondary: '#FFFFFF',
      secondaryContainer: '#E0E1F9',
      onSecondaryContainer: '#181A2C',
      surface: '#FBF8FF',
      onSurface: '#1B1B21',
      surfaceVariant: '#E3E1EC',
      onSurfaceVariant: '#46464F'
    }
  },
  elements: {
    ae: {
      button: {
        '[enable-powered-by]': {
          marginBottom: '32px'
        }
      },
      buttonCameraSelect: {
        right: '16px',
        top: '16px'
      },
      buttonCancel: {
        left: '16px',
        top: '16px'
      },
      buttonClose: {
        left: '16px',
        top: '16px'
      },
      buttonFlash: {
        right: '16px',
        top: '16px'
      },
      buttonPin: {
        backdropFilter: 'blur(16px)',
        borderRadius: '24px',
        height: '24px',
        width: '24px'
      },
      cameraBiometric: {
        width: 'calc(100% - 64px)'
      },
      cameraTip: {
        backdropFilter: 'blur(16px)',
        borderRadius: '24px',
        fontSize: '12px',
        fontWeight: '500',
        height: '24px',
        gap: '4px',
        padding: '0 12px',
        top: '16px'
      },
      headline: {
        fontSize: '24px',
        fontWeight: '500',
        marginTop: '32px'
      },
      host: {
        borderRadius: '16px',
        gap: '32px',
        padding: '32px'
      },
      poweredBy: {
        bottom: '32px'
      },
      text: {
        fontSize: '16px',
        fontWeight: '400'
      },
      texts: {
        gap: '8px'
      }
    },
    aeDialog: {
      host: {
        border: 'unset',
        borderRadius: '16px',
        boxShadow: '0px 0px 8px rgba(0, 0, 0, 0.1)',
        maxWidth: 'unset'
      }
    },
    button: {
      host: {
        borderRadius: '8px',
        fontSize: '14px',
        fontWeight: '500',
        padding: '14px 16px'
      }
    },
    cameraBackdrop: {
      host: {
        filter: 'blur(4px) brightness(0.9)',
        transform: 'scale(1.25)'
      }
    },
    cameraBiometric: {
      host: {
        border: '4px solid white',
        borderRadius: '40px'
      }
    },
    cameraInstructions: {
      host: {
        gap: '8px'
      },
      li: {
        borderRadius: '8px',
        gap: '16px',
        padding: '16px'
      },
      liText: {
        fontSize: '14px'
      }
    },
    cameraSelect: {
      labels: {
        gap: '12px',
        padding: '24px 0 calc(24px - 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: '20px 16px'
      }
    },
    poweredBy: {
      host: {
        gap: '6px'
      },
      img: {
        height: '7px'
      },
      span: {
        fontSize: '12px'
      }
    },
    spinner: {
      path: {
        strokeLineCap: 'round',
        strokeWidth: '6px'
      }
    }
  }
}

Here’s an example of changing the primary and surface colors:

<!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
      enable-camera-instructions
      key="IMAGE_ENCRYPTION_PUBLIC_KEY"
      key-id="IMAGE_ENCRYPTION_KEY_ID"
      lang="en"
      size="375"
      theme="light"
      username="USERNAME"
      ws-url="KEYLESS_AUTHENTICATION_SERVICE_URL"
    ></kl-auth>
    <script src="@keyless/sdk-web-components/elements/auth/auth-element.iife.js"></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 slot element is a powerful tool that allows extensive customization in cases like the usage of this package, please refer to the MDN Web Docs to read more about this technology.

The kl-auth, kl-auth-dialog, kl-enroll and kl-enroll-dialog web components support the following slots:

Slot

Description

button-cancel

The circle button with the left arrow icon on the top left of the component, its action is reconnecting to the WebSocket and restarting the whole flow from zero.

button-close

The circle button with the x icon on the top right of the component, its action is closing the WebSocket connection and emitting the close event used by the dialog components to close the dialog.

spinner

The spinner animation.

lottie-done

The checkmark animation.

lottie-error

The error animation.

texts

The title and description block.

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.

button-retry

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

powered-by

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

Let’s look at a few examples on how to use the slots, for example how to change the spinner:

<!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
      enable-camera-instructions
      key="IMAGE_ENCRYPTION_PUBLIC_KEY"
      key-id="IMAGE_ENCRYPTION_KEY_ID"
      lang="en"
      size="375"
      theme="light"
      username="USERNAME"
      ws-url="KEYLESS_AUTHENTICATION_SERVICE_URL"
    >
      <div id="custom-spinner" slot="spinner"></div>
    </kl-auth>
    <script src="@keyless/sdk-web-components/elements/auth/auth-element.iife.js"></script>
  </body>
</html>

Another example that shows how the visibility logic of the retry button could be customized, for example showing the retry button only when the liveness fails:

<!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-button-retry {
        display: none;
      }
    </style>
  </head>
  <body>
    <kl-auth
      enable-camera-instructions
      key="IMAGE_ENCRYPTION_PUBLIC_KEY"
      key-id="IMAGE_ENCRYPTION_KEY_ID"
      lang="en"
      size="375"
      theme="light"
      username="USERNAME"
      ws-url="KEYLESS_AUTHENTICATION_SERVICE_URL"
    >
      <kl-button async="" id="custom-button-retry" slot="button-retry">Retry</kl-button>
    </kl-auth>
    <script src="../dist/elements/auth/auth-element.iife.js"></script>
    <script src="../dist/elements/input/button-element.iife.js"></script>
    <script>
      const auth = document.querySelector('kl-auth')
      const customButtonRetry = document.getElementById('custom-button-retry')

      customButtonRetry.addEventListener('button-click', async (event) => {
        await auth.onClickRetry(event)
        customButtonRetry.style.display = 'none'
      })

      auth.addEventListener('liveness-failed', () => {
        customButtonRetry.style.display = 'flex'
      })
    </script>
  </body>
</html>

Last updated