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.
- 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.
- Open the theme editor. Sign in at
https://account.callstate.app/and open On Air themes. - Start from a ready-made theme.
neonis the built-in default, andteal,consoleandstatus-windowship with the editor. Copy one and edit it. - Check every state. The preview carries one toggle per status class. Flip them to see the sign, the panel and the offline labels.
- Save the theme. The saved CSS is exactly what the player injects.
- 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.
| Transport | cs-connected / cs-no-connection | The hub link is up. |
|---|---|---|
| Producer | cs-tray-online / cs-no-tray | At least one Call State app is online for the account. |
| Meeting | cs-in-meeting / cs-idle | A meeting is running. |
| Mic | cs-mic-on / cs-mic-off | The mic is open. |
| Camera | cs-cam-on / cs-cam-off | The camera is on. |
| Ringing | cs-ringing / cs-not-ringing | A call rings before you join it. |
| Hand | cs-hand-raised / cs-hand-lowered | Your hand is up. |
| Share | cs-sharing / cs-not-sharing | Your screen is shared. |
| Recording | cs-recording / cs-not-recording | The meeting is recorded. |
| Chat pane | cs-chat-open / cs-chat-closed | The 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.
| 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.
cs-shell | The single content wrapper inside the root. |
|---|---|
cs-sign | The bordered sign. Visible while the transport and a producer are online. |
cs-on-air | The “On Air” line. |
cs-toggles | The row that holds the two lamps. |
cs-toggle | One lamp. cs-toggle-mic and cs-toggle-cam say which. |
cs-toggle-name | The lamp label (“Mic” or “Cam”). |
cs-toggle-state | One pole of the lamp: cs-toggle-state-on or cs-toggle-state-off. |
cs-offline | The full-screen label while the display is offline: cs-offline-connection or cs-offline-tray. |
cs-panel | The whole status panel. Hidden by the built-in skin; a theme shows it. |
|---|---|
cs-bar | The title bar, with cs-bar-dots, cs-bar-title and cs-bar-tag. |
cs-status | The block that holds source, headline, timer and description. |
cs-lamp | The status lamp. cs-lamp-lens is an empty inner element for a lens or a glow. |
cs-source | The source line: cs-source-mark, cs-source-name and cs-source-device (the device name, empty when unknown). |
cs-state | The headline. One cs-state-text per state: cs-state-available, cs-state-ringing, cs-state-call, cs-state-presenting. |
cs-timer | The elapsed call time, ticking in the player. Empty outside a meeting. |
cs-description | One cs-description-text per state, and cs-description-part pieces for mic, camera and the tail. |
cs-signals | Two groups, cs-signal-group-you and cs-signal-group-meeting, each with a cs-signal-group-label and a cs-signal-list. |
cs-signal | One tile: cs-signal-mic, cs-signal-cam, cs-signal-hand, cs-signal-share, cs-signal-rec, cs-signal-chat. |
cs-signal-top | The top row of a tile, with cs-signal-icon, cs-signal-name and cs-signal-led. |
cs-signal-value | One 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
displayoncs-state-text,cs-description-text,cs-description-partorcs-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
displayoncs-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
:emptywhen 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-connectionandcs-offline-traya 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.