Component
Library
The official design system for Swivel's Virtual Automation Platform. Built on brand tokens: Switch, Graphite, Slate, and White.
Components
data-tooltip that describes what clicking the button does. Use a verb phrase — Add rule, Delete campaign, Export CSV — not the icon name or a noun. Without a visible label, the tooltip is the only affordance that communicates the action. Omitting it is an accessibility failure. A dashed red outline appears in dev/design tools when data-tooltip is missing.arrow_drop_down trigger opens the menu. Use when 2–4 closely related actions share a surface and one of them is the clear default. For unrelated actions, use separate IconButtons instead.Space-premium contexts only. Primary action still needs a data-tooltip.
No fill or stroke. Dropdown arrow is hidden at rest — hover the button to reveal it.
data-tooltip for pure-CSS auto-tooltips, or use .tooltip + placement class for JS-positioned overlays. Always pair with aria-label or aria-describedby for screen readers.Usage rules
- Verb-phrase copy — describe what the action does, not what the element is. Delete campaign, not Trash or Delete.
- No tail = no directional context — use the tailless variant for tooltips that appear centered above a group or for text truncation labels.
- Tail direction = placement — top tooltip → tail points down (
.tooltip-top); bottom tooltip → tail points up (.tooltip-bottom), etc. - Always add
aria-labelon icon-only controls so the tooltip copy doubles as the accessible name.
Button, IconButton, or CTA, not here.--switch-contrast stroke + 3px focus halo.--error.Fields · usage
Guidance for using the text, select, and tag-input fields above — label placement, required vs. optional, help & error copy, sizing, and how to group fields into forms.
Label placement
Three patterns are allowed, no others. Above when the field defines object properties; inset when the field affects page display; no label when the field is part of a labeled row or toolbar group — in which case it must use placeholder text that names what's being entered. Pick by context, not by taste. Exception: inside a Modal, always use inset labels — even for a field that defines an object property. A modal is already a compact, bounded surface, and inset labels keep its form dense; reserve the above/stacked pattern for full page forms.
- Create / edit forms that define what an object is.
- Settings and configuration where each field stores a value.
- Controls that filter, sort, or scope what's on screen.
- Toolbars, filter rows, and table headers.
- Only when the field's purpose is named elsewhere — a row header, a section title, or an adjacent label.
- Placeholder text must name what's being entered ("Choose an option", "$0.00"). Never use placeholder for examples or help.
Required & optional
Mark required fields with a red asterisk (<span class="req">*</span>) directly after the label text. Required is the default assumption — only mark it when the field is genuinely blocking.
In forms where most fields are optional, flip the polarity: leave required unmarked and append (optional) in graphite-bg to the ones that aren't. Pick one convention per screen, never both.
Help text & errors
Use help text sparingly. A well-labelled field with a sensible placeholder usually doesn't need it. Reserve it for the few cases where the user can't infer the constraint, unit, or default from context.
Help text explains constraints, units, or defaults. Keep it under 12 words — and skip it entirely when the label and placeholder are enough.
On error, the help line is replaced with the error message in --error. State the fix, not the symptom.
Inset fields skip help and error text. Validate inline elsewhere — usually a banner or row state.
Sizing & density
- Single-line height is always 34px. Don't introduce a second size for visual emphasis — control hierarchy with grouping, not field height.
- Multi-line uses a
<textarea>with vertical resize. Start at 3 rows. Cap at 8 before scrolling internally. - Min field width is 240px. Below that, fields look broken and labels truncate.
Grouping & layout
- Stack a single column for forms shorter than ~8 fields. It reads top-to-bottom and never wraps.
- Pair fields side-by-side only when they're semantically paired — start/end, min/max, daily/total. Two unrelated fields in a row read as a layout accident.
- Two-column page layouts: fields fill the width of their section. The column gives the grouping; fields don't need a second margin inside it.
- In toolbar rows, search takes
1frand other fields use fixed widths. Never let a filter compress search below 240px. - Section forms with
pp-sec-head+ rule. A horizontal divider every 4–6 fields keeps long forms scannable.
platform_name from assets/partners/; don't redraw or approximate a logo in code.
GAM

DCM

innovid

operative

springserve

magnite

freewheel

slack

airtable
salesforce

