On Air · Theme guide

A sign of your own.
Built from CSS.

On Air shows your live Teams status full screen in any browser. This guide names the status classes and the shell hooks a theme can style, so you can build your own look and paste it into the theme editor.

What On Air is.

On Air is a full-screen status display that runs in any browser and shows your live Teams status. It needs no commands, and it never joins your calls.

Quick start.

  1. Install the desktop app and sign in. Download Call State for Mac or Windows and finish the permissions setup. The app senses the status the display shows.
  2. Open the theme editor. Sign in at https://account.callstate.app/ and open On Air themes.
  3. Start from a ready-made theme. neon is the built-in default, and teal, console and status-window ship with the editor. Copy one and edit it.
  4. Check every state. The preview carries one toggle per status class. Flip them to see the sign, the panel and the offline labels.
  5. Save the theme. The saved CSS is exactly what the player injects.
  6. Share it. Copy the theme link, or open it in a new window.

Ready for your next call

Start on your next call.

Sign in to the editor, copy a theme and make the sign your own.

Or download the desktop app to get started.

How a theme works.

The player puts cs-root and exactly one class per concern on the <html> element. It replaces the whole set on every change, so the two poles of one concern are never present together.

A theme styles the fixed hooks below that root. The markup is product-owned and stays the same, so only your CSS changes the look.

Which text variant is visible is product behaviour, not skin. The built-in rules already show the right headline, description and signal value for the current classes.

Write selectors with the root classes in front, for example .cs-root.cs-in-meeting .cs-on-air. Then the same CSS renders identically in the editor preview and in the player.

Status classes.

Ten concerns, two poles each. Exactly one class per concern sits on the root at all times.

The root class vocabulary
Transportcs-connected / cs-no-connectionThe hub link is up.
Producercs-tray-online / cs-no-trayAt least one Call State app is online for the account.
Meetingcs-in-meeting / cs-idleA meeting is running.
Miccs-mic-on / cs-mic-offThe mic is open.
Cameracs-cam-on / cs-cam-offThe camera is on.
Ringingcs-ringing / cs-not-ringingA call rings before you join it.
Handcs-hand-raised / cs-hand-loweredYour hand is up.
Sharecs-sharing / cs-not-sharingYour screen is shared.
Recordingcs-recording / cs-not-recordingThe meeting is recorded.
Chat panecs-chat-open / cs-chat-closedThe chat pane is visible.

An unknown value counts as the off pole. A mic the app cannot read is shown as muted, and the same rule puts camera, hand, share, recording and chat on their off pole. Chat open means the pane is visible, never that messages are unread.

The four states a theme usually draws
Available.cs-idle.cs-not-ringing
Incoming call.cs-idle.cs-ringing
In a call.cs-in-meeting.cs-not-sharing
Presenting.cs-in-meeting.cs-sharing

Shell hooks.

Two blocks of markup hang under the root: the sign and the status panel. The built-in skin draws the sign and keeps the panel hidden until a theme shows it.

Sign hooks
cs-shellThe single content wrapper inside the root.
cs-signThe bordered sign. Visible while the transport and a producer are online.
cs-on-airThe “On Air” line.
cs-togglesThe row that holds the two lamps.
cs-toggleOne lamp. cs-toggle-mic and cs-toggle-cam say which.
cs-toggle-nameThe lamp label (“Mic” or “Cam”).
cs-toggle-stateOne pole of the lamp: cs-toggle-state-on or cs-toggle-state-off.
cs-offlineThe full-screen label while the display is offline: cs-offline-connection or cs-offline-tray.
Panel hooks
cs-panelThe whole status panel. Hidden by the built-in skin; a theme shows it.
cs-barThe title bar, with cs-bar-dots, cs-bar-title and cs-bar-tag.
cs-statusThe block that holds source, headline, timer and description.
cs-lampThe status lamp. cs-lamp-lens is an empty inner element for a lens or a glow.
cs-sourceThe source line: cs-source-mark, cs-source-name and cs-source-device (the device name, empty when unknown).
cs-stateThe headline. One cs-state-text per state: cs-state-available, cs-state-ringing, cs-state-call, cs-state-presenting.
cs-timerThe elapsed call time, ticking in the player. Empty outside a meeting.
cs-descriptionOne cs-description-text per state, and cs-description-part pieces for mic, camera and the tail.
cs-signalsTwo groups, cs-signal-group-you and cs-signal-group-meeting, each with a cs-signal-group-label and a cs-signal-list.
cs-signalOne tile: cs-signal-mic, cs-signal-cam, cs-signal-hand, cs-signal-share, cs-signal-rec, cs-signal-chat.
cs-signal-topThe top row of a tile, with cs-signal-icon, cs-signal-name and cs-signal-led.
cs-signal-valueOne pole per tile: cs-signal-value-on, cs-signal-value-off and cs-signal-value-unknown.
Full markup reference

