SWIVEL — DESIGN SYSTEM

Component
Library

The official design system for Swivel's Virtual Automation Platform. Built on brand tokens: Switch, Graphite, Slate, and White.

01

Components

Buttons · fields · chips · badges · navigation · overlays

actions / Button
Variants
Sizes
With icon
info
Icon buttons must always have a 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.
Icon button
States

actions / SplitButton
call_split
A split button pairs a primary action with a dropdown of related options. The left segment fires the default action immediately; clicking the 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.
Text
Icon + text
Compact (icon only)

Space-premium contexts only. Primary action still needs a data-tooltip.

Ghost / text

No fill or stroke. Dropdown arrow is hidden at rest — hover the button to reveal it.

overlays / Tooltip
chat_bubble_outline
Tooltips use UI/Small (12px Avantt SemiBold) and a fill that contrasts the page background. Five placements: no tail (default), top (tail↓), bottom (tail↑), right (tail←), left (tail→). Wire via 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.
No tail
text value
Top (tail↓)
text value
Bottom (tail↑)
text value
Right (tail←)
text value
Left (tail→)
text value
Live (hover)

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-label on icon-only controls so the tooltip copy doubles as the accessible name.

filters / ButtonGroup
filter_list
Use button groups for filtering and segmented selection — never for taking actions. A button group is a single-select control: it switches a view, scopes a list, or picks one mutually-exclusive option (a date range, a status filter, a chart type). It always has exactly one selected segment. If a control does something — saves, runs, deletes, opens a dialog — it is an action and belongs in a Button, IconButton, or CTA, not here.
Text
Icon only
Icon + text
Select one
Disabled
Select one

fields / TextField
Default
Used for reporting and rule scoping.
2px --switch-contrast stroke + 3px focus halo.
Endpoint hostname.
Value contains invalid characters.
Halo + 2px stroke in --error.
Inherited from parent rule.
Adornments
$
Inset label
search
search close
search
search
search

fields / SelectField
Empty
arrow_drop_down
arrow_drop_down
arrow_drop_down
Placeholder
arrow_drop_down
arrow_drop_down
arrow_drop_down
Disabled
arrow_drop_down
arrow_drop_down
arrow_drop_down
Selected
arrow_drop_down
arrow_drop_down
arrow_drop_down
Description
arrow_drop_down
Select a good one
arrow_drop_down
Select a good one
arrow_drop_down
Select a good one
Error
arrow_drop_down
You didn't pick one
arrow_drop_down
You didn't pick one
arrow_drop_down
You didn't pick one

fields / SelectMenu

Opens beneath a select field on click (when not disabled). Rows are 34 px tall, fill on --surface-base, hover to --graphite-10, and lock to --slate-bg when selected. Optional bits: a search row at the top, an M3-style checkbox for multi-select, a leading platform glyph, a section header, and a + value creator pinned at the bottom.

No search
United States
United Kingdom
Canada
Germany
Japan
addAdd country
Magnite
FreeWheel
SpringServe
Publica
PPubMatic
EEquativ
North America
United States
Canada
Mexico
Europe
United Kingdom
Germany
addAdd country
checkSelect All
checkMagnite only
checkFreeWheel only
checkSpringServe only
checkPublica only
checkPubMatic only
addAdd partner
Search
United States
United Kingdom
Canada
Germany
Japan
addAdd country
Magnite
FreeWheel
SpringServe
Publica
PPubMatic
EEquativ
North America
United States
Canada
Europe
United Kingdom
Germany
addAdd country
checkSelect All
checkMagnite only
checkFreeWheel only
checkSpringServe only
checkPublica only
checkPubMatic only
addAdd partner
Anatomy
STATES
Default — surface/base
Hover — graphite-10
Selected — slate/bg
Disabled
Group header
Leading platform icon
checkCheckbox — checked
checkCheckbox — unchecked only
checkCheckbox — indeterminate
addCreate new option

fields / TagInput
Default
chip 1 chip 2 chip 3 close
content_copy chip 1 chip 2 chip 3 close
content_copy chip 1 chip 2 chip 3
chip 1 chip 2 chip 3
close
content_copy
chip 1 chip 2 chip 3
close
content_copy
chip 1 chip 2 chip 3
Error
chip 1 chip 2 chip 3 close
content_copy chip 1 chip 2 chip 3 close
content_copy chip 1 chip 2 chip 3
chip 1 chip 2 chip 3
close
content_copy
chip 1 chip 2 chip 3
close
content_copy
chip 1 chip 2 chip 3
Disabled
content_copy chip 1 chip 2 chip 3
content_copy
chip 1 chip 2 chip 3

Fields · usage

forms · labels · sizing · grouping

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

forms / labels

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.

ABOVE · DEFINE OBJECT PROPERTIES
Used for reporting and rule scoping.
  • Create / edit forms that define what an object is.
  • Settings and configuration where each field stores a value.
INSET · AFFECT PAGE DISPLAY
search
  • Controls that filter, sort, or scope what's on screen.
  • Toolbars, filter rows, and table headers.
NO LABEL · PLACEHOLDER NAMES THE FIELD
arrow_drop_down
  • 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

forms / required

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

forms / help

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.

Resets at 00:00 UTC. Cap inherits if blank.

Help text explains constraints, units, or defaults. Keep it under 12 words — and skip it entirely when the label and placeholder are enough.

Minimum is $50.

On error, the help line is replaced with the error message in --error. State the fix, not the symptom.

search

Inset fields skip help and error text. Validate inline elsewhere — usually a banner or row state.

Sizing & density

forms / sizing
  • 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

