The widget renders as a glassmorphic bar with built-in light and dark themes. Customization is available at three levels: props for common options, CSS custom properties for theming, and CSS class overrides for full control.
These styling options apply to the pre-built widget rendered by the ThunderPhoneWidget React component and the ThunderPhone.mount() CDN method. If you need a completely custom UI, use the headless hook instead.
Themes
The theme prop controls the widget’s color scheme. It applies a tp--light or tp--dark class to the widget root:
<ThunderPhoneWidget
publishableKey="pk_live_your_publishable_key"
theme="dark"
/>
| Theme | Class | Description |
|---|
'light' | tp--light | Light background with dark text. Default. |
'dark' | tp--dark | Dark background with light text. |
Both themes use the glassmorphic bar design with backdrop blur and subtle transparency.
CSS Custom Properties
The widget exposes CSS custom properties (variables) that you can override to change colors without touching individual classes. These are set on the .tp-widget root element:
| Property | Default (light) | Default (dark) | Description |
|---|
--tp-accent | #6366f1 | #6366f1 | Primary accent color (call button, active indicators). Set via the primaryColor prop. |
--tp-bg | rgba(255, 255, 255, 0.8) | rgba(23, 23, 23, 0.8) | Widget background color (with transparency for glassmorphism). |
--tp-text | #1a1a1a | #f5f5f5 | Primary text color. |
--tp-text-secondary | #6b7280 | #a1a1aa | Secondary text color (status text, timer). |
--tp-border | rgba(0, 0, 0, 0.1) | rgba(255, 255, 255, 0.1) | Border color. |
Overriding Custom Properties
You can set the accent color via the primaryColor prop:
<ThunderPhoneWidget
publishableKey="pk_live_your_publishable_key"
primaryColor="#e11d48"
/>
Or override any custom property with CSS:
.tp-widget {
--tp-accent: #e11d48;
--tp-bg: rgba(0, 0, 0, 0.9);
--tp-text: #ffffff;
--tp-text-secondary: #a1a1aa;
--tp-border: rgba(255, 255, 255, 0.15);
}
CSS Classes
All widget classes are prefixed with tp- to avoid conflicts with your existing styles.
| Class | Element | Description |
|---|
.tp-widget | Outer container | The root wrapper. Applies the glassmorphic bar style with backdrop blur. |
.tp--light | Theme modifier | Applied to .tp-widget when theme="light". |
.tp--dark | Theme modifier | Applied to .tp-widget when theme="dark". |
.tp-button | All buttons | Base style for all circular buttons (44px default). |
.tp-button--start | Start / mic button | The primary call button. Uses --tp-accent as background. |
.tp-button--mute | Mute toggle button | Shown during an active call to mute/unmute the mic. |
.tp-button--end | End call button | Shown during an active call to hang up. |
.tp-status | Status text container | Wraps the title and connection state text. |
.tp-status__name | Title / agent name | Displays the title prop value or the connected agent’s name. |
.tp-status__text | State / timer text | Shows connection state (e.g., “Connecting…”) or call duration. |
Examples
Custom Accent via Props
The simplest way to brand the widget:
<ThunderPhoneWidget
publishableKey="pk_live_your_publishable_key"
theme="light"
primaryColor="#059669"
title="Talk to support"
/>
Custom Colors via CSS
Override the custom properties for full color control:
/* Emerald theme */
.tp-widget {
--tp-accent: #059669;
--tp-bg: rgba(236, 253, 245, 0.85);
--tp-text: #064e3b;
--tp-text-secondary: #047857;
--tp-border: rgba(5, 150, 105, 0.2);
}
Custom Size
Make the widget larger or smaller by adjusting the button dimensions:
/* Larger buttons */
.tp-button {
width: 56px;
height: 56px;
}
/* Larger status text */
.tp-status__name {
font-size: 16px;
}
.tp-status__text {
font-size: 14px;
}
Hide Status Text
If you only want the buttons without the status labels:
.tp-status {
display: none;
}
Theme-Specific Overrides
Target a specific theme with the theme class:
/* Only affect dark theme */
.tp--dark .tp-button--start {
box-shadow: 0 0 20px rgba(99, 102, 241, 0.4);
}
/* Only affect light theme */
.tp--light .tp-widget {
--tp-bg: rgba(255, 255, 255, 0.95);
}
Scoping with className
When using the React component, pass a className prop to scope your overrides to a specific widget instance:
<ThunderPhoneWidget
publishableKey="pk_live_your_publishable_key"
theme="dark"
className="support-widget"
/>
Then target that class in your CSS:
.support-widget.tp-widget {
--tp-accent: #8e44ad;
}
.support-widget .tp-status__name {
font-weight: 700;
}
This lets you have multiple widget instances on the same page with different styles.
Fully Custom UI
If CSS overrides are not enough, the headless hook gives you full control. You provide all the HTML and styling while useThunderPhone handles the voice session. The hook also provides audioLevelRef for building audio-reactive visualizations like waveforms.
import { useThunderPhone } from '@thunderphone/widget'
function MyWidget() {
const phone = useThunderPhone({
publishableKey: 'pk_live_your_publishable_key',
})
return (
<div className="my-totally-custom-widget">
{/* Your own buttons, animations, layouts -- anything */}
<button onClick={phone.state === 'connected' ? phone.disconnect : phone.connect}>
{phone.state === 'connected' ? 'Hang up' : 'Call us'}
</button>
{phone.audio}
</div>
)
}
The headless hook is the right choice when you need audio-reactive animations, custom layouts, or integration into an existing component library. CSS overrides and custom properties are better for quick theming adjustments.