Tela Design Language Visual Reference

This is the canonical visual reference for every reusable primitive in Tela Design Language (TDL). The definitive reference may be found at https://github.com/paulmooreparks/tela/blob/main/TELA-DESIGN-LANGUAGE.md. Toggle the theme in the topbar to preview each component in light and dark modes. Every class in this document is part of the design language; every design language primitive appears here. If you need a pattern that is not shown, open a discussion before inventing a new class.

1Design tokens

All colors, spacing, and typography are defined as CSS custom properties on :root. The theme (light or dark) is applied by setting data-theme on <html>. Accent, warn, and danger colors are theme-invariant; backgrounds, text, and borders flip.

Palette

accent
#2ecc71
accent-hover
#27ae60
warn
#f39c12
danger
#e74c3c
danger-hover
#c0392b

Light theme surfaces

bg
#f5f6f8
surface
#ffffff
surface-alt
#f0f1f4
text
#1a1a2e
text-muted
#6b7280
border
#e2e5ea

Dark theme surfaces

bg
#111827
surface
#1f2937
surface-alt
#1a2332
text
#e5e7eb
text-muted
#9ca3af
border
#4b5668

2Typography

One family (system UI stack) for body text, one family (monospace) for code, version strings, and terminal output. Font weights are restricted to 400, 500, 600, and 700. Never use italic or underline for emphasis in UI chrome.

Page title
Page title
Top-of-page header. One per page.
  • Size: 28px
  • Weight: 700
  • Letter-spacing: -0.01em
Section header
Section header
Major sections within a page. Typically paired with a 2px bottom border.
  • Size: 20px
  • Weight: 700
Card / subsection title
Card title
Used for card headers and modal titles.
  • Size: 14-15px
  • Weight: 600-700
Group label
Group label h3.sub
Short uppercase label above a group of controls or a list section.
  • Size: 13px
  • Weight: 600
  • Transform: uppercase, 0.06em letter-spacing
  • Color: --text-muted
Body text is 13px. This is the default size for all content copy, table cells, form inputs, and buttons.
Body
Default reading size. Matches form inputs and button labels.
  • Size: 13px
  • Line-height: 1.55
Muted text for descriptions, hints, secondary metadata, and timestamps.
Muted
Secondary information. Use --text-muted, never reduce opacity.
v0.7.0-dev.20   /api/admin/tokens
Monospace
For version strings, paths, IDs, code, terminal output, and timestamps. Never mix with body text without a visual separator.

3Buttons

Every content-area button shares an elevation invariant: a 1px border, a non-zero drop shadow, and a fill that contrasts with the card it sits on. The elevation is what signals "raised = pressable" without depending on hover. Touch devices cannot hover; colorblind users cannot rely on color cues alone. Elevation is the universal signal.

Primary .btn.btn-primary
The main action in a view or modal. Exactly one per context. Filled accent green with dark text and strong elevation.
  • Where: content area only, one per form / modal / settings card.
  • Examples: Save, Create, Connect, Apply, Add Hub.
Secondary .btn
Default button. Any content-area action that is not the primary commit: Cancel, Refresh, Logs, Browse, Restart.
  • Where: content area.
  • Rule: a modal always has exactly one Primary and one Secondary.
Danger .btn.btn-danger
Initiates a destructive action. Same geometry as Secondary with red text and an automatic warning glyph prefix (⚠, injected via ::before) for colorblind-safe identification. Hover commits to filled red.
  • Where: content area, next to the item being acted on.
  • Examples: Delete token, Remove machine, Revoke access.
  • Escalation: for irreversible actions, Danger opens a confirmation modal containing a .btn-destructive.
Destructive .btn.btn-destructive
High-commitment destructive action used only inside confirmation modals. Filled red matches the visual weight of a primary button so the commit is always the dominant choice on screen.
  • Where: confirmation modals only, never in the main content area.
  • Sibling: always paired with a .btn labeled "Cancel".
  • Examples: Delete permanently, Discard changes, Remove forever.
Icon button .btn.btn-icon
Compact square button with a single glyph. Same elevation as other content buttons. Always carries a title attribute for the action name.
  • Where: content-area toolbars and list rows.
  • Do not confuse with .chrome-btn (topbar-only circular button).
Small variant .btn-sm
Modifier class for dense contexts (toolbars, inline editors, row-level actions). Reduces padding and font-size.

Links are the one place in TDL where underline carries meaning. Any clickable text must be underlined by default — not only on hover. The underline is the universal affordance for text links. The brand link is the one exception.

