ThunderPhone 2.0 is live.Self-serve, from 2¢/min.Read the announcement

Widget

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"
/>
ThemeClassDescription
'light'tp--lightLight background with dark text. Default.
'dark'tp--darkDark 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:

PropertyDefault (light)Default (dark)Description
--tp-accent#000#fffAccent color: start button, waveform bars, connecting dot, connected-status text. Set inline from the primaryColor prop.
--tp-bgrgba(255, 255, 255, 0.82)rgba(15, 15, 15, 0.85)Bar background (translucent; blurred by --tp-glass).
--tp-surfacergba(0, 0, 0, 0.04)rgba(255, 255, 255, 0.07)Mute-button background.
--tp-surface-hoverrgba(0, 0, 0, 0.07)rgba(255, 255, 255, 0.12)Mute-button hover background.
--tp-borderrgba(0, 0, 0, 0.08)rgba(255, 255, 255, 0.1)Bar and button borders.
--tp-border-hoverrgba(0, 0, 0, 0.14)rgba(255, 255, 255, 0.18)Border color on hover.
--tp-textrgba(0, 0, 0, 0.88)rgba(255, 255, 255, 0.95)Primary text (title, agent name).
--tp-text-2rgba(0, 0, 0, 0.5)rgba(255, 255, 255, 0.55)Secondary text (subtitle, status line, call timer).
--tp-glassblur(32px) saturate(180%)blur(32px) saturate(180%)backdrop-filter that creates the glass effect on the bar.
--tp-shadowthree-layer shadow stackthree-layer shadow stackThe bar's box-shadow (ring + near + far layers).
--tp-shadow-hoverthree-layer shadow stackthree-layer shadow stackDeclared for hover elevation; not currently applied by any rule.
--tp-glowinset 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#34d399Connected-state indicator color (status dot).
--tp-error#dc2626#fb7185Error-state status text color.
--tp-end-bgrgba(239, 68, 68, 0.08)rgba(251, 113, 133, 0.12)End-call button background.
--tp-end-color#ef4444#fb7185End-call button icon color.
--tp-end-borderrgba(239, 68, 68, 0.12)rgba(251, 113, 133, 0.15)End-call button border.
--tp-end-hoverrgba(239, 68, 68, 0.14)rgba(251, 113, 133, 0.2)End-call button hover background.
--tp-idle-opacity0.40.3Declared 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.

ClassElementDescription
.tp-widgetRoot wrapperFixed-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--darkTheme modifiersApplied to .tp-widget alongside the theme; define all --tp-* custom properties.
.tp-barThe barThe glassmorphic pill itself: background, backdrop blur, border, 99px radius, shadow. 300px wide.
.tp-metaText blockContainer for all text -- title and subtitle when idle, agent name and status during a call.
.tp-namePrimary labelShows the title prop when idle, and the connected agent's name (falling back to title) during a call.
.tp-subSubtitleThe "Available now" line shown when idle.
.tp-startIdle call buttonThe circular accent start button (42px). Uses --tp-accent as background.
.tp-dotConnecting dotPulsing accent dot shown at the left of the bar while connecting.
.tp-wave / .tp-wave--idleWaveformThe five-bar waveform. --idle adds the slow breathing animation; during a call the bars are audio-reactive.
.tp-buttonIn-call buttonsBase style for the in-call controls (42px, rounded 12px).
.tp-button-groupButton rowWraps the mute and end buttons during a call.
.tp-button--startConnect button variantAccent-colored variant shown while a call is starting.
.tp-button--muteMute toggleMutes/unmutes the mic during a call. Uses --tp-surface.
.tp-button--endEnd call buttonHangs up. Uses the --tp-end-* palette.
.tp-button--loadingLoading modifierDims the button while connecting.
.tp-icon / .tp-spinIconsButton icon sizing; tp-spin animates the connecting spinner.
.tp-statusIn-call status blockWraps the status line during connecting/connected/error states.
.tp-status__textStatus lineConnection 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__nameAgent-name slotPart of the status block, but not rendered in the current bar layout -- the agent name appears in .tp-name instead.
.tp-status__dotStatus dotPulsing 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>
  )
}