Theming
The widget uses CSS custom properties (variables) for all visual styling. It provides full style isolation - your page styles won't leak in, and the widget styles won't leak out.
Interactive Playground
Preview all theme customizations in real time using the playground. Adjust colors, fonts, spacing, and border radius, then copy the generated code.
Share Config copies a link that reproduces your exact setup, so you can send a look to a colleague for review. Access tokens you enter yourself are never included in the link.
How It Works
The theming system has three layers, applied in order of priority:
- CSS Defaults - Base values defined in the widget's
:hoststyles - Backend Theme API - Runtime overrides fetched from the SayTV API
- Consumer Overrides - Inline
styleattribute on the<saytv-chat>element (highest priority)
Dark Mode
Set the theme attribute to switch between light and dark:
<saytv-chat app-id="your-app-id" theme="dark"></saytv-chat>The dark theme automatically overrides backgrounds, text colors, borders, and shadows.
Custom Colors
Override any CSS variable via inline styles:
<saytv-chat
app-id="your-app-id"
theme="light"
style="
--stv-color-primary: #4f46e5;
--stv-color-accent: #10b981;
--stv-bg-primary: #fafafa;
--stv-text-primary: #111827;
"
></saytv-chat>CSS Custom Properties Reference
Header & Footer
Style the chat header and the bottom tab-bar independently of your brand color. Each variable defaults to the current brand-derived look, so nothing changes until you set one.
| Property | Default | Description |
|---|---|---|
--stv-header-bg | primary gradient | Header background — a solid color or your own gradient |
--stv-header-text | --stv-text-light | Header title text |
--stv-header-icon | --stv-header-text | Header icons (back, search, etc.) |
--stv-footer-bg | --stv-bg-primary | Tab-bar background |
--stv-footer-border | --stv-border-light | Tab-bar top border |
--stv-footer-text | --stv-text-muted | Inactive tab label |
--stv-footer-text-active | --stv-color-primary | Active tab label |
--stv-footer-icon | --stv-footer-text | Inactive tab icon |
--stv-footer-icon-active | --stv-footer-text-active | Active tab icon |
--stv-footer-indicator | --stv-color-primary | Active-tab indicator pill |
<saytv-chat
app-id="your-app-id"
style="
--stv-header-bg: #0b1020;
--stv-header-text: #ffffff;
--stv-footer-bg: #0b1020;
--stv-footer-text: #8a8f9c;
--stv-footer-text-active: #4da3ff;
--stv-footer-indicator: #4da3ff;
"
></saytv-chat>Floating Navigation
When you set hide-header, a floating back button takes over so users can still leave a hashtag room or a profile. It sits directly on top of the chat, so it has its own surface colors rather than the header's.
| Property | Default | Description |
|---|---|---|
--stv-floating-nav-bg | --stv-bg-primary | Button background |
--stv-floating-nav-icon | --stv-text-primary | Back arrow color |
--stv-floating-nav-shadow | --stv-shadow-md | Drop shadow that lifts it off the content |
--stv-floating-nav-size | 36px | Button diameter |
--stv-floating-nav-offset | 10px | Distance from the top and side edges |
<saytv-chat
app-id="your-app-id"
hide-header="true"
style="
--stv-floating-nav-bg: rgba(0, 0, 0, 0.6);
--stv-floating-nav-icon: #ffffff;
--stv-floating-nav-size: 40px;
"
></saytv-chat>Quiz & Poll Panels
The panel that drops in over the message list when a quiz or poll starts. Both follow your brand primary color out of the box, so setting --stv-color-primary is usually enough.
Check the text color if your brand color is light. The panels put text directly on the brand color and default to white, which needs a dark background to stay readable. WCAG AA wants a contrast ratio of at least 4.5:1 — white on a light blue such as #2faee0 is only 2.5:1. Set --stv-quiz-text / --stv-poll-text to a dark color in that case. The Playground's Style → Quiz & Poll section previews both panels and reports the ratio as you pick colors.
| Property | Default | Description |
|---|---|---|
--stv-quiz-bg | primary gradient | Quiz panel background — a solid color or your own gradient |
--stv-quiz-text | --stv-text-light | Question, options and header text |
--stv-quiz-option-bg | rgba(255,255,255,0.08) | Answer chip background |
--stv-quiz-option-bg-selected | rgba(255,255,255,0.2) | Answer chip background once picked |
--stv-quiz-option-border | rgba(255,255,255,0.3) | Answer chip border |
--stv-quiz-option-border-selected | --stv-quiz-text | Answer chip border once picked |
--stv-quiz-option-bar | rgba(255,255,255,0.15) | Result bar fill behind each chip |
--stv-quiz-icon-bg | rgba(255,255,255,0.15) | Close-button circle |
--stv-quiz-submit-bg | rgba(255,255,255,0.95) | Submit-answer button |
--stv-quiz-submit-text | --stv-color-primary-dark | Submit-answer label |
--stv-poll-bg | --stv-quiz-bg | Poll panel background |
--stv-poll-text | --stv-quiz-text | Poll question and option text |
--stv-poll-option-bg | --stv-quiz-option-bg | Poll option background |
--stv-poll-option-border | --stv-quiz-option-border | Poll option border |
--stv-poll-option-bar | --stv-quiz-option-bar | Poll result bar fill |
--stv-poll-icon-bg | --stv-quiz-icon-bg | Poll close-button circle |
Polls inherit every quiz value, so one override covers both. Set the --stv-poll-* variables only to make a poll deliberately different from a quiz.
<saytv-chat
app-id="your-app-id"
style="
--stv-color-primary: #2faee0;
--stv-quiz-text: #0b2b3a;
--stv-quiz-option-bg: rgba(0, 0, 0, 0.06);
--stv-quiz-option-border: rgba(0, 0, 0, 0.15);
--stv-quiz-submit-bg: #0b2b3a;
--stv-quiz-submit-text: #ffffff;
"
></saytv-chat>Brand Colors
| Property | Light Default | Description |
|---|---|---|
--stv-color-primary | #7e171c | Primary brand color |
--stv-color-primary-dark | #5d1014 | Darker primary variant |
--stv-color-accent | #0143d9 | Accent / interactive color |
--stv-color-accent-yellow | #f8dc22 | Secondary accent |
--stv-color-danger | #e03333 | Error / destructive actions |
--stv-color-success | #1fa84a | Success indicators |
--stv-color-info | #3b82f6 | Informational elements |
--stv-color-warning | #f59e0b | Warning indicators |
Backgrounds
| Property | Light | Dark | Description |
|---|---|---|---|
--stv-bg-primary | #fafaf8 | #0e0e12 | Main background |
--stv-bg-secondary | #f2f0ec | #161619 | Secondary surfaces |
--stv-bg-light | #f5f3ef | #1c1c22 | Elevated surfaces |
--stv-bg-hover | #efede8 | #212128 | Hover state |
--stv-bg-input | #ffffff | #1a1a20 | Input fields |
--stv-bg-disabled | #eae8e3 | #252530 | Disabled elements |
--stv-bg-overlay | rgba(0,0,0,0.5) | - | Modal overlays |
Text
| Property | Light | Dark | Description |
|---|---|---|---|
--stv-text-primary | #0d0d0f | #f0eff5 | Primary text |
--stv-text-secondary | #4a4852 | #b0aebe | Secondary text |
--stv-text-muted | #8b8a96 | #706e80 | Muted / hint text |
--stv-text-light | #ffffff | - | Text on dark backgrounds |
--stv-text-hashtag | #1d4ed8 | - | Hashtag text |
--stv-text-link | #0143d9 | - | Link text |
--stv-text-danger | #e03333 | - | Error text |
Borders
| Property | Light | Dark | Description |
|---|---|---|---|
--stv-border-color | #e5e2db | #2a2932 | Default borders |
--stv-border-light | #edebe5 | #252430 | Subtle borders |
--stv-border-focus | #0143d9 | - | Focus ring color |
--stv-border-error | #e03333 | - | Validation error |
Border Radius
| Property | Default | Description |
|---|---|---|
--stv-border-radius-xs | 0.25rem | Extra small (4px) |
--stv-border-radius-sm | 0.375rem | Small (6px) |
--stv-border-radius-md | 0.625rem | Medium (10px) |
--stv-border-radius-lg | 1rem | Large (16px) |
--stv-border-radius-xl | 1.5rem | Extra large (24px) |
--stv-border-radius-full | 50% | Circle |
Typography
| Property | Default | Description |
|---|---|---|
--stv-font-primary | System sans-serif stack | Body font family |
--stv-font-heading | System rounded stack | Heading font family |
--stv-font-mono | "SF Mono", "Fira Code", monospace | Monospace font |
--stv-font-size-xs | 0.6875rem | 11px |
--stv-font-size-sm | 0.8125rem | 13px |
--stv-font-size-base | 0.875rem | 14px |
--stv-font-size-md | 1rem | 16px |
--stv-font-size-lg | 1.25rem | 20px |
--stv-font-size-xl | 1.5rem | 24px |
--stv-font-weight-normal | 400 | Normal weight |
--stv-font-weight-medium | 500 | Medium weight |
--stv-font-weight-semibold | 600 | Semibold weight |
--stv-font-weight-bold | 700 | Bold weight |
--stv-line-height-tight | 1.25 | Compact line height |
--stv-line-height-normal | 1.5 | Default line height |
Custom Fonts
To use custom fonts, override the font family variables:
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<saytv-chat
app-id="your-app-id"
style="--stv-font-primary: 'Inter', sans-serif;"
></saytv-chat>The font @import or <link> must be on the host page since the Shadow DOM can inherit font-face declarations from the parent document.
Spacing
| Property | Default | Description |
|---|---|---|
--stv-spacing-xs | 0.25rem | 4px |
--stv-spacing-sm | 0.5rem | 8px |
--stv-spacing-md | 1rem | 16px |
--stv-spacing-lg | 1.5rem | 24px |
--stv-spacing-xl | 2rem | 32px |
--stv-spacing-2xl | 3rem | 48px |
Shadows
| Property | Light | Dark |
|---|---|---|
--stv-shadow-sm | 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04) | Stronger |
--stv-shadow-md | 0 4px 12px rgba(0,0,0,0.08), 0 2px 4px rgba(0,0,0,0.05) | Stronger |
--stv-shadow-lg | 0 8px 24px rgba(0,0,0,0.1), 0 4px 8px rgba(0,0,0,0.06) | Stronger |
--stv-shadow-xl | 0 16px 48px rgba(0,0,0,0.14), 0 8px 16px rgba(0,0,0,0.08) | Stronger |
Transitions
| Property | Default | Description |
|---|---|---|
--stv-transition-fast | 150ms ease | Hover, focus states |
--stv-transition-normal | 250ms ease | General animations |
--stv-transition-slow | 400ms ease | Page transitions |
Extended Tokens
| Property | Default | Description |
|---|---|---|
--stv-button-border-radius | var(--stv-border-radius-md) | Button corners |
--stv-input-border-radius | var(--stv-border-radius-lg) | Input field corners |
--stv-footer-label-size | 0.6875rem | Footer nav label size |
--stv-skeleton-duration | 1.8s | Loading skeleton animation |
Message Appearance
Every message row can be styled with a background, a hover background, rounded corners, and a horizontal inset. A corner radius reads best paired with a background (or border) and a little side margin, which turns each row into a "card".
| Property | Default | Description |
|---|---|---|
--stv-message-bg | transparent | Message row background |
--stv-message-bg-hover | var(--stv-bg-hover) | Message row background on hover |
--stv-message-border-radius | 0 | Message row corner radius |
--stv-message-inline-margin | 0 | Left/right inset of each message row |
<!-- Card-style messages -->
<saytv-chat
app-id="your-app-id"
style="
--stv-message-bg: #f4f4f6;
--stv-message-bg-hover: #ececf0;
--stv-message-border-radius: 12px;
--stv-message-inline-margin: 8px;
"
></saytv-chat>Moderator Messages
Moderator messages support the same appearance controls as regular messages, plus a customizable border. Each border side is set independently and accepts a full CSS border value (<width> <style> <color>); use the --stv-mod-border shorthand to set every side at once. Every appearance variable defaults to its --stv-message-* counterpart, so moderators inherit the base message look unless you override them here.
| Property | Default | Description |
|---|---|---|
--stv-mod-border | none | Border shorthand applied to all four sides unless a side is overridden |
--stv-mod-border-top | var(--stv-mod-border) | Top border of moderator messages |
--stv-mod-border-right | var(--stv-mod-border) | Right border of moderator messages |
--stv-mod-border-bottom | var(--stv-mod-border) | Bottom border of moderator messages |
--stv-mod-border-left | var(--stv-mod-border) | Left border of moderator messages |
--stv-mod-bg | var(--stv-message-bg) | Background of moderator messages |
--stv-mod-bg-hover | var(--stv-message-bg-hover) | Background of moderator messages on hover |
--stv-mod-border-radius | var(--stv-message-border-radius) | Corner radius of moderator messages |
--stv-mod-inline-margin | var(--stv-message-inline-margin) | Left/right inset of moderator messages |
<!-- Accent bar on the left edge only -->
<saytv-chat
app-id="your-app-id"
style="--stv-mod-border-left: 3px solid #f8dc22;"
></saytv-chat>
<!-- Full card with a subtle tint -->
<saytv-chat
app-id="your-app-id"
style="
--stv-mod-border: 2px solid #f8dc22;
--stv-mod-bg: rgba(248, 220, 34, 0.08);
--stv-mod-border-radius: 12px;
--stv-mod-inline-margin: 8px;
"
></saytv-chat>Examples
Brand Customization
<saytv-chat
app-id="your-app-id"
style="
--stv-color-primary: #7c3aed;
--stv-color-primary-dark: #5b21b6;
--stv-color-accent: #06b6d4;
--stv-border-radius-md: 12px;
--stv-border-radius-lg: 16px;
"
></saytv-chat>Rounded Theme
<saytv-chat
app-id="your-app-id"
style="
--stv-border-radius-xs: 0.5rem;
--stv-border-radius-sm: 0.75rem;
--stv-border-radius-md: 1rem;
--stv-border-radius-lg: 1.5rem;
--stv-border-radius-xl: 2rem;
--stv-button-border-radius: 9999px;
--stv-input-border-radius: 9999px;
"
></saytv-chat>Compact Spacing
<saytv-chat
app-id="your-app-id"
style="
--stv-spacing-xs: 0.125rem;
--stv-spacing-sm: 0.25rem;
--stv-spacing-md: 0.5rem;
--stv-spacing-lg: 0.75rem;
--stv-spacing-xl: 1rem;
"
></saytv-chat>