Primary link .link
Accent-colored underlined text. Always underlined by default; hover thickens the underline from 1px to 2px and shifts color to --accent-hover.
  • Where: body copy, modals, help text, menu items, anywhere non-topbar.
  • Examples: "Learn more", "View documentation", "Open in browser", inline references in help text.
Muted link .link-muted
Secondary navigation link. Same underline rule, but colored --text-muted so it sits quietly beside surrounding body content. Hover brightens to --text.
  • Where: footers, metadata cross-references, breadcrumb tails, secondary navigation.
  • Examples: "Privacy", "Terms", "Back to list", inline cross-references.

The file share is configured in Client Settings. See the file sharing guide for details.

Inline use
Links inline with body text inherit the paragraph's font size. The 1px-to-2px underline thickness bump on hover works at any font size.
Rules at a glance.

5Status and badges

Status labels are flat, inline, non-interactive, and always prefixed by a glyph (colored dot, checkmark, or arrow). The glyph is the primary signal so meaning survives red-green colorblindness. Status elements never have a border, fill, or elevation — that would invite clicks.

Online Connecting Error Offline
Status badge .status
Flat inline label for connection, health, or reachability state. Colored dot + uppercase label. Non-interactive.
  • Variants: .status-online (accent), .status-degraded (warn), .status-error (danger), .status-offline (muted).
  • Examples: Agent ONLINE, Hub CONNECTING, Session ERROR.
v0.7.0-dev.20 v0.7.0-dev.18
Version status .status-current / .status-outdated
Specialization of the status badge for software versions. Monospace, mixed-case, with a glyph prefix (✓ for current, ↑ for outdated) injected via ::before. The glyph is the primary signal for colorblind-safe identification.
  • Rule: applied to the installed version only. The available (latest) version is rendered in default text color with no status decoration.
  • Examples: Installed Tools card, sidebar agent version, topbar TelaVisor version.
2 hubs windows/amd64 production
Metadata chip .chip
Small flat filled pill for neutral metadata tags. Muted text on --surface-alt, no border. Explicitly not a state indicator — use .status for state.
  • Why not outlined? Outlined boxes around text are a web convention for interactive tags (GitHub labels, Stack Overflow). A border is a false affordance.
  • Examples: "2 hubs" next to an agent, platform labels, region or environment tags.
Status dot .dot
The raw colored dot primitive underneath .status. Use alone when space is tight or when the dot is paired with a name instead of a state label.
  • Variants: .dot-online, .dot-degraded, .dot-error, .dot-offline.
  • Accessibility: when used alone, pair it with a tooltip (title) so the meaning is reachable.

6Form inputs

All inputs share a common visual grammar: a 1px border, subtle surface fill, accent-colored focus ring (3px low-opacity), and a consistent padding scale. Labels always appear above the control (except for checkboxes and radios, where the label is to the right of the control).

The public URL where your hub is reachable.
Text input .form-input
Standard text field. Wrap in a .form-group to include the label and hint.
  • Label: above the input, 12px 600.
  • Hint: below the input, 11px muted.
  • Focus: accent border + 3px accent ring.
Monospace variant .form-input.mono
For tokens, IDs, paths, version strings, and any content the user copies verbatim.
Port must be between 1 and 65535.
Invalid state .form-input.invalid + .form-error
Border turns danger red. The error message appears below with a ⚠ prefix.
Select .form-select
Native dropdown with a custom chevron. Same padding and focus ring as .form-input.
Textarea .form-textarea
Multi-line input. Defaults to monospace for YAML / config / token editing. Resize: vertical only.
Checkbox .form-check
Accent-colored checkbox with an inline label to the right. This is the one place where the label is not above the control.
Radio group
Same .form-check wrapper; the radios are visually grouped in a row or stacked column.

7Cards

Cards are the primary content container. White (or dark) surface, 1px border, 8px radius, subtle shadow. Never nest a card inside another card. For visual grouping inside a card, use h3.sub labels with spacing.

Connection
Default behavior when TelaVisor starts.
Basic card .card
Standard card with title, optional description, body, and optional footer.
  • Structure: .card-title → .card-desc → .card-body → .card-footer.
  • Padding: 20px 22px.
Delete account
This cannot be undone. All tokens, hubs, and session history will be removed.
Danger card .card.card-danger
Modifier for cards that house irreversible actions. Red border and warning glyph prefix on the title.
  • Where: "Danger zone" sections of settings pages.

8Modals

Modals center on a semi-transparent overlay. The dialog has a header with title and close button, a body, and an action footer on a subtle tinted background. A confirmation modal always has exactly two buttons: Cancel (left) and the commit action (right, Primary or Destructive).

