# Tactile UI Components
Tactile UI is Planet's portable React control layer. It was extracted from the Admin Next button, switch, scrollbar, and tooltip work, but the components themselves do not depend on Admin Next, AntD, Radix, Tailwind, or `an-*` classes. The immediate goal is stable in-repo usage; the structure is intentionally close to something that can later be published as an npm package.
## Design Goals
- **Light tactile feel**: controls default to a white or themed surface, thin borders, and external shadow, matching the subtle depth of the Docs theme slider rather than large colored blocks or glow.
- **Portable styling**: classes use the `tui-*` prefix and styles live in `frontend/src/components/tactile-ui/styles.css`.
- **Low dependency surface**: components assume React and React DOM. Preset icons currently use `lucide-react`, and callers may also pass custom React nodes.
- **Theme friendly**: CSS variables expose the default styling surface. Planet pages adapt the library through theme variables and props.
- **Clear semantics**: unambiguous actions should prefer icon-only buttons with tooltips; strong-intent actions such as save, confirm, create, and run can keep text.
## Import
In this repository:
```tsx
import { TactileButton, TactileSwitch, ControlGroup } from '@/components/tactile-ui'
import '@/components/tactile-ui/styles.css'
```
After a future package extraction:
```tsx
import { TactileButton, TactileSwitch } from '@planet/tactile-ui'
import '@planet/tactile-ui/styles.css'
```
## Theme Tokens
Core tokens are exposed as `--tui-*` CSS variables. Product themes should override variables instead of rewriting internal component classes.
```css
:root {
--tui-surface: #ffffff;
--tui-surface-raised-hover: #f8fbff;
--tui-border-soft: #d6dfeb;
--tui-border-hover: #b8c6d9;
--tui-text: #0f172a;
--tui-primary: #2563eb;
--tui-danger: #dc2626;
}
[data-theme='dark'] {
--tui-surface: #111827;
--tui-text: #e5edf8;
}
```
Most controls also accept a `tactile` prop for local width, height, radius, background, border, and shadow overrides. Use local overrides for small special cases; use CSS variables for product-wide styling.
## `TactileButton`
The button component covers regular buttons, icon buttons, strong-intent buttons, and link-like buttons.
Common props:
| Prop | Description |
| --- | --- |
| `variant` | `neutral`, `primary`, `danger`, `subtle`, or `ghost` |
| `size` | `sm`, `md`, `lg`, or `icon` |
| `shape` | `square` or `pill` |
| `icon` | Preset icon name or a custom React node |
| `iconOnly` | Fixed-size icon button; provide `tooltip` or `aria-label` |
| `tooltip` | Rendered through a portal and offset away from the cursor |
| `loading` | Disables the button and exposes `aria-disabled` |
| `tactile` | Overrides size, radius, shadow, background, and dark-mode variables |
```tsx
Save
```
`variant="neutral"` defaults to a white tactile button. Colored buttons should still keep the same height and external shadow instead of relying on page-specific CSS overrides.
## Icon Presets
Preset icons are maintained in `tactileIconPresets`. Feature pages should call icons by semantic name so actions remain consistent across the console.
Common semantics:
| Name | Use |
| --- | --- |
| `refresh` | Refresh data |
| `save` | Save |
| `delete` | Delete |
| `trigger` / `collect` | Trigger collection |
| `start` / `play` | Start |
| `stop` | Stop |
| `connect` / `test` | Connectivity check |
| `guide` | Credential guide |
| `generate` | AI generation |
| `reset` | Restore defaults |
| `detail` | View details |
| `copy` | Copy |
| `upload` | Upload |
Custom icons are also supported:
```tsx
} iconOnly tooltip="Custom action" />
```
## `TactileSwitch`
`TactileSwitch` is for binary settings. It is not a large iOS-style switch; it matches the compact tactile slider used by Docs.
```tsx
```
The label defaults to tooltip content. If visible text is needed, render it in the surrounding layout rather than inside the switch.
## `ControlGroup`
`ControlGroup` arranges action buttons with consistent spacing and alignment. It has no grey base by default; use `withBase` or `baseTactile` only when a base surface is intentional.
```tsx
Save
```
In list footers, the group should appear at the end of the scroll content, not float in the middle of the list. In detail toolbars, size and shadow should come from the shared component defaults.
## `TactileTooltip`
Tooltips render to `document.body` via `createPortal`, support `top`, `right`, `bottom`, and `left`, and perform simple collision correction. The default position is offset down and to the right from the trigger so it does not sit under the cursor.
```tsx
```
Application code usually does not need to use it directly because `TactileButton` and `TactileSwitch` include tooltip handling.
## `Scrollbar`
`Scrollbar` wraps ordinary scrollable content. Native scrolling remains on the viewport; Tactile UI only renders the overlay track and thumb.
```tsx
```
Common props:
| Prop | Description |
| --- | --- |
| `axis` | `x`, `y`, or `both` |
| `autoHide` | Fade/soften when not hovered or dragged |
| `alwaysVisible` | Keep scrollbars visible |
| `trackSize` / `thumbSize` | Track and thumb dimensions |
| `inset` / `radius` | Edge inset and radius |
| `thumbColor` / `trackColor` | Local color overrides |
| `viewportRef` | External real scroll node |
Do not force overlay scrollbars onto textareas. Text selection and the resize grip need native behavior. If a consistent textarea look is needed, use a dedicated resizable textarea style that preserves the native grip hit area.
## `ScrollbarOverlay`
`ScrollbarOverlay` is for areas that already own a scroll node, such as a third-party table or custom viewport. It does not create a new scroll container; it observes the target and paints overlay scrollbars.
```tsx
```
## `TableScrollRegion`
`TableScrollRegion` is a convenience wrapper for table scroll areas. The default target selector is `.tui-scroll-target`; Admin Next passes its table viewport selector explicitly so package code does not contain product-specific names.
```tsx
```
## Accessibility
- `iconOnly` buttons must provide `tooltip` or `aria-label`.
- `TactileSwitch` uses `role="switch"` and `aria-checked`.
- Tooltip text is explanatory only; state must still be represented by text, badges, or `aria-*` attributes.
- Disabled and loading states set `aria-disabled`; real `button` elements also receive `disabled`.
## Admin Next Migration Rules
Admin Next should use Tactile UI for global tool buttons, detail-panel toolbars, and list footer actions:
- Unambiguous actions: `TactileButton iconOnly tooltip`
- Save/create/confirm: `TactileButton variant="primary"`, usually with text
- Delete/stop: `TactileButton variant="danger"`, with text only when risk or ambiguity requires it
- Switches: `TactileSwitch`
- Long lists, tables, logs, and Markdown: `Scrollbar` / `ScrollbarOverlay` / `TableScrollRegion`
The boundary is deliberate: Tactile UI owns touch feel, size, shadow, tooltip, and scrollbar behavior; feature pages own APIs, state machines, permissions, and copy.