Skip to content

Tutorial 2: UI Components

The tango-api package provides themed components that match Tango’s look and feel. This tutorial walks through each one with code examples.

Starting from the Hello World instrument, we’ll enhance the sidebar panel to showcase the component library.

Groups related content with an optional title and description:

<UISection title="Settings" description="Configure your instrument">
{/* section content */}
</UISection>

A bordered container for content blocks:

<UICard>
<p>Card content goes here</p>
</UICard>

Tab navigation with content panels:

<UITabs
tabs={[
{ value: "overview", label: "Overview", content: <p>Overview content</p> },
{ value: "details", label: "Details", content: <p>Detail content</p> },
]}
initialValue="overview"
/>

Triggers actions. Uses a label prop for the button text:

<UIButton label="Click me" variant="primary" onClick={() => console.log("clicked")} />
<UIButton label="Secondary" variant="secondary" />
<UIButton label="Danger" variant="danger" />
<UIButton label="Ghost" variant="ghost" size="sm" />

Variants: primary, secondary, danger, ghost Sizes: sm, md

Single-line text input:

const [name, setName] = useState("");
<UIInput
value={name}
placeholder="Enter your name"
onInput={setName}
/>

Multi-line text input:

const [notes, setNotes] = useState("");
<UITextarea
value={notes}
placeholder="Write notes..."
rows={6}
onInput={setNotes}
/>

Dropdown selection:

const [color, setColor] = useState("red");
<UISelect
options={[
{ value: "red", label: "Red" },
{ value: "blue", label: "Blue" },
{ value: "green", label: "Green" },
]}
value={color}
onChange={setColor}
/>

On/off switch with label:

const [enabled, setEnabled] = useState(false);
<UIToggle
label="Enable feature"
checked={enabled}
onChange={setEnabled}
/>

Checkbox with label:

const [agreed, setAgreed] = useState(false);
<UICheckbox
label="I agree to the terms"
checked={agreed}
onChange={setAgreed}
/>

Radio button group:

const [size, setSize] = useState("medium");
<UIRadioGroup
name="size"
options={[
{ value: "small", label: "Small" },
{ value: "medium", label: "Medium" },
{ value: "large", label: "Large" },
]}
value={size}
onChange={setSize}
/>

Segmented button strip (like iOS):

const [view, setView] = useState("list");
<UISegmentedControl
options={[
{ value: "list", label: "List" },
{ value: "grid", label: "Grid" },
]}
value={view}
onChange={setView}
/>

Colored label for status or metadata:

<UIBadge label="Active" tone="success" />
<UIBadge label="Warning" tone="warning" />
<UIBadge label="Error" tone="danger" />
<UIBadge label="Info" tone="info" />
<UIBadge label="Default" tone="neutral" />

Tones: neutral, info, success, warning, danger

Placeholder for empty views:

<UIEmptyState
title="No items yet"
description="Create your first item to get started."
action={<UIButton label="Create" variant="primary" onClick={handleCreate} />}
/>

Simple list with optional selection:

const [selected, setSelected] = useState<string | null>(null);
<UIList>
{items.map((item) => (
<UIListItem
key={item.id}
title={item.name}
subtitle={item.description}
active={selected === item.id}
onClick={() => setSelected(item.id)}
/>
))}
</UIList>

List with single or multi-select:

const [selected, setSelected] = useState<string[]>([]);
<UISelectionList
items={[
{ value: "a", title: "Option A", subtitle: "First option" },
{ value: "b", title: "Option B", subtitle: "Second option" },
]}
selected={selected}
multiple={true}
onChange={setSelected}
/>

Groups are collapsible sections used for tree-like UIs (like the Tasks sidebar):

A collapsible container with title, subtitle, meta, and actions:

const [expanded, setExpanded] = useState(true);
<UIGroup
title="Project Alpha"
subtitle="/Users/me/alpha"
meta={<UIBadge label="3" tone="info" />}
actions={<UIButton label="New" variant="ghost" size="sm" onClick={handleNew} />}
expanded={expanded}
onToggle={setExpanded}
>
{/* group body content */}
</UIGroup>

Render items inside a group:

<UIGroup title="Tasks" expanded={true} onToggle={setExpanded}>
<UIGroupList>
<UIGroupItem
title="Fix login bug"
subtitle="in_progress"
active={selectedId === "1"}
onClick={() => selectTask("1")}
/>
<UIGroupItem
title="Add dark mode"
subtitle="todo"
meta={<UIBadge label="P1" tone="warning" />}
onClick={() => selectTask("2")}
/>
</UIGroupList>
</UIGroup>

Shown when a group has no items:

<UIGroup title="Tasks" expanded={true}>
<UIGroupEmpty text="No tasks yet" />
</UIGroup>

Here’s a complete sidebar that uses several components:

import { useState } from "react";
import {
defineReactInstrument, useInstrumentApi,
UIRoot, UIPanelHeader, UISection, UICard,
UIButton, UIInput, UIBadge, UIToggle, UIEmptyState,
} from "tango-api";
function SidebarPanel() {
const api = useInstrumentApi();
const [name, setName] = useState("");
const [greeting, setGreeting] = useState<string | null>(null);
const [loud, setLoud] = useState(false);
const greet = () => {
const msg = `Hello, ${name || "world"}!`;
setGreeting(loud ? msg.toUpperCase() : msg);
};
return (
<UIRoot>
<UIPanelHeader title="Greeter" subtitle="UI Components demo" />
<UISection title="Configuration">
<UICard>
<UIInput value={name} placeholder="Your name" onInput={setName} />
<UIToggle label="LOUD MODE" checked={loud} onChange={setLoud} />
<UIButton label="Greet" variant="primary" onClick={greet} />
</UICard>
</UISection>
<UISection title="Result">
{greeting ? (
<UICard>
<UIBadge label={greeting} tone={loud ? "warning" : "success"} />
</UICard>
) : (
<UIEmptyState title="No greeting yet" description="Enter a name and click Greet" />
)}
</UISection>
</UIRoot>
);
}
export default defineReactInstrument({
defaults: { visible: { sidebar: true } },
panels: { sidebar: SidebarPanel },
});

A single panel is useful but limited. In the next tutorial you’ll use two panels that communicate with each other.