publica
Magnite dominated demand revenue at ~$1.58M over the last 7 days — roughly 4× the next four partners combined. Magnite is the clear dominant partner at ~80% of the top-5 total.
Usage rules
- Always paired with text — render alongside "Ready to keep swiveling…" never without it.
- Four fixed sizes, no in-between —
inline(32px) beside a line of running text;reaction(48px) for a chat-thread message;thread(80px) andemphasis(128px) for when the sticker is the message itself. - No container — transparent background renders directly on the chat surface. No bubble, plate, or drop shadow.
- Last message only — replaces the typing indicator once the agent response is complete. Remove when the user sends the next prompt.
- Reduced motion: if
prefers-reduced-motion: reduce, swap the GIF for a static first-frame image. - Source:
assets/brand/keep_swiveling.gif· 128×128 native · 30 frames.
Active rules
14 rules running across 6 SSP integrations. Last fired 38 seconds ago.
Switch is on
Virtual automation actively shaping bidstream traffic across 12 endpoints.
Pause auto-pause-low-fill?
This rule has fired 142 times in the last 24 hours, preventing $2,140 in unprofitable spend. Pausing will affect 6 SSP endpoints.
Usage rules
- Not a nav replacement — drawers hold ephemeral UI (settings, filters, detail). Persistent navigation belongs in the sidebar.
- Focus trap — when open, trap focus inside the drawer. Return focus to the trigger on close.
- Backdrop closes — clicking outside the panel always dismisses. Unsaved-state warning may intercept this for destructive cases.
- Size guidance —
drawer-sm(360px) for filter panels; default (480px) for forms;drawer-lg(640px) for rich detail views.
| ID | Type | Name | Metric | Children | Name | Created at | Labels | State |
|---|---|---|---|---|---|---|---|---|
| 314273 | rocket_launch | object_name | 166,483.50 | # | personuser_name | 2025/05/06 19:27 | chip 1chip 2chip 3 | task_alt |
| 594188 | radar | object_name | 472,332.58 | # | personuser_name | 2025/03/14 15:06 | chip 1chip 2chip 3 | remove_circle_outline |
| 903867 | rule | object_name | 193,397.18 | # | personuser_name | 2025/01/29 23:13 | chip 1chip 2chip 3 | task_alt |
| 163294 | smart_toy | object_name | 74,868.25 | # | personuser_name | 2025/12/03 16:00 | chip 1chip 2chip 3 | remove_circle_outline |
| 748261 | equalizer | object_name | 263,084.83 | # | personuser_name | 2025/12/22 01:57 | chip 1chip 2chip 3 | task_alt |
| ID | Type | Name | Metric | Children | Name | Created at | Labels | State |
|---|
No objects yet
Objects will appear here once they're created.
| ID | Type | Name | Metric | Children | Name | Created at | Labels | State |
|---|---|---|---|---|---|---|---|---|
| 525632 | gavel | object_name | 897,351.53 | # | personuser_name | 2025/04/30 09:54 | chip 1chip 2chip 3 | remove_circle_outline |
| 558495 | campaign | object_name | 598,898.46 | # | personuser_name | 2025/01/30 13:10 | chip 1chip 2chip 3 | task_alt |
| 381842 | leaderboard | object_name | 29,889.33 | # | personuser_name | 2025/11/08 17:28 | chip 1chip 2chip 3 | remove_circle_outline |
| ID | Type | Name | Metric | Children | Name | Created at | Labels | State |
|---|
No matches
Nothing in this view matches your filters. Loosen them or clear the search to see everything.
Count expander.
.count-btn and the records behind it open as a sub-table directly beneath the row; click it again to close. One level, one sub-table per row (a second count in the same row swaps it), at most 10 rows with a "View all" link. A zero count is .is-zero and opens nothing. Never a chevron column, a row click, a Drawer, or a Modal. The full contract is Table.expandable_rows in components.json.| Platform ID | Campaign | Product | Demand tags | Creatives | Start | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1284511 | Q4 Holiday Homescreen Takeover | Homescreen | Nov 1, 2026 | ||||||||||||||
| |||||||||||||||||
| 1284530 | Black Friday CTV Burst | CTV | Nov 24, 2026 | ||||||||||||||
| 1283977 | Fall Streaming Launch | CTV | Oct 5, 2026 | ||||||||||||||
- Use for known-shape loads — tables, cards, profile rows. Mirror the real layout, not a generic spinner.
- Skeletons replace content for the duration of the fetch, then swap in place. No fade. No cross-fade.
- If the load is under 200ms, skip the skeleton — flash is worse than a brief blank.
- If the load exceeds 4s, switch to an indeterminate progress bar at the top of the surface and keep the skeleton beneath.
No automations yet
Automations run on a schedule and act on bidstream traffic.
No matches
Nothing in this view matches your filters. Loosen them or clear the search to see everything.
- Object icon in
slate-bgchip — never decorative art, never blank space. - Title states the absence in 3–5 words. Body explains what belongs here.
- Primary action is reserved for cases where the user can fix the absence in place — filtered-to-zero gets a clearing action. First-run states get no CTA; creation lives in the page-level header or empty side nav, never in the body.
- Never use the word "oops", "sorry", or an emoji. Empty isn't an error.
Couldn't load automations
The server didn't respond. Check your connection and try again — nothing has changed.
You don't have access to this view
Automations are managed by your org admin. Ask them for the Operator role, or switch to a workspace you can edit.
- Mark uses the matching semantic chip —
error-bgfor failures,warning-bgfor permission or quota. - Title names what failed in plain language, not the HTTP status.
- Body tells the user three things: what happened, whether data is safe, and what to try.
- Primary action retries when retryable. Always include a secondary path — status page, support, switching context.
- Diagnostic line at the bottom is for engineers. Use
request_id+ short cause, never a stack trace.
Ready to keep swiveling…