Styling
Customize the appearance of the ThunderPhone voice widget with CSS
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.
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. They are defined by the theme class (.tp--light or .tp--dark) applied to the .tp-widget root:
| Property | Default (light) | Default (dark) | Description |
|---|---|---|---|
--tp-accent | #000 | #fff | Accent color: start button, waveform bars, connecting dot, connected-status text. Set inline from the primaryColor prop. |
--tp-bg | rgba(255, 255, 255, 0.82) | rgba(15, 15, 15, 0.85) | Bar background (translucent; blurred by --tp-glass). |
--tp-surface | rgba(0, 0, 0, 0.04) | rgba(255, 255, 255, 0.07) | Mute-button background. |
--tp-surface-hover | rgba(0, 0, 0, 0.07) | rgba(255, 255, 255, 0.12) | Mute-button hover background. |
--tp-border | rgba(0, 0, 0, 0.08) | rgba(255, 255, 255, 0.1) | Bar and button borders. |
--tp-border-hover | rgba(0, 0, 0, 0.14) | rgba(255, 255, 255, 0.18) | Border color on hover. |
--tp-text | rgba(0, 0, 0, 0.88) | rgba(255, 255, 255, 0.95) | Primary text (title, agent name). |
--tp-text-2 | rgba(0, 0, 0, 0.5) | rgba(255, 255, 255, 0.55) | Secondary text (subtitle, status line, call timer). |
--tp-glass | blur(32px) saturate(180%) | blur(32px) saturate(180%) | backdrop-filter that creates the glass effect on the bar. |
--tp-shadow | three-layer shadow stack | three-layer shadow stack | The bar's box-shadow (ring + near + far layers). |
--tp-shadow-hover | three-layer shadow stack | three-layer shadow stack | Declared for hover elevation; not currently applied by any rule. |
--tp-glow | inset 0 1px 0 0 rgba(255,255,255,0.5) | inset 0 1px 0 0 rgba(255,255,255,0.06) | Inner top highlight layered onto the bar shadow. |
--tp-connected | #059669 | #34d399 | Connected-state indicator color (status dot). |
--tp-error | #dc2626 | #fb7185 | Error-state status text color. |
--tp-end-bg | rgba(239, 68, 68, 0.08) | rgba(251, 113, 133, 0.12) | End-call button background. |
--tp-end-color | #ef4444 | #fb7185 | End-call button icon color. |
--tp-end-border | rgba(239, 68, 68, 0.12) | rgba(251, 113, 133, 0.15) | End-call button border. |
--tp-end-hover | rgba(239, 68, 68, 0.14) | rgba(251, 113, 133, 0.2) | End-call button hover background. |
--tp-idle-opacity | 0.4 | 0.3 | Declared for idle-state dimming; not currently applied by any rule. |
Overriding Custom Properties
Set the accent color via the primaryColor prop:
<ThunderPhoneWidget
publishableKey="pk_live_your_publishable_key"
primaryColor="#e11d48"
/>Override the other custom properties with CSS. Use a two-class selector (.tp-widget.tp--light / .tp-widget.tp--dark) so your rule outweighs the theme class that defines the defaults, regardless of stylesheet order:
.tp-widget.tp--light {
--tp-bg: rgba(0, 0, 0, 0.9);
--tp-text: rgba(255, 255, 255, 0.95);
--tp-text-2: rgba(255, 255, 255, 0.55);
--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 | Root wrapper | Fixed-position container (position: fixed, corner set by the position prop, z-index: 9999). Carries the theme class and base font settings; no visual chrome of its own. |
.tp--light / .tp--dark | Theme modifiers | Applied to .tp-widget alongside the theme; define all --tp-* custom properties. |
.tp-bar | The bar | The glassmorphic pill itself: background, backdrop blur, border, 99px radius, shadow. 300px wide. |
.tp-meta | Text block | Container for all text -- title and subtitle when idle, agent name and status during a call. |
.tp-name | Primary label | Shows the title prop when idle, and the connected agent's name (falling back to title) during a call. |
.tp-sub | Subtitle | The "Available now" line shown when idle. |
.tp-start | Idle call button | The circular accent start button (42px). Uses --tp-accent as background. |
.tp-dot | Connecting dot | Pulsing accent dot shown at the left of the bar while connecting. |
.tp-wave / .tp-wave--idle | Waveform | The five-bar waveform. --idle adds the slow breathing animation; during a call the bars are audio-reactive. |
.tp-button | In-call buttons | Base style for the in-call controls (42px, rounded 12px). |
.tp-button-group | Button row | Wraps the mute and end buttons during a call. |
.tp-button--start | Connect button variant | Accent-colored variant shown while a call is starting. |
.tp-button--mute | Mute toggle | Mutes/unmutes the mic during a call. Uses --tp-surface. |
.tp-button--end | End call button | Hangs up. Uses the --tp-end-* palette. |
.tp-button--loading | Loading modifier | Dims the button while connecting. |
.tp-icon / .tp-spin | Icons | Button icon sizing; tp-spin animates the connecting spinner. |
.tp-status | In-call status block | Wraps the status line during connecting/connected/error states. |
.tp-status__text | Status line | Connection state text (e.g., "Connecting...") or the call timer. Gets .tp-status--connected (accent color) or .tp-status--error (error color) per state. |
.tp-status__name | Agent-name slot | Part of the status block, but not rendered in the current bar layout -- the agent name appears in .tp-name instead. |
.tp-status__dot | Status dot | Pulsing connected-state dot style (uses --tp-connected). |
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. Remember that the accent comes from the primaryColor prop, not CSS:
<ThunderPhoneWidget
publishableKey="pk_live_your_publishable_key"
primaryColor="#059669"
/>/* Emerald theme for everything else */
.tp-widget.tp--light {
--tp-bg: rgba(236, 253, 245, 0.85);
--tp-text: rgba(6, 78, 59, 0.95);
--tp-text-2: rgba(4, 120, 87, 0.8);
--tp-border: rgba(5, 150, 105, 0.2);
}Custom Size
Make the widget larger or smaller by adjusting the bar, button, and text dimensions:
/* Wider bar */
.tp-bar {
width: 340px;
}
/* Larger buttons (42px by default) */
.tp-start,
.tp-button {
width: 56px;
height: 56px;
}
/* Larger text */
.tp-name {
font-size: 16px;
}
.tp-sub,
.tp-status__text {
font-size: 14px;
}Hide the Text Labels
All of the widget's text lives in .tp-meta. Hide it entirely to keep just the waveform and buttons:
.tp-meta {
display: none;
}Or hide individual pieces:
/* Hide only the idle "Available now" subtitle */
.tp-sub {
display: none;
}
/* Hide only the in-call status line (connection state / timer) */
.tp-status {
display: none;
}Theme-Specific Overrides
Target a specific theme with the theme class:
/* Only affect dark theme */
.tp--dark .tp-start {
box-shadow: 0 0 20px rgba(255, 255, 255, 0.25);
}
/* Only affect light theme */
.tp-widget.tp--light {
--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--dark {
--tp-bg: rgba(30, 30, 46, 0.9);
}
.support-widget .tp-name {
font-weight: 700;
}This lets you have multiple widget instances on the same page with different styles. Give each instance its own accent through its primaryColor prop (CSS cannot override --tp-accent -- it is set inline).
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>
)
}