forms / layout
SINGLE COLUMN · STACKED
TWO-UP · RELATED PAIRS
TWO-COLUMN PAGE · FIELDS FILL THEIR SECTION
Basic info
keyboard_arrow_down
Scheduling
keyboard_arrow_down
Frequency
FULL-WIDTH SEARCH · TOOLBAR ROW
search
  • 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 1fr and 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.

data-display / Chip
Filter
All Live Paused Drafts
Tags
CTV Display Audio Switch on
ID chip
BYR-4471
Small · 20 Default · 24 Large · 32
With icon
circleLive boltAuto-pause filter_altCTV only checkSelected
Dismissable
Small Default Large Outlined circleLive Square
Square
v2.4.1 PROD tagLabel Selected
Stacked
boltAuto tuneFilters timelineTrends videocam#1234

data-display / Badge
Semantic
Switches
Auto-pause
Notifications
Beta features

data-display / PlatformIcon
hub
Third-party platform logos. Never recolor, restyle, or substitute. Each mark keeps its own brand colors and sits on a neutral white plate at a fixed box size in both light and dark mode — the plate is the only thing that adapts. Reference assets by their platform_name from assets/partners/; don't redraw or approximate a logo in code.
Symbols
Google Ad Manager
Google Ad Manager
GAM
Campaign Manager 360
Campaign Mgr 360
DCM
Innovid
Innovid
innovid
Operative
Operative
operative
SpringServe
SpringServe
springserve
Magnite
Magnite
magnite
FreeWheel
FreeWheel
freewheel
Slack
Slack
slack
Airtable
Airtable
airtable
Salesforce
Salesforce
salesforce
Publica
Publica
publica

beacon / ReadyState
gif_box
Beacon's AI ready-state indicator. Appears as the last message in the Beacon chat thread once the agent has finished responding and is ready for the next prompt. Always shown at 48px alongside the text "Ready to keep swiveling…" — never larger, never standalone without the label text, never in a bubble container.
In context
inline · 32px
reaction · 48px
thread · 80px
emphasis · 128px
make me a canvas pie chart with top 5 demand partners over the last week
@Datalake Explorer

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.

Ready to keep swiveling…

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) and emphasis (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.

feedback / Alert
FILLED
info
Sync in progressPulling 14 days of delivery from Magnite.
check_circle
Rule deployedauto-pause-low-fill is live across 6 endpoints.
warning
Approaching QPS limitYou're at 84% of your monthly quota.
error
Authentication failedFreeWheel returned 401. Re-authorize from Integrations.
OUTLINED
info
Sync in progressPulling 14 days of delivery from Magnite.
check_circle
Rule deployedauto-pause-low-fill is live across 6 endpoints.
warning
Approaching QPS limitYou're at 84% of your monthly quota.
error
Authentication failedFreeWheel returned 401. Re-authorize from Integrations.

navigation / TopHeader
Dark
Swivel
light_mode dark_mode
mood user_name
Light
Swivel
light_mode dark_mode
mood user_name

navigation / Sidenav + Tabs + Breadcrumb
Breadcrumb — above the page title, never as it
auto-pause-low-fill
Q3 Upfront
A collection has no object, so no ID chip.

layout / Card

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.

overlays / Modal

overlays / Drawer
side_navigation
A slide-in panel anchored to the viewport edge. Use for settings, detail views, and filter panels that need more space than a modal but shouldn’t navigate away. Always includes a close button in the header. Clicking the overlay backdrop closes the drawer. Right placement is the default; left is available for secondary panels.
Anatomy
Panel title
Filters

Filter content

Interactive

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.

data / Table · elevation + outline · empty state
Elevation · with content
IDTypeName Metric ChildrenNameCreated at LabelsState
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
Rows per page:25 keyboard_arrow_down
1–25 of 311
Elevation · first-run empty
IDTypeName Metric ChildrenNameCreated at LabelsState
inbox

No objects yet

Objects will appear here once they're created.

Outline · with content
IDTypeName Metric ChildrenNameCreated at LabelsState
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
Rows per page:25 keyboard_arrow_down
1–25 of 311
Outline · filtered to zero
IDTypeName Metric ChildrenNameCreated at LabelsState
filter_list

No matches

Nothing in this view matches your filters. Loosen them or clear the search to see everything.

Toolbar
Pagination
Rows per page: 25 keyboard_arrow_down
1–25 of 311

Count expander.

data / Table · expandable_rows
unfold_more
A count of child objects is the way into them. Click the square .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 IDCampaignProductDemand tagsCreativesStart
1284511Q4 Holiday Homescreen TakeoverHomescreenNov 1, 2026
1284530Black Friday CTV BurstCTVNov 24, 2026
1283977Fall Streaming LaunchCTVOct 5, 2026

feedback / Skeleton
Table loading
Card loading
Usage
  • 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.

feedback / EmptyState
First-run
auto_mode

No automations yet

Automations run on a schedule and act on bidstream traffic.

Filtered to zero
filter_list

No matches

Nothing in this view matches your filters. Loosen them or clear the search to see everything.

Usage
  • Object icon in slate-bg chip — 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.

feedback / ErrorState
Failed fetch
cloud_off

Couldn't load automations

The server didn't respond. Check your connection and try again — nothing has changed.

request_id: 8f3c4a2b · 503 upstream_timeout
Permission denied
lock

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.

Usage
  • Mark uses the matching semantic chip — error-bg for failures, warning-bg for 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.
// IconButton tooltip lint — warn in console for any .btn-icon missing data-tooltip. // The CSS already applies a dashed red outline; this adds a console message with // the element so developers can find it in the DOM tree quickly. document.addEventListener('DOMContentLoaded', function() { var missing = document.querySelectorAll('.btn-icon:not([data-tooltip])'); if (missing.length) { console.warn('[Swivel] ' + missing.length + ' IconButton(s) are missing data-tooltip — every icon button requires a tooltip label for accessibility.', [...missing]); } });