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
}
buttonsStmChoice: {
gap: 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
}
size: {
small: {
host: {
fontSize: string
padding: string
}
}
}
variant: {
text: {
host: {
borderBottom: string
hover: {
opacity: 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
}
}
poweredBy: {
host: {
gap: string
}
img: {
height: string
}
span: {
fontSize: string
}
}
qrcode: {
host: {
borderRadius: string
padding: 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: '#193CE6',
onPrimary: '#FFFFFF',
secondary: '#FFDE33',
onSecondary: '#1A1A1A',
secondaryContainer: '#2B2B2B',
onSecondaryContainer: '#F8F8F8',
surface: '#1A1A1A',
onSurface: '#F8F8F8',
surfaceVariant: '#2B2B2B',
onSurfaceVariant: '#F8F8F8'
},
light: {
primary: '#1430B8',
onPrimary: '#FFFFFF',
secondary: '#FFDE33',
onSecondary: '#1A1A1A',
secondaryContainer: '#F5F5F5',
onSecondaryContainer: '#1A1A1A',
surface: '#FFFFFF',
onSurface: '#1A1A1A',
surfaceVariant: '#F5F5F5',
onSurfaceVariant: '#1A1A1A'
}
},
elements: {
ae: {
button: {
'[enable-powered-by]': {
marginBottom: '32px'
}
},
buttonCameraSelect: {
right: '48px',
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'
},
buttonsStmChoice: {
gap: '4px'
},
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'
},
size: {
small: {
host: {
fontSize: '12px',
padding: '6px 8px'
}
}
},
variant: {
text: {
host: {
borderBottom: '1px solid',
hover: {
opacity: '0.1'
}
}
}
}
},
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'
}
},
qrcode: {
host: {
borderRadius: '8px',
padding: '4px'
}
}
}
}
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
customer="CUSTOMER_NAME"
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="./node_modules/@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.
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 qrcode 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 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
customer="CUSTOMER_NAME"
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="./node_modules/@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
customer="CUSTOMER_NAME"
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="./node_modules/@keyless/sdk-web-components/elements/auth/auth-element.iife.js"></script>
<script src="./node_modules/@keyless/sdk-web-components/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
Was this helpful?