Standard modal

Confirmation modal (destructive)

9Tables

Tables use uppercase-muted headers, muted row separators, and a hover highlight on selectable rows. Numeric or identifier columns should use the monospace font.

Tool Installed Available
TelaVisor v0.7.0-dev.20 v0.7.0-dev.20
tela v0.7.0-dev.20 v0.7.0-dev.20
telad service (running) v0.7.0-dev.18 v0.7.0-dev.20
telahubd not installed v0.7.0-dev.20

10Tabs and toolbars

Tab bars use text buttons with an accent bottom border on the active tab. Toolbars are containers for content-area command buttons; the buttons themselves are .btn.btn-sm — the same elevation invariant as every other content button, just smaller. There is no separate toolbar-button class. The toolbar's tinted background is what makes it visually distinct, not the buttons inside it.

Tab bar

Toolbar

Sidebar items are selectable rows with a left accent border on the active item. They pair a name (and optional dot, chip, or version status) with a tight hit area.

12Topbar

The topbar has a true light variant and a true dark variant. Chrome buttons and the brand mark use topbar-scoped custom properties (--tb-*) so the same markup adapts to either context. The topbar is the only place where .chrome-btn and .brand-link appear.

Light topbar

v0.7.0-dev.20

Dark topbar

v0.7.0-dev.20

Brand link .brand-link

The clickable brand mark. Deliberate exception to the "visible non-hover affordance" rule: web convention has trained users that a top-left logo goes home, so the click must work. "Tela" follows --tb-brand (dark text on light topbar, white on dark). "Visor" / "Saya" stays accent green in both themes. Hover: subtle brightness bump (filter: brightness(1.15)). No underline, ever. Focus: 2px accent outline with 4px offset.

Chrome button .chrome-btn

Circular icon button with a persistent outline and a meaning-carrying glyph. Topbar only — never in content. Three color variants: neutral, .chrome-accent (active/primary), and .chrome-danger (quit). Inline SVG is preferred over Unicode glyphs for stroke consistency across platforms.

Mode bar .mode-bar

The mode bar switches between top-level application modes (TelaVisor's Clients / Infrastructure; Awan Saya's user / admin). It lives in the topbar only — content-area navigation uses the main tab bar.

