Skip to main content
The widget UI uses CSS classes prefixed with tp- that you can override to match your brand. All customization works through standard CSS — no JavaScript configuration needed.
These CSS classes 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.

CSS Classes

All widget classes are prefixed with tp- to avoid conflicts with your existing styles.
ClassElementDescription
.tp-widgetOuter containerThe root wrapper. Uses inline-flex layout.
.tp-buttonAll buttonsBase style for all circular buttons (44px default).
.tp-button--startStart / mic buttonThe primary call button shown in the idle state.
.tp-button--muteMute toggle buttonShown during an active call to mute/unmute the mic.
.tp-button--endEnd call buttonShown during an active call to hang up.
.tp-statusStatus text containerWraps the agent name and connection state text.
.tp-status__nameAgent nameDisplays the connected agent’s name.
.tp-status__textState / timer textShows connection state (e.g., “Connecting…”) or call duration.

Examples

Custom Colors

Override the button colors to match your brand:
/* Brand-colored start button */
.tp-button--start {
  background-color: #4a90d9;
}

.tp-button--start:hover {
  background-color: #3a7bc8;
}

/* Red end button */
.tp-button--end {
  background-color: #e74c3c;
}

.tp-button--end:hover {
  background-color: #c0392b;
}

/* Subtle mute button */
.tp-button--mute {
  background-color: #95a5a6;
}

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;
}

Dark Theme

.tp-widget {
  color: #ffffff;
}

.tp-button--start {
  background-color: #2ecc71;
  color: #ffffff;
}

.tp-button--end {
  background-color: #e74c3c;
  color: #ffffff;
}

.tp-button--mute {
  background-color: #34495e;
  color: #ffffff;
}

.tp-status__name {
  color: #ecf0f1;
}

.tp-status__text {
  color: #bdc3c7;
}

Scoping with className

When using the React component, pass a className prop to scope your overrides to a specific widget instance:
<ThunderPhoneWidget
  apiKey="pk_live_your_publishable_key"
  agentId={123}
  className="support-widget"
/>
Then target that class in your CSS:
.support-widget .tp-button--start {
  background-color: #8e44ad;
  border-radius: 8px;
}

.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:
import { useThunderPhone } from '@thunderphone/widget'

function MyWidget() {
  const phone = useThunderPhone({
    apiKey: 'pk_live_your_publishable_key',
    agentId: 123,
  })

  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 animations, custom layouts, or integration into an existing component library. CSS overrides are better for quick color and size adjustments.