The default shell markup, exactly as the player and the editor preview render it. A theme styles these hooks, or replaces the whole tree with its own HTML fragment.

<div class="cs-shell">
  <div class="cs-sign">
    <p class="cs-on-air">On Air</p>
    <div class="cs-toggles">
      <div class="cs-toggle cs-toggle-mic">
        <span class="cs-toggle-name">Mic</span>
        <span class="cs-toggle-state cs-toggle-state-on">On</span>
        <span class="cs-toggle-state cs-toggle-state-off">Off</span>
      </div>
      <div class="cs-toggle cs-toggle-cam">
        <span class="cs-toggle-name">Cam</span>
        <span class="cs-toggle-state cs-toggle-state-on">On</span>
        <span class="cs-toggle-state cs-toggle-state-off">Off</span>
      </div>
    </div>
  </div>
  <div class="cs-panel">
    <div class="cs-bar">
      <span class="cs-bar-dots" aria-hidden="true"><i></i><i></i><i></i></span>
      <span class="cs-bar-title">Call State</span>
      <span class="cs-bar-tag">Live</span>
    </div>
    <div class="cs-status">
      <span class="cs-lamp" aria-hidden="true"><span class="cs-lamp-lens"></span></span>
      <p class="cs-source">
        <span class="cs-source-mark" aria-hidden="true">T</span>
        <span class="cs-source-name">Microsoft Teams</span>
        <span class="cs-source-device">Work laptop</span>
      </p>
      <p class="cs-state">
        <span class="cs-state-text cs-state-available">Available</span>
        <span class="cs-state-text cs-state-ringing">Incoming call</span>
        <span class="cs-state-text cs-state-call">In a call</span>
        <span class="cs-state-text cs-state-presenting">Presenting</span>
      </p>
      <p class="cs-timer">00:12:58</p>
      <p class="cs-description">
        <span class="cs-description-text cs-description-available">No active meeting. Call signals are unavailable.</span>
        <span class="cs-description-text cs-description-ringing">Not joined yet. Call signals are unavailable.</span>
        <span class="cs-description-text cs-description-meeting">
          <span class="cs-description-part cs-description-mic-on">Mic on. </span>
          <span class="cs-description-part cs-description-mic-off">Mic muted. </span>
          <span class="cs-description-part cs-description-cam-on">Camera on. </span>
          <span class="cs-description-part cs-description-cam-off">Camera off. </span>
          <span class="cs-description-part cs-description-tail-default">Your tools know.</span>
          <span class="cs-description-part cs-description-tail-hand">Hand raised.</span>
          <span class="cs-description-part cs-description-tail-share">Screen shared.</span>
        </span>
      </p>
    </div>
    <div class="cs-signals">
      <div class="cs-signal-group cs-signal-group-you">
        <p class="cs-signal-group-label">You</p>
        <div class="cs-signal-list">
          <div class="cs-signal cs-signal-mic">
            <span class="cs-signal-top">
              <svg class="cs-signal-icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="2" width="6" height="13" rx="3" /><path d="M5 10v2a7 7 0 0 0 14 0v-2M12 19v3m-4 0h8" /></svg>
              <span class="cs-signal-name">Mic</span>
              <span class="cs-signal-led" aria-hidden="true"></span>
            </span>
            <span class="cs-signal-value cs-signal-value-on">On</span>
            <span class="cs-signal-value cs-signal-value-off">Muted</span>
            <span class="cs-signal-value cs-signal-value-unknown">Unknown</span>
          </div>
          <div class="cs-signal cs-signal-cam">
            <span class="cs-signal-top">
              <svg class="cs-signal-icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="6" width="12" height="12" rx="2" /><path d="m15 10 6-4v12l-6-4" /></svg>
              <span class="cs-signal-name">Cam</span>
              <span class="cs-signal-led" aria-hidden="true"></span>
            </span>
            <span class="cs-signal-value cs-signal-value-on">On</span>
            <span class="cs-signal-value cs-signal-value-off">Off</span>
            <span class="cs-signal-value cs-signal-value-unknown">Unknown</span>
          </div>
          <div class="cs-signal cs-signal-hand">
            <span class="cs-signal-top">
              <svg class="cs-signal-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 12V6a2 2 0 0 1 4 0v5-7a2 2 0 0 1 4 0v7-5a2 2 0 0 1 4 0v10a6 6 0 0 1-10 4L4 14a2 2 0 0 1 3-3l3 3" /></svg>
              <span class="cs-signal-name">Hand</span>
              <span class="cs-signal-led" aria-hidden="true"></span>
            </span>
            <span class="cs-signal-value cs-signal-value-on">Raised</span>
            <span class="cs-signal-value cs-signal-value-off">Lowered</span>
            <span class="cs-signal-value cs-signal-value-unknown">Unknown</span>
          </div>
        </div>
      </div>
      <div class="cs-signal-group cs-signal-group-meeting">
        <p class="cs-signal-group-label">Meeting</p>
        <div class="cs-signal-list">
          <div class="cs-signal cs-signal-share">
            <span class="cs-signal-top">
              <svg class="cs-signal-icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2" /><path d="M12 14V6m-3 3 3-3 3 3M8 21h8m-4-4v4" /></svg>
              <span class="cs-signal-name">Share</span>
              <span class="cs-signal-led" aria-hidden="true"></span>
            </span>
            <span class="cs-signal-value cs-signal-value-on">Sharing</span>
            <span class="cs-signal-value cs-signal-value-off">Off</span>
            <span class="cs-signal-value cs-signal-value-unknown">Unknown</span>
          </div>
          <div class="cs-signal cs-signal-rec">
            <span class="cs-signal-top">
              <svg class="cs-signal-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" /><circle cx="12" cy="12" r="3" /></svg>
              <span class="cs-signal-name">Rec</span>
              <span class="cs-signal-led" aria-hidden="true"></span>
            </span>
            <span class="cs-signal-value cs-signal-value-on">Recording</span>
            <span class="cs-signal-value cs-signal-value-off">Off</span>
            <span class="cs-signal-value cs-signal-value-unknown">Unknown</span>
          </div>
          <div class="cs-signal cs-signal-chat">
            <span class="cs-signal-top">
              <svg class="cs-signal-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4h14a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H9l-6 3V6a2 2 0 0 1 2-2Z" /><path d="M7 9h10m-10 4h6" /></svg>
              <span class="cs-signal-name">Chat</span>
              <span class="cs-signal-led" aria-hidden="true"></span>
            </span>
            <span class="cs-signal-value cs-signal-value-on">Open</span>
            <span class="cs-signal-value cs-signal-value-off">Closed</span>
            <span class="cs-signal-value cs-signal-value-unknown">Unknown</span>
          </div>
        </div>
      </div>
    </div>
  </div>
  <p class="cs-offline cs-offline-connection">No Connection</p>
  <p class="cs-offline cs-offline-tray">No Tray</p>