The mode bar uses an outlined container (signaling "these are interactive options") with a 3px accent bar flush along the bottom edge of the active segment (the existing TDL vocabulary for "you are here", matching the main tab bar's active indicator). The active segment is bold, has cursor: default, and carries no button chrome. Inactive segments show a subtle hover fill on mouseover to confirm they are interactive.

TelaVisor
TelaVisor

13Feedback

Inline, non-blocking messages. Toasts appear briefly after an action. Empty states explain what the user sees (or doesn't see) without burying a useful action.

Connection lost. Reconnecting…
Profile saved
Hub offline
Toast .toast
Transient status message. Three variants: error (default), success, warn. Each carries its own icon prefix.
  • Position: fixed near the top-center of the window.
  • Duration: 3-5 seconds auto-dismiss, or manual dismiss for errors.
No agents registered.
Pair Agent to add one.
Empty state .empty-state
Explains why a list or view has no content. Includes a pointer to the action the user should take next.

14Progress and code

Progress bar .progress
Determinate (set width on .progress-bar) or indeterminate (add .indeterminate).
  • Examples: file upload / download progress, update install progress.
# Connect to a hub
tela hub add "https://owlsnest.parkscomputing.com"
tela connect owlsnest
Code block .code-block
Monospace block for command examples, config snippets, and terminal output. Tokenization classes: .tok-comment, .tok-kw, .tok-str.

15Log panel

The log panel is a dockable bottom-of-window container for multi-source log output. It is TelaVisor-specific in its current implementation but generalizes to "any docked panel with tabs and a collapse affordance". Tabs behave like the main tab bar (active accent underline). Individual tabs may be closable. The panel itself can be collapsed to a single-row header.

Expanded panel

2026-04-11T02:20:02Z TelaVisor started
2026-04-11T02:20:02Z Profile loaded
2026-04-11T02:20:04Z Connected to owlsnest.parkscomputing.com
2026-04-11T02:20:04Z 4 agents registered

Collapsed panel

Logs
Live Idle
Log tab state indicators
Reuse .status for per-tab live / idle markers. No new classes needed.
Log timestamps must be ISO 8601 UTC. The client converts to local time for display in other views, but raw log output stays in UTC so logs from multiple machines line up when cross-referenced.
Logs
Collapse/expand toggle
A plain .btn.btn-sm.btn-icon carrying a chevron glyph. No special log-panel class needed — it is just an icon button. Because all icon buttons share the same sizing, the chevron matches the height of the adjacent + attach-source button and the Copy / Save / Clear buttons in the same strip.
  • Collapsed state: the label .log-panel-collapsed-title appears immediately to the left of the chevron so the docked panel is identifiable at a glance.
  • Caveat: clearing any inline style.height on collapse is required so the CSS class rule takes effect.
Attach source button .log-panel-tab-add
Opens a dropdown menu of log sources that can be added as a new tab. Styled as a small icon button so it reads as a control, not another tab. The modifier class only handles positioning within the tab strip (margin + self-align); the base styles come from .btn.btn-sm.btn-icon.

Dropdowns are small floating panels anchored to a trigger button. Used for user menus, theme pickers, attach-source popovers, context menus, and any short list of options where a full modal would be overkill. The menu itself is a surface card with an item list.

Menu .menu
Floating surface panel. 6px vertical padding, 200px minimum width, strong outer shadow for pop-off-surface effect.
  • Structure: optional .menu-header → one or more .menu-item with optional .menu-sep dividers.
  • Item states: plain, .active (accent color), .danger (red, typically last item).
  • Icon column: .menu-item-icon reserves a 16px column on the left so items without icons align with items that have them.
Anchoring and dismissal. A menu is positioned programmatically near its trigger button. It dismisses on: click outside, Escape key, trigger re-click, or selection of an item. The trigger button should toggle aria-expanded for screen readers.
Menu vs modal. Use a menu for short choice lists (theme, sign out, attach source). Use a modal for any interaction that collects more than one field or requires confirmation. If you find yourself putting form inputs inside a menu, switch to a modal.

17Layout primitives

Two-column sidebar + detail is the canonical layout for any view with a selectable list and its detail pane. The sidebar is a fixed width; the detail fills the remaining space and scrolls.

Work Laptop Online
On owlsnest.parkscomputing.com

18Invariant rules

Category separation

Every interactive or state-bearing element in a TDL app falls into exactly one of four categories. The categories never share a location, so their visual styles never compete.

Color + glyph redundancy

Any state information that matters must be conveyed by both color and shape. Destructive actions get a ⚠ glyph. Up-to-date versions get a ✓. Outdated versions get a ↑. Status badges get a colored dot. This survives red-green colorblindness (WCAG 1.4.1) and the cost is negligible.

No false affordances

An element that is not clickable must not look clickable. No outlined boxes around static labels. No filled pills for non-interactive state. No hover states on text. No cursor: pointer on non-buttons.

Every link is underlined

Any clickable text outside the topbar must be underlined by default, not only on hover. TDL uses accent green for primary links and --text-muted for secondary links; blue is not used. The brand link is the one documented exception.

Mode bars live in the topbar

Segmented mode toggles (Clients / Infrastructure, user / admin) live only in the topbar. Content-area navigation uses the main tab bar, never a segmented mode bar. The two patterns have different visual signatures so they never compete.

One primary per context

A view, form, or modal has exactly one .btn-primary. If the design seems to need two, one of them is probably a Secondary or Danger. Confirmation modals have exactly one .btn-destructive sibling to a Cancel.

ISO UTC everywhere

All timestamps in data, logs, and configs are ISO 8601 UTC. The client converts to the user's local time zone for display. Never store or transmit local time.

No emoji in chrome

Some platforms render Unicode glyphs (⚙, ⚠, 📁) as colorful emoji. This breaks the monochrome chrome convention. Prefer inline SVG for any icon that must render consistently across platforms.

Modal stacking (z-order)

Modals stack. When a modal opens a child modal (for example, a confirmation dialog asking "Discard unsaved changes?" from inside an Application Settings modal), the child must render above its parent, not behind it. Implementations must use a shared modal stack that assigns ascending z-index per push, or give every nested overlay a strictly higher z-index than its parent. A confirmation dialog hidden behind its parent is a serious UX bug: from the user's perspective, clicking Cancel appears to do nothing.

Modals capture window chrome

While any modal is open, native window controls (close / quit / minimize) must not dismiss the application directly. The close button on the OS title bar, a Cmd+Q, and a window-level beforeunload should all route through the active modal's cancel flow first, just as they would for a native OS dialog. This prevents data loss from accidentally quitting while a settings modal has unsaved edits. Implementation: bind the window close event while a modal is active and either block the close, or programmatically trigger the modal's Cancel handler.