Components
The live style guide is the authoritative component reference. Every
component ships with a runnable showcase route under /components/*
that demonstrates props, variants, and rendered output side-by-side
with the Vue source.
Run the style guide:
npm run dev
# → http://localhost:3030/components
What ships today
| Component | Wraps | Route |
|---|---|---|
TuxAccordion | tux native | /components/accordion |
TuxActivityTimeline | tux native | /components/activity-timeline |
TuxAlert | UAlert | /components/alert |
TuxAlphaNav | tux native | /components/alpha-nav |
TuxAnnouncementBanner | tux native | /components/announcement-banner |
TuxAvatar | tux native | /components/avatar |
TuxAppFrame | tux native | /components/app-frame |
TuxAppSwitcher | UPopover | /components/app-switcher |
TuxArtifact | tux native | /components/artifact |
TuxBadge | UBadge | /components/badge |
TuxBetaRibbon | tux native | /components/beta-ribbon |
TuxBigStat | tux native | /components/big-stat |
TuxBlockquote | tux native | /components/blockquote |
TuxBranchNav | tux native | /components/branch-nav |
TuxBreadcrumbs | tux native | /components/breadcrumbs |
TuxButton | UButton | /components/button |
TuxCallout | tux native | /components/callout |
TuxCaptionedMedia | tux native | /components/captioned-media |
TuxCard | tux native | /components/card |
TuxCardCarousel | UCarousel (embla) wrap | /components/card-carousel |
TuxCardSlab | tux native | /components/card-slab |
TuxCookieConsent | tux native | /components/cookie-consent |
TuxChatMessage | tux native | /components/chat-message |
TuxCitations | tux native | /components/citations |
TuxCodeBlock | Shiki | /components/code-block |
TuxCodeMaroon | tux native | /components/code-maroon |
TuxCommandPalette | tux native | /components/command-palette |
TuxCommentThread | tux native | /components/comment-thread |
TuxContactCard | tux native | /components/contact-card |
TuxContextMeter | UPopover | /components/context-meter |
TuxContextPanel | tux native | /components/context-panel |
TuxConversationList | tux native | /components/conversation-list |
TuxComposer | tux native | /components/composer |
TuxCTA | tux native | /components/cta |
TuxDataTable | tux native | /components/data-table |
TuxDescriptionList | tux native | /components/description-list |
TuxDiagram | Mermaid | /components/diagram |
TuxDocsSidebar | tux native | /components/docs-sidebar (renders internal child TuxDocsSidebarNode) |
TuxDropdown | tux native | /components/site-nav |
TuxEmptyState | TuxCard composite | /components/empty-state |
TuxErrorPage | tux native | /components/error-page |
TuxExample | showcase primitive | (used on every component page) |
TuxFAB | tux native | /components/fab |
TuxFactoid | tux native | /components/factoid |
TuxFilterPanel | tux native | /components/filter-panel |
TuxFocusView | tux native | /components/focus-view |
TuxFooter | tux native | /components/footer |
TuxIconFeature | tux native | /components/icon-feature |
TuxIdentity | tux native | /components/identity |
TuxInfoLabel | UPopover | /components/info-label |
TuxInlineCitation | UPopover | /components/inline-citation |
TuxKbd | tux native | /components/kbd |
TuxLinkList | tux native | /components/link-list |
TuxLinkSlab | tux native | /components/link-slab |
TuxMcpEmbed | tux native | /components/mcp-embed |
TuxMediaSlab | tux native | /components/media-slab |
TuxMegaMenu | tux native | /components/site-nav |
TuxMenuBar | UDropdownMenu | /components/menu-bar |
TuxMobileFrame | tux native (CSS) | /components/mobile-frame |
TuxModal | UModal | /components/modal |
TuxNewsCollection | tux native | /components/news-collection |
TuxPageContainer | tux native | /components/page-container |
TuxPageHeader | tux native | /components/page-header |
TuxPagination | tux native | /components/pagination |
TuxPhotoGrid | tux native | /components/photo-grid |
TuxPopover | UPopover | /components/popover |
TuxProse | tux native | /components/prose |
TuxQACollection | tux native | /components/qa-collection |
TuxRailNav | tux native (<details>) | /components/rail-nav |
TuxRemovableChip | tux native | /components/removable-chip |
TuxRichDataGrid | tux native | /components/rich-data-grid |
TuxRichTextEditor | Tiptap + lowlight | /components/rich-text-editor |
TuxRuleBuilder | tux native | /components/rule-builder (renders internal child TuxRuleBuilderGroup recursively) |
TuxSearch | tux native | /components/search |
TuxSectionHeader | tux native | /components/section-header |
TuxShortcutsHelp | tux native (<dialog>) | /components/shortcuts-help |
TuxSidebarBlock | tux native | /components/sidebar-block |
TuxSignupFeature | tux native | /components/signup-feature |
TuxSiteNav | tux native | /components/site-nav |
TuxSkeleton | tux native | /components/skeleton |
TuxSlideover | tux native | /components/slideover |
TuxSplashScreen | tux native | /components/splash-screen |
TuxSplitPane | tux native | /components/split-pane |
TuxStatusToast | tux native (useTuxToast bus) | /components/status-toast |
TuxStepper | tux native | /components/stepper |
TuxStatComparison | tux native | /components/stat-comparison |
TuxSuggestionChips | tux native | /components/suggestion-chips |
TuxTable | UTable | /components/table |
TuxTabBar | tux native | /components/tab-bar |
TuxTabs | UTabs | /components/tabs |
TuxTeachingPopover | tux native | /components/teaching-popover |
TuxTestimonial | tux native | /components/testimonial |
TuxTOC | tux native | /components/toc |
TuxTooltip | UTooltip | /components/tooltip |
TuxTree | tux native | /components/tree (renders internal child TuxTreeNode) |
TuxTreemap | tux native | /components/treemap |
TuxUserMenu | UDropdownMenu | /components/utility-cluster |
TuxUtilityCluster | composes TuxAppSwitcher + TuxUserMenu | /components/utility-cluster |
Pagination + result-display family (added 2026-05-21):
| Component | Stack | Showcase |
|---|---|---|
TuxResultCount | tux native | /components/result-count |
TuxLoadMore | tux native | /components/load-more |
TuxInfiniteScroll | tux native | /components/infinite-scroll |
TuxReactionBar | tux native | /components/reaction-bar |
Research-publishing family (added 2026-05-22):
| Component | Wraps | Showcase |
|---|---|---|
TuxAbstract | tux native | /components/research-publishing |
TuxAuthorByline | tux native | /components/research-publishing |
TuxPaperMeta | tux native | /components/research-publishing |
TuxFigureCaption | tux native | /components/research-publishing |
TuxTableCaption | composes TuxFigureCaption | /components/research-publishing |
TuxFootnote | UPopover | /components/research-publishing |
TuxCitationExport | UDropdownMenu | /components/research-publishing |
TuxAcknowledgments | tux native | /components/research-publishing |
TTI identity family (added 2026-05-22):
| Component | Wraps | Showcase |
|---|---|---|
TuxResearcher | tux native | /components/tti-identity |
TuxLab | tux native | /components/tti-identity |
TuxProgram | tux native | /components/tti-identity |
TuxFundingSource | tux native | /components/tti-identity |
TuxCenterBadge | tux native | /components/tti-identity |
Geospatial family (added 2026-05-22):
| Component | Wraps | Showcase |
|---|---|---|
TuxMapEmbed | iframe / slot | /components/geospatial |
TuxCorridorStrip | tux native SVG | /components/geospatial |
TuxMapLegend | tux native | /components/geospatial |
TuxMapMarker | tux native SVG | /components/geospatial |
Forms wrapper family (added 2026-05-22):
| Component | Wraps | Showcase |
|---|---|---|
TuxFormField | tux native | /components/forms-wrapper |
TuxMarkdownEditor | tux native (no deps) | /components/forms-wrapper |
TuxFileDropzone | tux native | /components/forms-wrapper |
TuxValidationSummary | tux native | /components/forms-wrapper |
TuxConfirmDialog | TuxModal preset | /components/forms-wrapper |
Native chart family (Priority B — closed 2026-05-22):
| Component | Stack | Showcase |
|---|---|---|
TuxChartLine | tux native SVG | /visualizations/chart-line |
TuxChartBar | tux native SVG | /visualizations/chart-bar |
TuxChartArea | tux native SVG | /visualizations/chart-area |
TuxChartDonut | tux native SVG | /visualizations/chart-donut |
TuxChartScatter | tux native SVG | /visualizations/chart-scatter |
TuxChartGauge | tux native SVG | /visualizations/chart-gauge |
TuxChartHeatmap | tux native SVG | /visualizations/chart-heatmap |
TuxChartHistogram | tux native SVG | /visualizations/chart-histogram |
Reports section
Finished-narrative deliverables — paper, PDF, print, or web-hosted
long-form. The reader reads top-to-bottom; they don't pivot or
filter. Components live in app/components/ like everything else,
but demos live under /reports/<kebab> (with a landing at
/reports).
| Component | Wraps | Output medium | Route |
|---|---|---|---|
TuxReportFrame | tux native | paper · PDF | /reports/frame |
TuxReportPrintSheet | useHead injection | print stylesheet | /reports/print-sheet |
TuxReportWebFrame | tux native | web (HTML page) | /reports/web-frame |
Visualizations section
Interactive data surfaces — BI dashboards, R artifacts, native charts. The reader pivots, filters, drills in. Same pattern as Reports (flat components, grouped routes), but a different section because the chrome and posture are different (provider chip, sandbox, source caption).
| Component | Wraps | Route |
|---|---|---|
TuxVizEmbed | sandboxed <iframe> + poster fallback | /visualizations/embed |
TuxVizRPlot | <img> / <object> / <iframe> | /visualizations/rplot |
TuxVizGrid | tux native (CSS Grid layout shell) | /visualizations/grid |
TuxSparkline | tux native (inline SVG) | /visualizations/sparkline |
TuxChartFrame | tux native (editorial wrapper) | (used by /visualizations/* showcase pages) |
TuxChartGeographic | tux native (5-kind Texas map) | /visualizations/chart-geographic |
TuxChartSunburst | tux native (two-ring radial) | /visualizations/chart-sunburst |
TuxMetroInset | tux native (neighborhood grid) | (used by /visualizations/chart-geographic) |
See ADR-0008
for the positioning rationale (why data-display stays flat in
/components/, why Reports and Visualizations get their own sections,
and the criteria for adding a future route group).
The showcase pages expose Vue (template source), HTML (rendered
DOM), and — where applicable — Source (the component SFC) tabs via
the TuxExample primitive.
Platform-aware chrome family (added 2026-05-22):
| Component / composable | Wraps | Route |
|---|---|---|
TuxAppFrame | tux native | /components/app-frame |
TuxAppSwitcher | UPopover | /components/app-switcher |
TuxFocusView | Teleport + native | /components/focus-view |
TuxMenuBar | UDropdownMenu | /components/menu-bar |
TuxSplashScreen | tux native | /components/splash-screen |
TuxSplitPane | tux native | /components/split-pane |
TuxTabBar | tux native | /components/tab-bar |
TuxFAB | tux native | /components/fab |
useTuxPlatform | composable | (consumed by all of the above) |
useTuxSwipe | composable | /components/swipe |
useTuxRipple | composable | /components/ripple |
These ship together under the platform-aware banner. They cover
the brand-vs-chrome split documented in
platform-awareness.md: one component
tree, platform-adaptive at the chrome layer. The tux-scrollbar.css
utility ships alongside (auto-imports via globals.css) and
branches scrollbar styling per host via the data-platform
attribute set by useTuxPlatform().
Pattern coverage — "want X? use Y"
If you're about to build something, check this map first. The catalog covers more than the file names suggest, and shipping a duplicate component because the existing one had a non-obvious name is the single most common failure mode.
| Want… | Use… |
|---|---|
Tag (mono-font label chip — topic:safety, pii:us_ssn) | <TuxBadge kind="tag"> |
| Status pill (live/running/failed/queued + dot) | <TuxBadge :status="…"> |
| Classification tier (Public / Internal / Restricted / ITAR) | <TuxBadge :tier="…"> |
Count badge (md (11) facet count) | <TuxBadge kind="count" :count="…"> |
| Page-level admonition (Docusaurus-style note/tip/warning/danger) | <TuxAlert variant="…"> — 8 variants |
| Compliance alert (ITAR / export-controlled / legal callout) | <TuxAlert variant="compliance"> |
| Inline body callout (single-paragraph aside in flowing prose) | `<TuxCallout kind="fact |
| Standalone pull quote (focal element, attribution + rules) | <TuxBlockquote> |
| Long-form Q&A (always-expanded explainer prose) | <TuxQACollection> |
| FAQ (collapsible question/answer for scanning) | <TuxAccordion kind="faq"> |
| Code block in a doc / blog / ADR | <TuxCodeBlock> |
| Code in a component-demo flow (Vue + HTML reveal tabs) | <TuxExample> |
| Term/definition list (event details, file metadata, spec list) | <TuxDescriptionList> |
| Architecture diagram (boxes + arrows, decision flows) | <TuxDiagram> (Mermaid) |
| Markdown content with Tux components inline | @nuxtjs/mdc + auto-import (see /markdown demo) |
| Form input (email / select / radio / etc.) | Nuxt UI native — UInput, USelect, etc., layer-themed (Batch K in tux.css: Work Sans controls, tux radius, maroon focus). The hybrid doctrine (unification-plan.md, owner-ratified 2026-07-30): U* is the blessed spelling for app controls — no Tux input wrappers ship; TuxFormField owns label/help/error anatomy; TuxButton is the editorial button (gold-bar/CTA contexts), not the app-control default. Lint targets raw <input>/<button> elements and hex literals, never U*. See /forms. |
| Data table (sortable, virtualizable, status cells) | <TuxTable> |
| Sortable / selectable / expandable data grid (Landscape-class operational lists with bulk actions, active-filter chips, row expansion) | <TuxRichDataGrid> |
| Static research table (numbered caption, ± CI uncertainty, footnotes, source citation, optional totals row) | <TuxDataTable> |
| Search bar (Landscape finder, conversation search) | <TuxSearch> |
| A–Z directory jump bar | <TuxAlphaNav> |
| Sidebar widget wrapper (related links, contact box, in-page nav) | <TuxSidebarBlock> |
| Newsletter signup | <TuxSignupFeature> |
| Big oversized stat (single headline metric) | <TuxBigStat> |
| Row of oversized stats ("by the numbers" 3/4/5-up) | <TuxFactoid> |
| Tiny inline trend (no axes, beside a stat) | <TuxSparkline> |
| Loading placeholder (cards, lists, tables, articles, stats) | <TuxSkeleton kind="…"> |
| 404 / 500 / 403 / 503 page | <TuxErrorPage code="…"> |
| Multi-step flow indicator (funding application, IRB, study onboarding) | <TuxStepper :steps :current-index> |
| Site-wide notice strip (closure, maintenance, security advisory) | <TuxAnnouncementBanner> |
| "This isn't production" label (preview env, public beta, sandbox) | <TuxBetaRibbon variant="…"> |
| Privacy / cookie consent (public TTI surface) | <TuxCookieConsent> |
| Quarterly / sponsor PDF report (paper, print, PDF export) | <TuxReportFrame> |
| Web-hosted long-form report (annual report, findings page at a permanent URL) | <TuxReportWebFrame> |
| "Print this page" affordance on an existing screen | <TuxReportPrintSheet> |
| BI dashboard embed (Tableau, Power BI, Superset, Grafana) | <TuxVizEmbed> |
| R / ggplot artifact (PNG, SVG, htmlwidget) | <TuxVizRPlot> |
| Side-by-side dashboard tiles (small-multiples 2/3/4-up) | <TuxVizGrid> |
| Texas-flavored map (county choropleth, TxDOT districts, in-state dot density, OD flow arcs, AlbersUsa context) | <TuxChartGeographic kind="…"> |
| Multi-metro inset grid (4-up neighborhood drill-down — Houston / DFW / Austin / SAT) | <TuxMetroInset> |
| Two-ring radial breakdown (sister to treemap; categorical part-to-whole with center total) | <TuxChartSunburst> |
| Time-of-day / matrix intensity (crashes by day × hour, corridor demand by month, station uptime) | <TuxChartHeatmap> |
| Distribution of raw observations (travel-time reliability, delay spread; p50/p95 planning-time pair) | <TuxChartHistogram :percentiles="[50, 95]"> |
| Vertical event timeline (project milestones, ingest/agent feed, session history) | <TuxActivityTimeline> |
| Editorial wrapper for a multi-exhibit visualizations page (eyebrow + Oswald title + maroon signature + body + source) | <TuxChartFrame> |
If your need isn't here, scan /components (or
app/utils/tuxCatalog.ts, the census the
catalog test enforces) — there are ~140 Tux* components and this map
only highlights the ones with the easiest-to-miss names.
Conventions
Prop & emit vocabulary — the words and what they mean
Ratified 2026-08-19 after a full-catalog vocabulary audit. TuxBadge
and TuxSkeleton are the reference implementations. New components
MUST use these words with these meanings; deviations listed below are
grandfathered until the next major.
| Word | Means | Value shape | Reference |
|---|---|---|---|
tone | Semantic color | info · success · warning · error · neutral (semantic family) — or brand-paint (maroon · gold · charcoal) on marketing surfaces | TuxBadge, TuxActivityTimeline |
kind | Structural preset | named layout/content presets (faq, first-run, tag) | TuxBadge, TuxSkeleton, TuxEmptyState |
variant | Editorial style | default · bold · elegant (the section-style triad) — or a visual style set where the triad doesn't apply | TuxBigStat, TuxCallout, TuxSkeleton |
intent | Action semantics on interactive controls | primary · secondary · ghost · destructive | TuxButton |
size | Scale | t-shirt (sm · md · lg) for components; px number for chart canvases; paper (letter · a4) for report frames | TuxAvatar, charts |
density | Row/space compactness | comfortable · compact | TuxDataTable, TuxRichDataGrid |
layout | Arrangement | prefer inline · stacked (+ grid · columns where real) | TuxDescriptionList |
emit hover | Chart roving-cursor payload (payload | null) | — | all interactive charts |
emit select | User picked an item | — | TuxAlphaNav, TuxTree |
emit update:x | v-model bridge only | — | everywhere |
Two meanings of tone, on purpose: product surfaces use the
semantic family (status colors); marketing/editorial surfaces
(TuxCTA, TuxCardSlab, TuxPhotoGrid, …) use brand-paint values.
Both are "which color is this thing" — that's the invariant.
Brand-paint values name the actual paint (maroon, gold,
charcoal, navy, neutral) — never alias one paint word for a
different color.
The v2.0.0 rename batch LANDED 2026-08-19 (see CHANGELOG
"Breaking"): TuxFactoid.columns, TuxBetaRibbon.kind,
TuxTabs.variant, TuxChartGauge band tone: success·warning·error, TuxCodeMaroon.tone (error replaces
alert), TuxTestimonial.tone, TuxConfirmDialog drops the
danger alias, select replaces pick
(TuxConversationList/TuxSuggestionChips) and citation open
(TuxCitations/TuxInlineCitation), stacked replaces stack
(TuxStatComparison/TuxMapLegend),
TuxFigureCaption/TuxTableCaption label replaces free-form
kind, TuxRichTextEditor's duplicate bare update emit removed,
and TuxTreemap joins the family hover emit.
Deviations kept, by decision: TuxResultCount emits
update:page (a legitimate v-model:page) while TuxPagination
uses update:modelValue — both are idiomatic Vue;
TuxCommentThread's comment:*/thread:* colon namespace;
TuxCommandPalette.open meaning "panel opened".
Composition standards for cases where a slot or prop combination shows up across multiple Tux* components or consuming apps. Capturing them here keeps consumers from inventing different icons or labels for the same affordance.
Chat-message actions
TuxChatMessage exposes a #tools slot for the row of small actions
that sit below an assistant response (copy, regenerate, like, etc.).
The Vercel AI Elements Actions reference shows the pattern; TUX
standardizes on this five-icon set so consumers don't pick five
different copy icons:
| Action | Icon | Label | Emit (host-wired) | Notes |
|---|---|---|---|---|
| Copy | lucide:copy | "Copy" | copy | Copies the message body to clipboard |
| Regenerate | lucide:refresh-cw | "Regenerate" | regenerate | Re-runs the prompt that produced this response |
| Share | lucide:share-2 | "Share" | share | Opens host-chosen share affordance |
| Helpful | lucide:thumbs-up | "Helpful" | feedback w/ 'up' | Positive feedback signal |
| Off | lucide:thumbs-down | "Off" | feedback w/ 'down' | Negative feedback signal |
Order convention: Copy · Regenerate · Share · Helpful · Off when
all five appear. Use a subset if the surface doesn't support an
action (e.g., no regenerate when the response is already final).
tti-ai-studio-session.vue
is the canonical example using a Copy / Helpful / Off subset.
Style: icon + label (label can hide at narrow widths), font-size
text-xs, color text-text-muted, hover transitions to
text-brand-primary. Sits in a flex row below a border-t rule.
Form validation — when to use which
Backstage's design system dedicates 38 frames to form validation (inline errors, dialogs, banners) because where validation lives matters as much as what it says. TUX standardizes on four placements; the right one depends on scope (field / form / page / session) and severity (advisory / blocking).
| Placement | Use when | Component(s) | Behavior |
|---|---|---|---|
| Inline field error | A single field is invalid. Most common case — wrong format, out of range, required-but-empty | UFormField with error prop, or compose <TuxInfoLabel> + <UInput> + an error <p class="text-text-danger text-xs"> below | Renders red text below the field; non-blocking; clears on next valid input |
| Inline form summary | Multiple fields invalid AND the user has tried to submit. Tells them how many issues without scrolling | <TuxAlert variant="danger"> above the form body, with a list of <a href="#field-id"> jumps to each broken field | Persists until all issues resolved; field-level inline errors remain the source of truth |
| Blocking dialog | The action being submitted is destructive or irreversible. Forces explicit confirmation | <UModal> (or <TuxModal> for editorial chrome) with primary/secondary actions | Blocks the page; "Delete this corpus?" / "Discard 12 unsaved changes?" / "Revoke API key — this can't be undone" |
| Page/session banner | The issue is not about this form — it's a session-level constraint that affects what the form can do (ITAR scope, expired token, rate limit, server down) | <TuxAlert variant="compliance" | "danger" | "warning"> at the top of the page | Persists across forms on the same page; doesn't compete with field-level errors |
| Toast (transient confirmation) | The form succeeded and the user has moved on. Not a "validation" placement per se, but the closing half of the validation lifecycle | useTuxToast().success("Saved") rendered by the app-shell <TuxStatusToast> host | Auto-dismisses in ~5 seconds (errors stick); non-blocking; escalates to OS notification in unfocused Tauri windows |
Decision tree:
- Single field is wrong → inline field error
- Multiple fields wrong + user tried to submit → inline form summary + keep inline errors
- Destructive action being submitted → blocking dialog
- Session-level constraint (ITAR, auth, rate limit) → page/session banner
- Form succeeded → toast
Anti-patterns to avoid:
- Modal dialog for a single bad field — friction without value; the inline error is faster
- Banner for a single bad field — wrong scope; the user has to find which field
- Inline error for a session-level issue — the user fixes the field but can't progress; root cause lives above
- Toast for an error — transient and easily missed; use banner or dialog for anything the user needs to act on
Canonical references for each pattern in real composition:
forms/inline-validation.vue (field-level),
forms/all-in-one.vue (form summary),
landscape-dashboard ITAR alert (session banner).
First-run AI surfaces — Examples / Capabilities / Limitations
When a consumer surface needs richer first-run framing than the single-
block TuxEmptyState kind="first-run" (e.g. tti-ai-studio's new-session
splash), use ChatGPT's canonical three-column taxonomy. Each column
sets a different expectation before the user types their first prompt:
| Column | What it answers | Typical content |
|---|---|---|
| Examples | "What should I ask?" | 3 concrete prompt strings the user can click to populate the composer |
| Capabilities | "What can it do?" | 3 strengths — corpora it can ground against, formats it can write, languages it understands |
| Limitations | "What should I not trust?" | 3 advisory caveats — knowledge cutoff, hallucination risk, ITAR scope boundaries |
Compose with <TuxFactoid density="3"> (numerical-statistic style) or
a 3-up <TuxCard> grid (richer per-cell layout). The taxonomy is the
discipline; the components rendering it are the consumer's choice.
No dedicated TuxFirstRunTaxonomy component is needed — the value
is the framing, not the chrome.
Anti-patterns to avoid:
- Single-paragraph welcome. "Welcome to tti-ai-studio." reads as marketing, not orientation. The three columns set expectations before the user invests their first prompt.
- Marketing tone in research IT. ChatGPT's first-run is a consumer surface ("Hi, what's on your mind today?"). Research-IT first-run should be quieter and more functional — "Recent session was about X · Pick up where you left off." Keep the taxonomy; drop the marketing voice.
- More than three columns. The discipline is exactly three — the gain over a flat "welcome" is the parallelism. Four blurs it.
Reference frame:
reference/figma-cache/chatgpt-ui-kit-ai-chat/screens/new-chat.png.
Absorbed 2026-05-21 from the ChatGPT UI Kit Figma file.
MCP tool output — inline card / inline carousel / full screen
When an MCP (Model Context Protocol) tool inside tti-ai-studio returns output to render in the chat, three display tiers cover the spectrum. Each is the canonical TUX composition; pick by the density and interactivity of the result.
| Tier | Use when | TUX composition |
|---|---|---|
| Inline card | Single result fits a card. | Static result (place, chart, document, record): <TuxArtifact> — title + icon + meta header, copy/download/regenerate/share actions. Interactive third-party app surface: <TuxMcpEmbed> — app icon + name + window controls (collapse/expand/exit), built-in skeleton, container slot for the app's UI. |
| Inline carousel | 3+ comparable results — list of places, sources, datasets, results. The user scans across, sometimes clicks one. | <TuxCardCarousel> with arrows + dots, sized to ~280–360px cards. Each card is a <TuxArtifact> (static) or <TuxMcpEmbed> (interactive). Header eyebrow names the tool (e.g. nearby trails) + count. |
| Full screen | Rich interaction needed — large data viz, multi-step picker, app-like surface that competes with chat scroll. | Listen for <TuxMcpEmbed>'s @expand event and switch to a focus-view layout (<TuxFocusView>, <TuxSlideover> side-docked, or <TuxModal> centered) holding the same embed. Static surfaces compose <TuxArtifact> inside the same shells. |
Decision tree:
- One static result + read-only → inline card (
TuxArtifact). - One interactive app surface → inline card (
TuxMcpEmbed). - Multiple comparable results + scanning → inline carousel.
- Heavy data / app needs >½ chat width → full screen.
Each tier has a skeleton state for the loading window — TuxMcpEmbed
ships its own (three shimmer bars + a content block, honors
prefers-reduced-motion); Nuxt UI 4's UChatShimmer covers TuxArtifact
inline; TuxSkeleton covers full-screen loads.
Anti-patterns to avoid:
- Full-screen for a one-line answer. The takeover surface is expensive; if the response fits a sentence, just render it in the chat body.
- Inline carousel for two items. Use two side-by-side
<TuxArtifact>blocks (or a single card with a comparison callout) — the carousel chrome (arrows, dots, advance buttons) overweights two items. - Inline card when the result is a list. A 12-result tool dump inside one card is unreadable; promote to inline carousel.
Reference frame:
reference/figma-cache/mcp-apps-for-claude/frames/.
Absorbed 2026-05-21 from Anthropic's official MCP Apps UI kit v1.1.
Faceted vs relational filtering — when to use which
Two filter components, two shapes of question. Reach for the one that matches what the user is actually doing — they're sisters, not substitutes.
| Shape of question | Component | Example |
|---|---|---|
| Narrow a list along known axes. Buckets are pre-aggregated; the user multi-selects from each. | <TuxFilterPanel> (faceted) | "Owner ∈ {Chen, Kim} AND type ∈ {PDF, CSV} AND district ∈ {Bryan, Austin}" — a Landscape browse / results page. |
| Express a relational predicate across heterogeneous fields. Operators matter; numeric / date comparisons appear; nesting appears. | <TuxRuleBuilder> (relational) | "compliance < 50% AND last inspected before 2024-01-01 AND treatment IS rumble" — a research-dashboard "save as smart view" flow. |
| Both, on the same surface. Facets do the dominant narrowing in the left rail; rule builder lives in a slideover for the long-tail query. | <TuxFilterPanel> + <TuxRuleBuilder> | Operational data surface where some users browse and some authors save smart views. |
Anti-patterns:
- TuxFilterPanel for relational predicates. If you find yourself
generating a facet bucket per
>50%/>60%/>70%threshold, the user wanted a numeric comparator. Switch to TuxRuleBuilder. - TuxRuleBuilder for "owner = X." If the dominant axis is multi-select narrowing across a known set, the rule builder makes the user click 3× for what a single facet checkbox covers. Use facets and keep the rule builder for the predicates that don't fit.
- Nesting beyond 3 levels. TuxRuleBuilder caps depth at 3 by default. Deeper trees signal the query should be split into named saved views the user composes, not a single deeply-nested predicate.
Empty states — no decorative illustrations
TuxEmptyState deliberately avoids the saturated-cartoon illustrations
common in consumer-SaaS empty-state kits. TUX is research-publishing;
editorial restraint outweighs the "consumer SaaS expectation" of a
friendly mascot. Empty states use a Lucide icon + heading + body +
optional CTA. If warmth is needed, place a real photograph (a future
TuxPhotoCard — roadmap; today a TuxCaptionedMedia) next to — not
inside — the empty state.
This stance was reaffirmed against the Empty State Illustration Kit
absorption (2026-05-21). The five kind presets (no-data /
no-results / not-found / no-permissions / first-run) cover
the real scenario taxonomy; 200 generic illustrations would not.
Focus rings — the universal rule owns them
The layer ships ONE focus affordance: *:focus-visible in
globals.css applies the themed two-ring --shadow-focus
(--focus-ring-inner / --focus-ring-outer, remapped per theme —
black-on-white in high-contrast). Components must NOT restate their
own ring: a per-component outline: 2px solid <color> double-draws
on top of the shadow, and a per-component outline: none silently
suppresses the universal ring (2026-08-12 audit found four competing
recipes across ten components — all removed).
Exactly two sanctioned exceptions:
- SVG child elements (
<rect>,<path>, …) —box-shadowcannot render on them, so a focusable SVG child carries its ownoutline: 2px solid var(--focus-ring-outer)(the themed token, never a brand color).TuxTreemap's cells are the canonical case. Plot-level capture rects may instead use the documented wash-fill alternative (TuxChartLine's hover capture). @media (forced-colors: active)blocks — Windows High Contrast rings use system colors by design; leave them.
Motion primitives — six canonical transitions
Absorbed from the Tailwind Headless UI w/ Animations Figma kit and reaffirmed by the chart-family entrance animations shipped 2026-05-22. Every Tux component that animates uses one of these six patterns. Designers and contributors should reach for the existing primitive, not invent new motion.
| Primitive | Pattern | Duration | When to use |
|---|---|---|---|
| Tooltip / Popover | fade + scale 0.95 → 1 | fast (~150ms) | TuxTooltip, TuxInfoLabel, TuxInlineCitation, chart tooltips |
| Dropdown menu | slide-down (-8px → 0) + fade | fast (~150ms) | UDropdownMenu, TuxMenuBar triggers |
| Modal / Dialog | fade + scale 0.95 → 1 (or sheet slide on Mac) | normal (~200ms) | TuxModal, TuxFocusView |
| Slideover / Drawer | slide-from-edge | normal (~250ms) | TuxSlideover, sidebar mobile-mode |
| Toast / Notification | slide-from-edge + fade | normal (~200ms) | TuxStatusToast + useTuxToast(), Tauri OS-notification escalation |
| Accordion / Disclosure | height auto + fade | fast (~150ms) | TuxAccordion, UCollapsible |
Plus three component-level entrance primitives shipped 2026-05-22
in tux-component-motion.css:
- Mount fade-up (4px slide + fade, 220ms) — opt-in via
.tux-mount-inon a section; immediate children animate in with optional stagger via--tux-mount-stagger-index. - Mount fade (no slide, 180ms) — for chrome surfaces like alerts and badges that shouldn't move.
- Mount scale (96% → 100%, 200ms) — for popovers / floating chrome that originate from a trigger.
Plus six chart-family entrance primitives in
tux-chart-motion.css:
- Lines draw on via animated
stroke-dashoffset. - Bars scale-Y from baseline (or scale-X horizontally).
- Areas + top-lines rise from scale-Y 0.94.
- Donut slices spin in -12° → 0, staggered.
- Scatter dots pop from scale(0), staggered.
- Gauge fill sweeps via stroke-dashoffset; needle swings.
All motion primitives collapse to instant on
@media (prefers-reduced-motion: reduce). This is non-negotiable —
no contributor escapes it, no "delight" affordance ignores it.
Chart tooltips — consistent pattern across the family
Every interactive chart in the native chart family uses the same
tooltip pattern, shipped 2026-05-22 (TuxChartLine, TuxChartBar,
TuxChartArea, TuxChartScatter) and extended to TuxChartDonut,
TuxChartSunburst, and TuxTreemap 2026-08-12:
- Hover via pointer or focus via keyboard — one tab stop per
chart. Lines / Areas / Bars accept a focusable rect across the
plot area; Scatter / Donut / Sunburst make the
<svg>itself the single focusable surface. Arrow keys cycle the active item as a roving cursor (scatter points in x order; donut slices clockwise; sunburst segments in hierarchical order — group, then its children). Never make every data point a tab stop: a 500-point scatter must not inject 500 tab stops. Heatmap keeps the single tab stop but its roving cursor is two-dimensional — Left/Right walk columns, Up/Down walk rows — because collapsing a matrix to a 1-D walk would alias the axes. Treemap is the one exception — its cells are real drill-in buttons (role="button", Enter/Space drills, Backspace drills up, arrows walk cells), so per-cell focus is the correct semantic. - Visual highlight — vertical guide line + per-series focus dots (Line / Area); column-wash on the active category (Bar); active-dot grow +2px (Scatter).
- Branded tooltip card with the active label, per-series values, and (where applicable) totals, comparison overlays, or previous-period readouts.
- Auto-flip — when the active point sits past 60% of chart width, the tooltip flips to the left of the cursor so it doesn't overflow the parent container on the right.
hoveremit — every chart emitshoverwith the active payload so consumers can sync hover state across multiple panels (e.g., highlight the matching row in an adjacent table).tooltipprop — pass:tooltip="false"to disable when the chart is used in a print / PDF / non-interactive context.- Native SVG
<title>fallback remains on all hover-capable elements for AT users who don't see the branded card.
When building a new chart that needs hover behavior, follow this pattern rather than inventing a parallel one. The visual + a11y + emit contract is the canonical TUX chart-tooltip shape.
Ideas not yet shipped
The catalog originally listed several components here. As of the current cycle, every aspirational entry has shipped. New components land when a consuming app (Landscape, tti-ai-studio, marcom WordPress kit) needs them; open an issue or ping the maintainer to add a row.
Guidance for adding a new component
- Write it against Nuxt UI where possible — inherit a11y, theming,
color-mode integration for free. Only go native when a Nuxt UI
primitive fights TTI branding too hard to override cleanly (see
TuxCardfor an example of when that happens). - Use container queries, not viewport media queries, for any
layout that shifts based on width. Tux components get embedded at
varying widths — full-page, sidebar tile, narrow article column,
demo wrapper, modal body. Viewport queries firing on a 1200px
viewport while the component lives in a 600px column is the
single most common bug we hit.
The pattern:.tux-component { container-type: inline-size; container-name: tux-component; /* base styles, mobile-first */ } @container tux-component (min-width: 44rem) { .tux-component__layout { grid-template-columns: 1.4fr 1fr; } }
Usecqiunits inclamp()for fluid typography that scales with the container, e.g.font-size: clamp(2rem, 1.2rem + 4.5cqi, 3.5rem).
Viewport queries are still right when the component does something inherently page-layout-relative — breaking out of an article column with negative margins, or floating into the page gutter.TuxCaptionedMedia'salign="wide"andalign="right"are the canonical examples; both keep@mediaqueries with an explanatory comment. - Hover/floating UI inside a bounded canvas (tooltips, popovers,
dropdowns) should flip placement near canvas edges — anchor with
right/bottompast the midpoint instead ofleft/top. SeeTuxTreemap's tooltip handler for the canonical pattern. - Add a showcase route at
/components/<kebab-name>. UseTuxExamplewith at least avueprop so the Vue template is exposed. If the component is load-bearing for the brand, also passsourceso readers can see the SFC. - Dogfood
TuxPageHeaderat the top of the page — keeps visual rhythm consistent across every component demo. - Add the entry to
app/app.vuenav,app/pages/index.vueFoundations/Components grid, andapp/pages/components/index.vuetable. - Update
CHANGELOG.mdunder Unreleased → Added. - Run the local guards before you push — don't wait for CI to find
what you could catch in seconds while the component is still fresh in
your head:
npm run lint+npm run typecheck— style + types.npm test— catalog conformance plus the mounted component suites (tests/components/*.nuxt.test.ts, nuxt environment via// @vitest-environment nuxtdocblock, mounted withmountSuspended). New interactive behavior — keyboard contracts, persistence, emits — should land with a mounted test; the chart tooltip suites are the house exemplars.npm run audit:tokens— everyvar(--token)you wrote resolves to a defined token (or fallback / known external namespace). Catches the--surface-baseclass of bug (undefined token → renders transparent), invisible to typecheck and the contrast audit.npm run generate && npm run audit:a11y— structural axe-core pass (roles, landmarks, accessible names, nesting) over your new showcase page.npm run audit:contrastcovers color separately. All three are blocking in CI, so a clean local run is a clean PR.
A component that skips any of these is invisible to future readers — the style guide is its own documentation, so being in the guide is the definition of "shipped."