</div>

Two worked examples.

Both examples only re-colour the built-in shell. The rules that show and hide the sign, the panel and the offline labels stay in place, so the preview and the player behave the same.

Minimal sign

A dark page, grey letters, and a glow that follows the status: red in a call, amber while a call rings.

/* A. Minimal sign: grey letters, a red glow in a call, amber while ringing. */
html, body { background: #0B0F0E; }

.cs-on-air { color: #6B7280; text-shadow: none; }

.cs-root.cs-in-meeting .cs-sign {
  border-color: #EF4444;
  box-shadow: 0 0 40px #EF4444;
}

.cs-root.cs-idle.cs-ringing .cs-sign {
  border-color: #F59E0B;
  box-shadow: 0 0 40px #F59E0B;
}

Panel instead of the sign

Hide the sign, show the status panel while the transport and a producer are online, and highlight an open signal.

/* B. Panel instead of the sign: a tile board. */
.cs-root .cs-sign,
.cs-root.cs-connected.cs-tray-online .cs-sign { display: none; }

.cs-root.cs-connected.cs-tray-online .cs-panel {
  display: block;
  padding: 24px;
  background: #132B27;
  color: #ECF2E9;
}

.cs-signal-group-label { display: none; }

.cs-signal-list { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }

.cs-signal { padding: 12px; border: 1px solid #476459; border-radius: 8px; }

.cs-signal-value-on { color: #22C55E; font-weight: 700; }

Rules for a theme.

  • Never set display on cs-state-text, cs-description-text, cs-description-part or cs-signal-value. The product owns which text variant is visible. Override it and you break every state.
  • The timer is the one exception. If you set display on cs-timer, repeat the rule that hides it outside a meeting: .cs-root.cs-idle .cs-timer { display: none; }.
  • The device name and the timer may be empty. The player binds both, so hide them with :empty when the app sends nothing.
  • Tiles are indicators, not buttons. The display sends no command. No hover, no pressed state, no key caps, no “press to try” wording.
  • Style the offline states too. A kiosk spends real time with no connection or no app, so give cs-offline-connection and cs-offline-tray a look.

Optional HTML and fonts.

A theme may also ship an HTML fragment. It replaces the .cs-shell tree, and an empty fragment keeps the default shell. Keep the hooks you want to style, plus .cs-source-device and .cs-timer if you want the live device name and the call time.

The root status classes stay on the <html> element either way, so a custom fragment changes no selector.

The server checks the fragment on save. It rejects script, iframe, forms, images and other media, every on… attribute and any external URL. It strips style attributes, so CSS stays the styling channel.

Fonts come as up to five Google Font family names, for example Archivo or IBM Plex Mono. Letters, digits and spaces only, never a URL. The player loads the families it is given.

Sharing a theme.

A theme link is only a look. Every viewer signs in and sees their own status, never yours. The link format is https://onair.callstate.app/{id}.

The root address https://onair.callstate.app/ is the built-in default and needs no theme row.