Tangible Design System · @tangible/ui 0.2.29 · Interactive version · Markdown

Getting Started

Install

npm install @tangible/ui

Optional peer dependencies

Some components require additional packages. Install only what you use:

Package Required by
@floating-ui/react Select, MultiSelect, Combobox, Dropdown, Tooltip
@tanstack/react-table DataTable
# If using Select, Dropdown, Tooltip, etc.
npm install @floating-ui/react

# If using DataTable
npm install @tanstack/react-table

Components without these dependencies (Button, Card, Accordion, Modal, Tabs, etc.) work with zero additional installs.

Import styles

In your app entry point:

import '@tangible/ui/styles';

Or if you're using SCSS and want access to tokens/mixins:

@use '@tangible/ui/styles/scss';

CSS cascade layers

TUI uses internal CSS layers (resets, defaults, components, utilities) to organise its own cascade. This works well when you control the full page.

For WordPress plugin contexts where theme CSS is unlayered, use the unlayered build — unlayered CSS always beats layered CSS regardless of specificity:

import '@tangible/ui/styles/unlayered';
Import Use when
@tangible/ui/styles You control the full page
@tangible/ui/styles/unlayered WordPress plugins, embedded UIs competing with theme CSS

Wrap your app

Components require the .tui-interface wrapper to access design tokens:

function App() {
  return (
    <div className="tui-interface">
      {/* Your UI here */}
    </div>
  );
}

This wrapper:

Dark mode

Set data-theme on the wrapper:

<div className="tui-interface" data-theme="dark">
Value Behaviour
"dark" Force dark mode
"auto" Follow prefers-color-scheme
(omit) Light mode (inherits host colour)

Use components

import { Button, Card, Select } from '@tangible/ui';

function Example() {
  return (
    <Card>
      <Card.Body>
        <Button label="Save" theme="primary" />

        <Select placeholder="Choose..." onValueChange={(v) => console.log(v)}>
          <Select.Trigger />
          <Select.Content>
            <Select.Option value="a">Option A</Select.Option>
            <Select.Option value="b">Option B</Select.Option>
          </Select.Content>
        </Select>
      </Card.Body>
    </Card>
  );
}

Tree-shaking

If your bundler doesn't tree-shake unused components from the barrel export, use individual imports:

import { Button } from '@tangible/ui/components/Button';
import { Tooltip } from '@tangible/ui/components/Tooltip';

Component patterns

Compound components

Several components use dot notation for their parts:

<Card>
  <Card.Head>Title</Card.Head>
  <Card.Body>Content</Card.Body>
  <Card.Foot>Footer</Card.Foot>
</Card>

<Accordion type="single" collapsible>
  <Accordion.Item value="item-1">
    <Accordion.Trigger>Section 1</Accordion.Trigger>
    <Accordion.Panel>Content here</Accordion.Panel>
  </Accordion.Item>
</Accordion>

<Modal open={isOpen} onClose={close} aria-labelledby="title" showCloseButton>
  <Modal.Head id="title">Edit Item</Modal.Head>
  <Modal.Body>Content</Modal.Body>
  <Modal.Foot>
    <Button label="Cancel" variant="ghost" onClick={close} />
    <Button label="Save" theme="primary" onClick={save} />
  </Modal.Foot>
</Modal>

The asChild pattern

Some components accept asChild to merge props onto your own element instead of wrapping:

{/* Without asChild — wraps in a span */}
<Tooltip.Trigger>
  <Button label="Hover" />
</Tooltip.Trigger>

{/* With asChild — props merged directly onto Button */}
<Tooltip.Trigger asChild>
  <Button label="Hover" />
</Tooltip.Trigger>

Prop conventions

Prop Purpose Values
theme Semantic colour primary, secondary, success, warning, danger, info
variant Visual treatment solid, outline, ghost, link (varies per component)
size Component scale sm, md, lg (varies per component)
onValueChange Value callback Used instead of onChange for non-native components
open / defaultOpen Floating state Controlled / uncontrolled for dropdowns, modals, etc.

Customising with tokens

Token architecture

TUI uses a four-layer token system:

  1. Primitives (--tui-*) — theme contract on .tui-interface (colours, spacing, radii)
  2. Component API (--tui-{component}-*) — never defined by TUI, only read via fallback. Set these from your code.
  3. Internal (--_*) — private to components, used for state management
  4. Structural ($_*) — SCSS-only, compile away
/* Override primitives globally */
.tui-interface {
  --tui-radius-md: 2px;
  --tui-focus-ring-color: hotpink;
}

/* Override a specific component's API token */
.my-context {
  --tui-button-radius: 0;
  --tui-accordion-padding: 8px;
  --tui-modal-spacing: 24px;
}

Each component's styles.scss header lists its full token API.

Colour scale

Theme colours use a contrast-based naming scale: subtlest → subtle → soft → base → strong → stronger → strongest. "Stronger" always means more contrast against the background, making it consistent across light and dark modes.

Form control sizing

All form controls (TextInput, Select, MultiSelect, Combobox) and buttons share unified height tokens:

.my-context .tui-interface {
  --tui-control-height-sm: 28px;
  --tui-control-height-md: 32px;
  --tui-control-height-lg: 40px;

  /* Optional: decouple font size from size tier */
  --tui-control-font-size-sm: 13px;
  --tui-control-font-size-md: 13px;
  --tui-control-font-size-lg: 14px;
}

Modal viewport offsets

Modal supports offset tokens for host chrome like the WordPress admin bar or sidebar:

.wp-admin .tui-modal {
  --tui-modal-offset-top: 32px;     /* admin bar */
  --tui-modal-offset-start: 160px;  /* sidebar */
}

Offset tokens fall back to env(safe-area-inset-*) for device notch/home indicator clearance.

Icons

Four icon sets via the registry: system, cred, reaction, player.

import { Icon } from '@tangible/ui';

<Icon name="system/check" />
<Icon name="system/settings" size="lg" />
<Icon name="reaction/clap-fill" />

Browse available icons in the Icon story.

CSS-only usage

Form inputs and some utilities work without React:

<div class="tui-interface">
  <input type="text" class="tui-input" placeholder="Text input" />

  <label class="tui-inline-choice">
    <input type="checkbox" />
    <span>Checkbox label</span>
  </label>

  <label class="tui-toggle">
    <input type="checkbox" id="toggle-1" />
    <label for="toggle-1">Toggle label</label>
  </label>
</div>

See the Inputs story for all available form patterns.