# Entangle UI — Full LLM Documentation
> Concatenated documentation for entangle-ui. Generated from the Astro docs source. Each section starts with `#
` and is separated by `---`.
=========================
# Overview
=========================
---
# Entangle UI
> React component library for professional editor interfaces
_Source: /index_
=========================
# Getting Started
=========================
---
# Installation
> How to install and set up Entangle UI in your project.
_Source: /getting-started/installation_
## Install Entangle UI
```bash
npm install entangle-ui@alpha
```
### Peer Dependencies
Entangle UI requires the following peer dependencies:
```bash
npm install react react-dom @base-ui/react @floating-ui/react
```
| Package | Version |
| -------------------- | ---------- |
| `react` | `>=19.1.0` |
| `react-dom` | `>=19.1.0` |
| `@base-ui/react` | `^1.1.0` |
| `@floating-ui/react` | `^0.27.17` |
:::note
Entangle UI is ESM-only (`"type": "module"`). Make sure your bundler supports ES modules.
:::
## Deep Imports
Color palettes are available via a dedicated export for better tree-shaking:
```tsx
import { MATERIAL_PALETTE, TAILWIND_PALETTE } from 'entangle-ui/palettes';
```
Available palettes: `MATERIAL_PALETTE`, `TAILWIND_PALETTE`, `PASTEL_PALETTE`, `EARTH_PALETTE`, `NEON_PALETTE`, `MONOCHROME_PALETTE`, `SKIN_TONES_PALETTE`, `VINTAGE_PALETTE`, `PROFESSIONAL_PALETTES`.
## TypeScript
All components export their prop types. Utility types are also available:
```tsx
import type {
Tokens,
ThemeVars,
Prettify,
DeepPartial,
LiteralUnion,
} from 'entangle-ui';
```
## Utilities
```tsx
import { cx, cn } from 'entangle-ui';
// cx — join class names, filtering out falsy values
;
```
## Next Steps
- [Quick Start](/getting-started/quick-start) — build your first component
- [Theming](/guides/theming) — customize the theme
---
# Introduction
> Entangle UI is a React component library for building professional editor interfaces — 3D tools, node editors, parameter systems, and more.
_Source: /getting-started/introduction_
Entangle UI is a specialized React component library for building **professional editor interfaces** — 3D tools, node editors, parameter systems, creative applications, and more.
## Why Entangle UI?
- **Editor-focused** — components designed for dense, information-rich UIs, not marketing pages
- **Dark-first** — all default values target dark backgrounds suitable for creative tools
- **Zero-runtime CSS** — powered by [Vanilla Extract](https://vanilla-extract.style/), styles compile to static CSS at build time
- **Type-safe** — full TypeScript support with strict types for all props and theme tokens
- **Tree-shakeable** — ESM-only, `preserveModules`, and `/*#__PURE__*/` annotations ensure only used code is bundled
## Component Catalog
### Primitives
Basic building blocks for any interface.
| Component | Description |
| ------------- | ----------------------------------------------------------------- |
| `Button` | Primary action trigger with `default`, `ghost`, `filled` variants |
| `IconButton` | Square button optimized for icon-only actions |
| `Input` | Text input with label, icons, error states |
| `Text` | Typography component with semantic variants |
| `Paper` | Surface container with elevation and nesting |
| `Icon` | SVG icon wrapper with consistent sizing |
| `Checkbox` | Toggle control with indeterminate state |
| `Switch` | Binary toggle switch |
| `Tooltip` | Accessible hover/focus tooltip |
| `Popover` | Floating content anchored to a trigger |
| `Collapsible` | Expandable/collapsible content section |
### Layout
Composable layout primitives with responsive breakpoints.
| Component | Description |
| -------------- | ---------------------------------------------------- |
| `Stack` | Flex-based stacking with spacing multiplier |
| `Flex` | Full flexbox control with responsive direction |
| `Grid` | CSS Grid layout |
| `Spacer` | Empty spacing element |
| `ScrollArea` | Custom-styled scrollbar container |
| `SplitPane` | Resizable split panel |
| `Accordion` | Multiple collapsible sections with single/multi mode |
| `PanelSurface` | Panel with header/body/footer slots |
### Controls
Advanced interactive controls for editor workflows.
| Component | Description |
| ----------------- | ----------------------------------------------- |
| `Slider` | Value slider with keyboard and modifier support |
| `NumberInput` | Numeric input with increment/decrement |
| `Select` | Dropdown with groups and search |
| `VectorInput` | Multi-axis numeric input (2D/3D vectors) |
| `ColorPicker` | Full color picker with modes and palettes |
| `CartesianPicker` | 2D coordinate picker |
| `CurveEditor` | Bezier curve editor with keyframe manipulation |
| `TreeView` | Hierarchical data with expand/collapse |
### Navigation
| Component | Description |
| ------------- | ------------------------------------------------------------------------- |
| `Menu` | Composable dropdown menu (`Menu.Trigger`, `Menu.Content`, `Menu.Item`, …) |
| `ContextMenu` | Right-click context menu |
| `Tabs` | Tab navigation with `TabList`, `Tab`, `TabPanel` |
### Feedback
| Component | Description |
| --------------- | ------------------------------------ |
| `Dialog` | Modal dialog with header/body/footer |
| `ToastProvider` | Toast notification system |
| `useToast` | Hook to show toasts programmatically |
### Shell (Application Layout)
Full-application layout components designed for professional editor interfaces.
| Component | Description |
| --------------- | -------------------------------------------------------------- |
| `AppShell` | Root layout with `.MenuBar`, `.Toolbar`, `.Dock`, `.StatusBar` |
| `MenuBar` | Top-level application menu bar |
| `Toolbar` | Action toolbar with buttons, toggles, groups |
| `StatusBar` | Bottom information bar |
| `FloatingPanel` | Draggable, resizable floating window |
### Editor
| Component | Description |
| ------------------- | ------------------------------------- |
| `ChatPanel` | AI chat interface panel |
| `PropertyInspector` | Inspector panel for object properties |
| `ViewportGizmo` | 3D viewport orientation gizmo |
## Next Steps
- [Installation](/getting-started/installation) — install Entangle UI and its peer dependencies
- [Quick Start](/getting-started/quick-start) — build your first component in minutes
- [Theming](/guides/theming) — customize colors, tokens, and create branded themes
- [Styling](/guides/styling) — learn the Vanilla Extract styling system
- [Docs for LLMs](/llms-txt) — plain-text documentation for AI coding assistants
---
# Quick Start
> Get up and running with Entangle UI in under five minutes.
_Source: /getting-started/quick-start_
Get up and running with Entangle UI in under five minutes.
## Basic Setup
Import the dark theme CSS to register all `--etui-*` CSS custom properties on `:root`. No wrapper component is needed for the default look:
```tsx
import 'entangle-ui/theme';
import { Button } from 'entangle-ui';
function App() {
return ;
}
```
The default dark theme is applied automatically — no extra configuration required.
### With Custom Overrides
Override tokens with plain CSS on any selector:
```css
.my-theme {
--etui-color-accent-primary: #2aa1ff;
--etui-color-bg-primary: #0d1117;
}
```
Or use `VanillaThemeProvider` for scoped overrides:
```tsx
import 'entangle-ui/theme';
import { VanillaThemeProvider } from 'entangle-ui';
function App() {
return (
);
}
```
## Your First Component
```tsx
import { Button, Stack, Text } from 'entangle-ui';
function Welcome() {
return (
Welcome to Entangle UI
);
}
```
## Common Patterns
### Sizing
Most components support a consistent `size` prop:
```tsx
{/* default */}
```
| Size | Typical Height |
| ---- | -------------- |
| `sm` | 20px |
| `md` | 24px |
| `lg` | 32px |
### Responsive Layout
`Stack` and `Flex` support responsive direction changes at four breakpoints:
```tsx
```
| Breakpoint | Width |
| ---------- | ------ |
| `sm` | 576px |
| `md` | 768px |
| `lg` | 992px |
| `xl` | 1200px |
### Form Inputs
Combine `Input` with form components for labeled fields with validation:
```tsx
```
### Application Shell
Build a complete editor layout:
```tsx
import {
AppShell,
MenuBar,
Toolbar,
StatusBar,
useAppShell,
} from 'entangle-ui';
function Editor() {
const appShell = useAppShell();
return (
{/* menu items */}}>Save
}>Grid
{/* panels */}Ready
);
}
```
## Next Steps
- [Theming](/guides/theming) — customize colors, tokens, and create branded themes
- [Styling](/guides/styling) — learn the Vanilla Extract styling system
=========================
# Guides
=========================
---
# Accessibility
> How Entangle UI handles accessibility concerns shared by every component, including reduced-motion support, focus rings, and keyboard semantics.
_Source: /guides/accessibility_
Entangle UI is designed for professional editor interfaces — surfaces where users spend hours every day. Accessibility is treated as a baseline, not a feature: every primitive ships with the focus, ARIA, and motion behavior expected of a 1.0 component library. This page documents the cross-cutting policies; component-specific notes (keyboard maps, ARIA roles) live on each component's own page.
## Reduced motion
The library honors the user's OS-level [`prefers-reduced-motion: reduce`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion) media query everywhere it ships autonomous motion. The rule we apply, drawn from [WCAG 2.1 SC 2.3.3 — Animation from Interactions](https://www.w3.org/TR/WCAG21/#animation-from-interactions):
> Motion is for communication, not decoration. When the user asks the OS to reduce motion, every animation that does not carry meaning is disabled or replaced with a static visual.
### What is disabled under reduced motion
These are autonomous or transition-driven motion patterns. With `prefers-reduced-motion: reduce` enabled, they snap to the final state instead of animating:
- **Loading indicators** — `Spinner` (ring, pulse, dots), `Button` and `IconButton` loading spinners, `ProgressBar` indeterminate stripe and circular rotation
- **Skeleton placeholders** — pulse and wave shimmer
- **Chat typing indicator** — bouncing dot loop and pulse bar
- **Dialog mount/unmount** — overlay fade and panel scale-in/out
- **Toast** — slide-in entry and the auto-dismiss progress bar (the timer still runs; only the visual indicator is hidden)
- **Popover** — opacity + scale entrance
- **Tooltip** — opacity + scale entrance, regardless of the `animation` prop
- **Select / dropdown** — `scaleY` open animation
- **Accordion / Collapsible** — chevron rotation and `grid-template-rows` height transition
- **Switch** — thumb travel
- **Radio** — inner-dot scale-in
- **Checkbox** — check-mark scale + opacity transition
- **Avatar (`interactive`)** — hover scale
- **Slider** — thumb hover scale, value tooltip translate
- **Color picker presets / palette swatches** — hover scale
- **TreeView / PropertySection / Select / Accordion / Collapsible / ChatPanel tool-call** — chevron rotations
- **Defensive `transition: all` blocks** — Button, Checkbox box, IconButton, TextArea, Tabs, Select trigger, VectorInput, InputWrapper
### What is preserved under reduced motion
These are direct manipulation or non-motion changes — disabling them would break the UI rather than help:
- **Drag, scrub, gizmo rotation, color-area picking, slider drag** — interactive direct manipulation
- **Focus rings** — instant box-shadow, never animated
- **Hover color / background changes** — color is not motion in the WCAG sense
- **`opacity` transitions on focus rings or banners** — fade in/out without translation is not vestibular motion
### Authoring custom components
When you build a custom component on top of Entangle's tokens, follow the same pattern. For Vanilla Extract styles:
```ts
import { style, keyframes } from '@vanilla-extract/css';
import { vars } from 'entangle-ui/theme';
const slideIn = keyframes({
from: { opacity: 0, transform: 'translateY(8px)' },
to: { opacity: 1, transform: 'translateY(0)' },
});
export const banner = style({
animation: `${slideIn} ${vars.transitions.normal} ease-out`,
transition: `transform ${vars.transitions.fast}`,
'@media': {
'(prefers-reduced-motion: reduce)': {
animation: 'none',
transition: 'none',
},
},
});
```
For inline styles or imperative animation (`requestAnimationFrame`, FLIP, custom physics), gate the animation behind a `matchMedia` check:
```tsx
const prefersReducedMotion =
typeof window !== 'undefined' &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReducedMotion) {
setState(finalState); // snap
} else {
animateTo(finalState); // animate
}
```
The shared utilities exported from `entangle-ui` (`animSpin`, `animPulse`, `animBlink`, `animFadeIn`, `animWave`) already include the media query — prefer them over reinventing keyframes. See [Animations](/guides/animations) for the full list.
### Verifying behavior
In Chrome / Edge DevTools: open the Rendering panel (`Cmd/Ctrl + Shift + P` → "Show Rendering") and toggle **Emulate CSS media feature `prefers-reduced-motion: reduce`**. Walk through the affected components — every animation in the list above should stop or snap.
---
# Animations
> Shared keyframes and utility classes for spinners, pulses, blinks, and fade-ins. Each utility honors prefers-reduced-motion.
_Source: /guides/animations_
Entangle ships a small set of shared keyframes and utility classes so components don't redefine the same animations over and over. They are exposed as both raw keyframe references (for use inside your own Vanilla Extract recipes) and ready-to-apply class names. Every utility class respects `prefers-reduced-motion: reduce` by halting the animation.
## Import
```tsx
import {
animSpin,
animPulse,
animBlink,
animFadeIn,
spinKeyframe,
pulseKeyframe,
blinkKeyframe,
fadeInKeyframe,
} from 'entangle-ui';
```
## Utility classes
Apply directly to any element via `className`:
```tsx
import { animSpin } from 'entangle-ui';
;
```
| Utility | Animation | Typical use |
| ------------ | ------------------------------------------------ | ----------------------------------- |
| `animSpin` | 1s linear infinite rotate (0deg → 360deg) | Loading rings, refresh icons |
| `animPulse` | 1.5s ease-in-out infinite opacity (1 → 0.4 → 1) | Skeleton placeholders, soft pings |
| `animBlink` | 1s steps(1) infinite opacity toggle | Cursor blinks, attention prompts |
| `animFadeIn` | One-shot opacity 0 → 1, normal transition timing | Mount transitions, toast appearance |
When the user has `prefers-reduced-motion: reduce` enabled in their OS, every utility class above sets `animation: none` automatically.
## Keyframes for custom recipes
Use the exported keyframe references when you want to compose your own animation in a Vanilla Extract recipe:
```ts
// my-component.css.ts
import { style } from '@vanilla-extract/css';
import { spinKeyframe } from 'entangle-ui';
export const slowSpin = style({
animation: `${spinKeyframe} 4s linear infinite`,
});
```
This keeps the keyframe definition shared across the bundle (smaller CSS) and avoids drift from copy-pasted versions.
## Reduced motion
The `prefers-reduced-motion` media query is the source of truth — utility classes react to it automatically. If you build your own animation with the keyframes, opt into the same behavior:
```ts
import { style } from '@vanilla-extract/css';
import { pulseKeyframe } from 'entangle-ui';
export const myPulse = style({
animation: `${pulseKeyframe} 1.5s ease-in-out infinite`,
'@media': {
'(prefers-reduced-motion: reduce)': {
animation: 'none',
},
},
});
```
## Where they're used internally
| Component | Keyframe |
| ------------------- | --------------- |
| `Spinner` (`ring`) | `spinKeyframe` |
| `Spinner` (`pulse`) | `pulseKeyframe` |
| `Spinner` (`dots`) | `pulseKeyframe` |
`Button`'s built-in loading spinner and `ChatTypingIndicator` keep their own local keyframes for now and will migrate to these utilities in a follow-up patch — switch to the shared utilities first when authoring new components.
---
# Styling
> Learn how to style Entangle UI components using Vanilla Extract and theme tokens.
_Source: /guides/styling_
Entangle UI uses **Vanilla Extract** for all styling — a zero-runtime CSS-in-JS framework where styles are written in `.css.ts` files and compiled to static CSS at build time.
## Style Files
Every component has its own `.css.ts` file:
```
Button/
├── Button.tsx
├── Button.css.ts ← styles
├── Button.test.tsx
└── index.ts
```
## Basic Styles
Use `style()` for atomic class names:
```typescript
// Card.css.ts
import { style } from '@vanilla-extract/css';
import { vars } from '@/theme';
export const cardStyle = style({
background: vars.colors.surface.default,
color: vars.colors.text.primary,
padding: vars.spacing.lg,
borderRadius: vars.borderRadius.md,
border: `1px solid ${vars.colors.border.default}`,
transition: `all ${vars.transitions.normal}`,
':hover': {
background: vars.colors.surface.hover,
boxShadow: vars.shadows.md,
},
':focus-visible': {
boxShadow: vars.shadows.focus,
},
});
```
```tsx
// Card.tsx
import { cardStyle } from './Card.css';
export const Card = ({ children }) => (
);
};
```
## Keyframe Animations
```typescript
import { keyframes, style } from '@vanilla-extract/css';
const spin = keyframes({
to: { transform: 'rotate(360deg)' },
});
export const spinnerStyle = style({
animation: `${spin} 1s linear infinite`,
});
```
## Global Styles and Media Queries
For responsive overrides, use `globalStyle()`:
```typescript
import { globalStyle } from '@vanilla-extract/css';
export const flexSelector = 'etui-flex';
globalStyle(`${flexSelector}[data-sm-dir="row"]`, {
'@media': {
'(min-width: 576px)': { flexDirection: 'row' },
},
});
```
Inline media queries within `style()`:
```typescript
const responsiveCard = style({
padding: vars.spacing.md,
'@media': {
'(min-width: 768px)': {
padding: vars.spacing.xl,
},
},
});
```
## Utilities
### `cx()` — Class Name Combiner
Joins class names, filtering out falsy values:
```typescript
import { cx } from '@/utils/cx';
function cx(...classes: (string | false | null | undefined)[]): string;
// Usage
```
Always place the recipe output first and user `className` last so consumer styles can override:
```tsx
```
### `cn()` — Alternative Class Combiner
Alias for `cx()` available for import preference:
```tsx
import { cn } from 'entangle-ui';
```
## Responsive Design
### Breakpoints
| Name | Width | Data Attribute |
| ---- | ------ | -------------- |
| `sm` | 576px | `data-sm-dir` |
| `md` | 768px | `data-md-dir` |
| `lg` | 992px | `data-lg-dir` |
| `xl` | 1200px | `data-xl-dir` |
### Responsive Direction (Stack / Flex)
Layout components accept responsive direction props:
```tsx
```
This renders as a column on mobile and switches to a row at `768px`.
### Spacing Multiplier
`Stack` and `Flex` use a 4px base unit for the `spacing` prop:
| `spacing` value | Computed gap |
| --------------- | ------------ |
| `0` | 0px |
| `1` | 4px |
| `2` | 8px |
| `3` | 12px |
| `4` | 16px |
| `5` | 20px |
| `6` | 24px |
| `7` | 28px |
| `8` | 32px |
Use `customGap` for arbitrary values:
```tsx
{/* ... */}
```
## Conventions
### Theme Tokens Over Hardcoded Values
```typescript
// Good
background: vars.colors.surface.default,
padding: vars.spacing.md,
// Bad
background: '#2d2d2d',
padding: '8px',
```
### Path Aliases
Always use `@/` imports for cross-directory references:
```typescript
// Good
import { vars } from '@/theme';
import type { Prettify } from '@/types/utilities';
// Bad
import { vars } from '../../theme';
```
### Recipe Naming
Recipe exports follow the pattern `{componentName}Recipe`:
```typescript
export const buttonRecipe = recipe({ ... });
export const textRecipe = recipe({ ... });
export const sliderTrackRecipe = recipe({ ... });
```
Standalone styles use `{name}Style`:
```typescript
export const iconWrapperStyle = style({ ... });
export const separatorStyle = style({ ... });
```
## Build Integration
### For Library Consumers
Entangle UI ships pre-compiled CSS. No Vanilla Extract build plugin is needed in consumer projects — just import and use:
```tsx
import { Button } from 'entangle-ui';
;
```
### For Library Contributors
The build uses Rollup with the `@vanilla-extract/rollup-plugin` to compile `.css.ts` files. Vitest uses `@vanilla-extract/vite-plugin` to compile styles for the jsdom test environment.
| Task | Command |
| --------------- | -------------------- |
| Dev (docs site) | `npm run dev` |
| Build | `npm run build` |
| Type check | `npm run type-check` |
### Tree-Shaking
The library is fully tree-shakeable:
- `sideEffects: false` in `package.json`
- `preserveModules` in Rollup output
- `/*#__PURE__*/` annotations where needed
Unused components and their styles are eliminated by the consumer's bundler.
---
# Theming
> Learn how to customize and extend the Entangle UI theme system — colors, spacing, typography, shadows, and more.
_Source: /guides/theming_
Entangle UI ships with a dark-first theme designed for professional editor interfaces. Every visual value — colors, spacing, typography, shadows — is defined as a **theme token** and exposed as a CSS custom property prefixed with `--etui-`.
The library also ships a maintained **light theme preset** for use in agent UIs, dashboards, and customer-facing surfaces — opt in via `createLightTheme()`.
## Architecture Overview
The theme system is built on CSS custom properties:
```
CSS Custom Properties (--etui-*)
Applied at :root by default
┌───────────────────────────────────┐
│ contract.css.ts (property names) │
│ darkTheme.css.ts (values) │
└──────────────┬────────────────────┘
│
┌────────────────────┼────────────────────┐
│ │
Vanilla Extract CSS Override
(compile-time) (any selector)
import { vars } --etui-*: value
```
- **Vanilla Extract** — tokens compile to CSS custom properties at build time. Zero runtime cost. Access via `vars` object.
- **CSS Override** — any `--etui-*` custom property can be overridden with plain CSS on any selector.
## Default Theme
The dark theme is applied globally on `:root` at build time. No provider is needed for the default look:
```tsx
import { Button } from 'entangle-ui';
// Works out of the box — dark theme is already active
;
```
## Light Theme
The light theme is shipped as a build-time preset. It is **not** applied globally — consumers opt in by generating a class with `createLightTheme()` and applying it to a subtree (typically the entire app, but it can also scope to a single dialog or panel).
### Quick start
The light-theme class is pre-generated at the package's build time, so it's a plain string you can import from anywhere — no consumer-side `.css.ts` file required:
```tsx
// app/Root.tsx
import { VanillaThemeProvider, lightThemeClass } from 'entangle-ui';
export function Root() {
return (
);
}
```
If you prefer a function-style API, `createLightTheme()` returns the same string and is interchangeable:
```tsx
import { VanillaThemeProvider, createLightTheme } from 'entangle-ui';
const lightThemeClass = createLightTheme();
;
```
The class flips every `--etui-*` custom property under the wrapped subtree to the light preset.
### What flips and what stays
The light theme only changes **visual** tokens (colors, shadows). Structural tokens — spacing, typography, border-radius, transitions, z-index — are identical between the two themes, so layout and rhythm don't drift when a user switches modes.
A few notes worth flagging:
- `colors.surface.whiteOverlay` keeps its name (the contract is part of the public API), but its value flips polarity in light mode — it becomes a subtle dark overlay (`rgba(0, 0, 0, 0.06)`) so it still reads as "subtle tint over the underlying surface".
- `colors.backdrop` uses lower opacity in light mode (`0.4` vs dark's `0.6`) — a heavier dim feels harsh against bright underlying content.
- Shadows use lower opacity and softer spread to remain visible on white surfaces without looking heavy.
- `shell.statusBar` keeps a solid accent background in both themes — it's a deliberately punchy element regardless of mode.
### Common components
### Property panels
### Alerts
### Scoped overrides — light dialog inside a dark app
`VanillaThemeProvider` is just a `
```
### 2. createCustomTheme (Build-Time)
Create a scoped theme that compiles to CSS at build time. This **must** be called in a `.css.ts` file. Use it for brand palettes or full custom themes — for the standard light preset prefer [`createLightTheme()`](#light-theme).
```typescript
// src/themes/brand.css.ts
import { createCustomTheme } from 'entangle-ui';
createCustomTheme('[data-theme="brand"]', {
colors: {
accent: {
primary: '#ff6600',
secondary: '#cc5200',
},
border: {
focus: '#ff6600',
},
},
});
```
Apply it:
```tsx
```
### 3. VanillaThemeProvider (Scoped Override)
Wrap a section with a className that carries theme overrides:
```tsx
import { VanillaThemeProvider } from 'entangle-ui';
;
```
## Accessing Tokens in Code
### Vanilla Extract (`.css.ts` files)
```typescript
import { vars } from 'entangle-ui';
import { style } from '@vanilla-extract/css';
export const card = style({
background: vars.colors.surface.default,
color: vars.colors.text.primary,
padding: vars.spacing.lg,
borderRadius: vars.borderRadius.md,
boxShadow: vars.shadows.md,
transition: `all ${vars.transitions.normal}`,
':hover': {
background: vars.colors.surface.hover,
boxShadow: vars.shadows.lg,
},
});
```
`vars` is fully typed — autocomplete guides you through the entire token tree.
### Plain CSS
```css
.custom-panel {
background: var(--etui-color-bg-secondary);
padding: var(--etui-spacing-lg);
border: 1px solid var(--etui-color-border-default);
border-radius: var(--etui-radius-md);
}
```
## ThemeProvider
Most apps don't need a provider — the dark theme is applied globally to `:root`. Use `ThemeProvider` for opt-in global behaviors.
```tsx
import { ThemeProvider } from 'entangle-ui';
;
```
### Global scrollbars (opt-in)
Set `globalScrollbars` to apply Entangle's dark-theme scrollbar styling to native scrollbars on overflowing elements throughout the document. The provider toggles a class on `document.body`; styles are scoped under that class so they don't leak when the prop is `false` (the default).
| Prop | Type | Default | Description |
| ------------------ | --------- | ------- | ----------------------------------------------------------------------------------- |
| `globalScrollbars` | `boolean` | `false` | Add a class to `document.body` that themes WebKit and Firefox scrollbars dark-thin. |
You can also opt in manually by adding the published class to your own root element:
```tsx
import { GLOBAL_SCROLLBARS_CLASS } from 'entangle-ui';
document.body.classList.add(GLOBAL_SCROLLBARS_CLASS);
```
This is helpful when Entangle is embedded inside another app shell that already manages mount/unmount of the provider.
## Token Export
Entangle UI ships machine-readable copies of every token next to the published
JS bundle, so design tools and non–Vanilla-Extract projects can consume the
same values the components compile against. Three artifacts are emitted on
each release:
| Artifact | Format | Use case |
| ------------------ | --------------------------- | -------------------------------------------------------------- |
| `tokens.json` | Loosely DTCG-aligned JSON | Figma plugins, Style Dictionary pipelines, doc generators |
| `tokens.dark.css` | `:root { --etui-*: ... }` | Drop-in dark stylesheet for projects not using Vanilla Extract |
| `tokens.light.css` | `.etui-theme-light { ... }` | Companion light stylesheet — apply the class to a wrapper |
### Plain CSS consumers
Import the stylesheet directly in projects that don't use Vanilla Extract.
Once it's loaded, every `--etui-*` custom property is available as a normal
CSS variable:
```css
@import 'entangle-ui/tokens.dark.css';
.my-button {
background: var(--etui-color-accent-primary);
padding: var(--etui-spacing-md);
border-radius: var(--etui-radius-md);
transition: var(--etui-transition-normal);
}
```
For the light preset, import the matching stylesheet and apply the
`etui-theme-light` class to the subtree you want to theme:
```html
...
```
> `tokens.light.css` uses the documented class name `etui-theme-light`. This
> is independent from the hashed runtime class returned by `createLightTheme()`
> — the runtime class is for Vanilla Extract consumers, the documented class
> is for plain-CSS consumers.
### JSON consumers (Figma plugins, design tools)
`tokens.json` follows the W3C Design Tokens Community Group structure where it
fits, with `$value` / `$type` leaves grouped under each theme:
```ts
import tokens from 'entangle-ui/tokens.json' with { type: 'json' };
console.log(tokens.themes.dark.colors.accent.primary.$value);
// → '#007acc'
console.log(tokens.themes.light.spacing.md.$type);
// → 'dimension'
```
Top-level structure:
```json
{
"$schema": "https://design-tokens.org/schema.json",
"$version": "",
"themes": {
"dark": { "colors": { ... }, "spacing": { ... }, "shell": { ... } },
"light": { "colors": { ... }, "spacing": { ... }, "shell": { ... } }
}
}
```
The `$version` field is read from `package.json` at export time, so each
published artifact carries the exact version it shipped with.
The export is loosely DTCG-aligned rather than spec-strict — the goal is
completeness over conformance, so the proprietary `shell.*` branch ships
alongside standard categories. Library-internal tokens under `demo.*`
(used only by the documentation site's editor demos) are intentionally
excluded.
### Node / SSR Data Consumers
Use `entangle-ui/theme-values` when you need the raw theme objects in a Node
script, server-only route, PDF/email generator, or any other environment that
does not process CSS imports. This entrypoint contains only plain TypeScript
data modules and does not import Vanilla Extract CSS files:
```ts
import {
darkThemeValues,
lightThemeValues,
themeContractData,
} from 'entangle-ui/theme-values';
console.log(lightThemeValues.colors.accent.primary);
// -> '#0066cc'
console.log(themeContractData.colors.accent.primary);
// -> 'etui-color-accent-primary'
```
Use `entangle-ui/theme` for React UI code that needs `ThemeProvider`, `vars`,
`createLightTheme()`, or `createCustomTheme()`. That entrypoint intentionally
loads the CSS runtime and is meant for bundled app code.
## TypeScript Types
```tsx
import type {
Tokens, // Raw token definitions
ThemeVars, // Vanilla Extract contract type (CSS variable references)
DarkThemeValues, // Dark theme value map
LightThemeValues, // Light theme value map (matches DarkThemeValues shape)
ThemeValues, // Alias for DarkThemeValues (used in createCustomTheme)
DeepPartial, // Recursive Partial for theme overrides
} from 'entangle-ui';
```
For CSS-free theme data imports, use:
```ts
import type {
DarkThemeValues,
LightThemeValues,
ThemeContractData,
ThemeValues,
} from 'entangle-ui/theme-values';
```
## Design Principles
- **Dark-first** — all default values target dark backgrounds suitable for 3D editors, node editors, and creative tools.
- **CSS-native** — tokens are CSS custom properties. Override them with CSS, no JavaScript runtime needed.
- **Type-safe** — `vars` and `Theme` provide full autocomplete and compile-time checks.
- **Scoped overrides** — custom themes can target any CSS selector for sectional theming.
- **Stable API** — CSS property names (`--etui-*`) are considered public API. Renaming them requires a major version bump.
=========================
# Components — Primitives
=========================
---
# Avatar
> Identity primitive with image, initials, and icon fallback chain. AvatarGroup handles overlapping clusters with overflow.
_Source: /components/primitives/avatar_
Render a person, agent, or any named entity. `Avatar` resolves an image when one is available, falls back to initials derived from the name, and finally to a generic user glyph. `AvatarGroup` overlaps multiple avatars and collapses overflow into a `+N` indicator.
Reach for `Avatar` to represent identities — collaborators, comment authors, agent attributions. For decorative imagery (thumbnails, hero photos) use a plain ``; an avatar isn't the right semantic.
**Live Preview**
## Import
```tsx
import { Avatar, AvatarGroup } from 'entangle-ui';
```
## Usage
```tsx
```
When `src` is provided the avatar attempts to load it; the underlying initials remain in place as a backdrop until the image paints (or fails). If it fails, the initials become the visible content.
## Sizes
Six fixed sizes from `xs` (toolbar / chip) through `xxl` (profile header).
**Sizes**
| Size | Diameter | Use case |
| ----- | -------- | ------------------------------ |
| `xs` | 16px | Inline next to dense rows |
| `sm` | 20px | Compact lists / chat bubbles |
| `md` | 24px | Default |
| `lg` | 32px | Side panels, comments |
| `xl` | 40px | Cards, member rows |
| `xxl` | 56px | Profile pages / detail headers |
```tsx
```
## Shapes
**Shapes**
| Shape | Border radius | When to use |
| --------- | ----------------- | ----------------------------------- |
| `circle` | `50%` | Default — people / personal context |
| `square` | `0` | Logos, machine identifiers |
| `rounded` | `borderRadius.md` | Soft fallback between the two |
```tsx
```
## Initials
Without a `src`, the avatar derives initials from `name`. A single word produces its first two characters; multi-word names produce the first character of the first and last word. With `color="auto"` (default) the background colour is hashed deterministically from the name — same name, same colour, every render.
**Initials**
```tsx
{/* "SK" */}
{/* "AL" */}
{/* "MS" */}
{/* explicit override */}
```
To force a specific accent, pass `color="primary"` (or `success`, `warning`, `error`, `info`, `neutral`). Any other string is treated as a raw CSS colour.
```tsx
```
## Icon fallback
When neither `src` nor `name`/`initials` resolves, a generic user glyph renders. Override it with `fallbackIcon`.
**Icon fallback**
```tsx
} />
```
## Statuses
Add a presence indicator in the bottom-right corner via the `status` prop.
**Statuses**
| Status | Colour token |
| --------- | ----------------------- |
| `online` | `colors.accent.success` |
| `away` | `colors.accent.warning` |
| `busy` | `colors.accent.error` |
| `offline` | `colors.text.muted` |
```tsx
```
## Fallback chain
The component renders both the image and the fallback in the same DOM, with the image positioned over the fallback. As soon as the image loads it paints over the initials; if it errors, the image is hidden via `data-loaded="false"` and the initials become visible without a layout shift.
**Broken image**
In order:
1. **Image** — `src` provided and loads successfully
2. **Initials** — derived from `name` (or set explicitly via `initials`)
3. **Icon** — `fallbackIcon` if provided, otherwise a generic user glyph
## Image source recommendations
- Square aspect ratio. The `` is rendered with `object-fit: cover`, so non-square sources crop to centre.
- Render at 2× the avatar's pixel size for crisp results on HiDPI displays — for a 32px avatar, a 64×64 source.
- Prefer modern formats (WebP / AVIF). Avatars sit in a container with `overflow: hidden`, so transparency is preserved when the format supports it.
- For user-uploaded images, consider running them through a face-aware crop service (Unsplash, Cloudinary, your own pipeline) before passing the URL to ``.
## Clickable
Provide `onClick` to make the avatar interactive — focusable, with a hover affordance and Enter/Space activation.
**Clickable**
```tsx
openProfile('alice')}
/>
```
## AvatarGroup
Display multiple avatars as an overlapping cluster.
**AvatarGroup**
```tsx
```
`size` and `bordered` propagate to every child — the group always wins so the cluster reads uniformly. The default `bordered={true}` adds a 2px ring matching the surrounding background; the leftmost avatar stacks on top via descending z-index.
## AvatarGroup overflow
When the number of children exceeds `max`, the remainder collapses into a `+N` indicator. Hover it to reveal the names of the hidden avatars.
**AvatarGroup overflow**
```tsx
{/* 12 avatars — 4 visible, "+8" overflow */}
```
Disable the tooltip with `showOverflowTooltip={false}` when the names aren't useful in context (e.g. when none of the avatars carry a `name` prop).
### Group sizes
**Group sizes**
### Editor example
A collaborators row in a project header.
**Editor example**
## Accessibility
- The accessible name resolves in this order: `aria-label` → `name` → `alt`. Always supply at least one of these.
- The image's `alt` attribute defaults to `name` (then the empty string), so when an image fails the underlying initials read with the supplied name.
- The status dot has its own `aria-label` (`"Status: Online"` etc.) so a screen reader reaches it independently of the avatar's name.
- When `onClick` is provided, the avatar gets `role="button"` and `tabIndex={0}`, and Enter / Space trigger the handler.
- The fallback glyph is marked `aria-hidden="true"` once an image successfully loads so it doesn't double-announce.
## API Reference
### Avatar
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `src` | `string` | — | Image source URL. Falls back to initials, then to an icon, on error. |
| `alt` | `string` | — | Image alt text and accessible name fallback. |
| `name` | `string` | — | Display name. Drives initials and the auto color hash. |
| `initials` | `string` | — | Manual initials override. Truncated to two characters and uppercased. |
| `fallbackIcon` | `ReactNode` | — | Icon shown when neither image nor initials resolve. |
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'xxl'` | `'md'` | Diameter (16 / 20 / 24 / 32 / 40 / 56 px). |
| `shape` | `'circle' \| 'square' \| 'rounded'` | `'circle'` | Border radius treatment. |
| `color` | `'auto' \| 'neutral' \| 'primary' \| 'success' \| 'warning' \| 'error' \| 'info' \| string` | `'auto'` | Background colour for the initials / icon surface. |
| `status` | `'online' \| 'away' \| 'busy' \| 'offline'` | — | Optional presence indicator in the bottom-right corner. |
| `onClick` | `(event) => void` | — | When provided, the avatar becomes a focusable button with Enter/Space activation. |
| `bordered` | `boolean` | `false` | Render a 2px ring matching the surrounding background. |
| `className` | `string` | — | Additional CSS class names. |
| `style` | `CSSProperties` | — | Inline styles. |
| `testId` | `string` | — | Test identifier for automated testing. |
| `ref` | `Ref` | — | Ref to the avatar root element. |
### AvatarGroup
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `max` | `number` | `4` | Maximum number of visible avatars before the +N indicator appears. |
| `spacing` | `number \| string` | `-8` | Spacing between avatars. Negative values overlap. Numbers are pixels. |
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'xxl'` | `'md'` | Size applied to every child avatar. |
| `bordered` | `boolean` | `true` | Force a border on every child, separating overlapping avatars. |
| `showOverflowTooltip` | `boolean` | `true` | Show a tooltip listing hidden avatar names when the +N indicator is visible. |
| `children` *(required)* | `ReactNode` | — | Avatar children. |
| `className` | `string` | — | Additional CSS class names. |
| `style` | `CSSProperties` | — | Inline styles. |
| `testId` | `string` | — | Test identifier for automated testing. |
| `ref` | `Ref` | — | Ref to the group root element. |
---
# Badge
> Inline status indicator and tag primitive with subtle, solid, outline, and dot variants.
_Source: /components/primitives/badge_
A small inline indicator for status, counts, or tags. Designed for editor density: short label, semibold weight, optional uppercase. Supports semantic color tokens or arbitrary CSS color strings, an optional leading icon, and a removable variant for chip-like usage.
**Live Preview**
## Import
```tsx
import { Badge } from 'entangle-ui';
```
## Usage
```tsx
SavedDraftOffline
```
## Variants
The `variant` prop controls the visual treatment.
### Subtle
Tinted translucent fill with colored text — the default. Reads as a label without competing with surrounding UI.
**subtle**
```tsx
Success
```
### Solid
Saturated fill with contrasting text. Use sparingly for high-visibility status.
**solid**
```tsx
Primary
```
### Outline
Transparent background with a colored border. Good for secondary chips that share row space with subtle badges.
**outline**
```tsx
Warning
```
### Dot
A small filled circle followed by the label. Use for online/offline or health indicators.
**dot**
```tsx
Online
```
## Sizes
| Size | Height | Use case |
| ---- | ------ | ------------------------------- |
| `xs` | 14px | Inline counters in dense rows |
| `sm` | 16px | Default — paired with body text |
| `md` | 20px | Standalone status pills |
| `lg` | 24px | Prominent labels in toolbars |
**Sizes**
```tsx
3DefaultStatusPill
```
## Colors
Named colors map to theme accent tokens. Any other string is treated as a raw CSS color (hex, `rgb()`, `hsl()`, …).
| Name | Maps to |
| --------- | ----------------------- |
| `neutral` | `colors.text.muted` |
| `primary` | `colors.accent.primary` |
| `info` | `colors.accent.primary` |
| `success` | `colors.accent.success` |
| `warning` | `colors.accent.warning` |
| `error` | `colors.accent.error` |
### Custom CSS colors
Pass any CSS color string — hex, `rgb()`, `hsl()`, CSS variables — and the badge picks it up via a per-instance runtime variable.
**Custom colors**
```tsx
orange hexpurple solidteal rgb
```
## Uppercase
Auto-applies `text-transform: uppercase` and a subtle letter-spacing — matches the editor-UI tag convention.
**Uppercase**
```tsx
draft
```
## With Icon
The `icon` slot renders before the label. Pass any ReactNode — SVGs or Entangle's `Icon` wrappers are the common choice.
**With icon**
```tsx
}>Saved
}>Featured
}>Pro
```
## Removable
The `removable` prop renders a built-in close button. The badge body click is **not** treated as a remove action — use the dedicated button.
**Removable** — Click the × on any tag to remove it.
```tsx
const [tags, setTags] = useState([...]);
{tags.map(tag => (
setTags(prev => prev.filter(t => t !== tag))}
>
{tag}
))}
```
The remove button gets `aria-label="Remove"` automatically. Clicks on the remove button stop propagation so your own click handler on the badge body remains separate.
## In a List Row
Pair with `ListItem` (or a plain row) for status-tagged lists.
**In a list row**
```tsx
done}>
Bake meshes
running}>
Normalize UVs
```
## Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | — | Label content. |
| `variant` | `'subtle' \| 'solid' \| 'outline' \| 'dot'` | `'subtle'` | Visual style. |
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg'` | `'sm'` | Size scale. |
| `color` | `'neutral' \| 'primary' \| 'info' \| 'success' \| 'warning' \| 'error' \| string` | `'neutral'` | Semantic color name (mapped to theme accent) or any CSS color string. |
| `uppercase` | `boolean` | `false` | Auto-uppercase the label and apply a small letter-spacing. |
| `icon` | `ReactNode` | — | Icon rendered before the label. |
| `removable` | `boolean` | `false` | Render a remove (×) button after the label. |
| `onRemove` | `(event: MouseEvent) => void` | — | Called when the remove button is clicked. |
| `className` | `string` | — | Additional CSS class names. |
| `style` | `CSSProperties` | — | Inline styles. |
| `testId` | `string` | — | Test identifier for automated testing. |
| `ref` | `Ref` | — | Ref to the underlying span element. |
The component also accepts all standard HTML `` attributes.
## Accessibility
- Renders a semantic `` (inline)
- The remove button has `aria-label="Remove"` and stops click-propagation so the badge body click handler stays separate
- Decorative icons are marked `aria-hidden`; provide your own `aria-label` if the badge is the only label for an action
---
# Button
> Versatile button component for editor interfaces with multiple variants, sizes, and states.
_Source: /components/primitives/button_
Versatile button component optimized for professional editor interfaces. Supports multiple variants, sizes, loading states, and icons.
**Live Preview**
## Import
```tsx
import { Button } from 'entangle-ui';
```
## Usage
```tsx
```
## Variants
The `variant` prop controls the button's visual style.
**default** — Transparent with border, fills on hover. Use for secondary actions.
```tsx
```
**ghost** — No border, subtle hover state. Use for inline or low-emphasis actions.
```tsx
```
**filled** — Solid background with accent color. Use for primary actions.
```tsx
```
## Sizes
Sizes are optimized for editor interface density.
```tsx
```
| Size | Height | Use case |
| ---- | ------ | ----------------- |
| `sm` | 20px | Compact toolbars |
| `md` | 24px | Standard panels |
| `lg` | 32px | Prominent actions |
## With Icon
Pass an icon element via the `icon` prop. Icons should be 16x16px for optimal appearance.
```tsx
import { SaveIcon, PlayIcon } from 'entangle-ui';
}>Save
} variant="filled">Play
```
## Loading State
The `loading` prop shows a spinner and disables interaction. Use for async operations.
```tsx
```
## Full Width
The `fullWidth` prop makes the button span the full width of its container. Useful for form actions and modal buttons.
```tsx
```
## Disabled
```tsx
```
## Combining Props
```tsx
}
variant="filled"
size="lg"
loading={isSaving}
onClick={handleSave}
>
Save Project
```
## Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | — | Button content — text, icons, or other React elements. |
| `variant` | `'default' \| 'ghost' \| 'filled'` | `'default'` | Visual variant of the button. |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size variant optimized for editor interfaces. |
| `disabled` | `boolean` | `false` | Whether the button is disabled. |
| `loading` | `boolean` | `false` | Loading state — shows spinner and disables interaction. |
| `icon` | `ReactNode` | — | Icon element to display before text. Should be 16x16px. |
| `fullWidth` | `boolean` | `false` | Whether the button should take the full width of the container. |
| `onClick` | `(event: MouseEvent) => void` | — | Click event handler. |
| `className` | `string` | — | Additional CSS class names. |
| `style` | `CSSProperties` | — | Inline styles. |
| `testId` | `string` | — | Test identifier for automated testing. |
| `ref` | `Ref` | — | Ref to the underlying button element. |
The component also accepts all standard HTML `
}
>
Advanced configuration options
```
## Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `trigger` | `ReactNode` | — | Content shown in the collapsible header/trigger. |
| `children` | `ReactNode` | — | Content to render when expanded. |
| `open` | `boolean` | — | Whether the section is expanded (controlled mode). |
| `defaultOpen` | `boolean` | `false` | Default expanded state (uncontrolled mode). |
| `size` | `'sm' \| 'md' \| 'lg'` | `'sm'` | Size controlling header height, padding, and font size. |
| `indicator` | `ReactNode \| null` | — | Custom indicator icon. Defaults to a chevron. Set to null to hide. |
| `disabled` | `boolean` | `false` | Whether the collapsible is disabled. |
| `keepMounted` | `boolean` | `false` | Whether to keep content in the DOM when collapsed. |
| `onChange` | `(open: boolean) => void` | — | Callback when the open state changes. |
| `className` | `string` | — | Additional CSS class names. |
| `style` | `CSSProperties` | — | Inline styles. |
| `testId` | `string` | — | Test identifier for automated testing. |
| `ref` | `Ref` | — | Ref to the root div element. |
## Accessibility
- Trigger button uses `aria-expanded` to indicate current state
- Trigger is linked to content via `aria-controls` / `id`
- Content region uses `role="region"` with `aria-labelledby` pointing to the trigger
- `aria-disabled` is set when the collapsible is disabled
- Content is hidden with the `hidden` attribute when collapsed (unless `keepMounted` is false, in which case it is removed from the DOM)
- Keyboard accessible: Enter/Space to toggle, Tab to navigate
---
# HoverCard
> Hover- and focus-driven floating panel for previews, with safe-polygon cursor handover from trigger to content.
_Source: /components/primitives/hover-card_
A floating panel anchored to a trigger and shown on hover or keyboard focus. Use `HoverCard` for read-only previews — user avatars, file metadata, linked-node summaries — where the user wants more context without clicking. The cursor can travel from the trigger onto the content through a safe polygon, so the card stays open while the user reads it.
**Live Preview**
## When to use
- **HoverCard** — read-only preview that's nice to have, not critical. Opens on hover, dismisses when the cursor leaves. Don't put primary actions inside.
- **Popover** — interactive content (forms, menus, multi-step pickers) that needs an explicit click to open.
- **Tooltip** — single-line label for an icon or truncated text. Smaller surface, faster open.
`HoverCard` is the right pick when the preview is informational, the user might want to skim a few in a row, and a click would feel heavy.
## Import
```tsx
import { HoverCard } from 'entangle-ui';
```
The compound members come along automatically:
```tsx
......
```
## Usage
Wrap any single-element trigger and pair it with `HoverCard.Content`:
```tsx
@octocat
```
`HoverCard.Trigger` clones its single child and attaches the hover / focus listeners to it — pass exactly one React element.
**Basic**
## Placements
`placement` controls which side of the trigger the card opens on. The default is `bottom-start`. Each placement automatically flips when there's not enough room.
**Placements**
```tsx
............
```
The full set of placements (`'top' | 'top-start' | 'top-end' | 'right' | 'right-start' | 'right-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end'`) is shared with [Popover](/components/primitives/popover).
## Delays
`openDelay` and `closeDelay` (both in ms) tune how eager the card is to appear and how forgiving it is once the cursor leaves.
**Delays**
| Setting | Default | Use when |
| ------------------- | ------- | ------------------------------------------------------------ |
| `openDelay={0}` | — | Compact rows where users hover many items in succession. |
| `openDelay={400}` | default | Standard discovery feel — avoids flickering across labels. |
| `openDelay={800}` | — | Heavy content (avatars + stats + actions) on rarely-used UI. |
| `closeDelay={150}` | default | Enough cushion to move the cursor onto the card. |
| `closeDelay={300+}` | — | Interactive content the user is likely to read for a moment. |
```tsx
...
```
## Controlled
Pass `open` and `onOpenChange` to drive the card from external state — useful for tests, automation, or pairing the card with another UI surface.
**Controlled**
```tsx
const [open, setOpen] = useState(false);
Trigger...;
```
When `open` is controlled, hover and focus still fire `onOpenChange`, so you can intercept or ignore as needed.
## Interactive content
The safe polygon means buttons and links inside the card stay reachable — the cursor's path from the trigger keeps the card alive. Reach for `Popover` if the interaction is the point (forms, multi-step pickers).
**Interactive content**
```tsx
Profile previewSebastian Reyes
Senior Technical Artist
Follow
Message
```
Set `disableSafePolygon` if you want the card to close the moment the cursor leaves the trigger rect — useful when the content has no interactive children and you want a snappier dismiss.
## Disabled
`disabled` short-circuits open requests entirely — the card never appears regardless of hover or focus.
**Disabled**
```tsx
Won't preview...
```
## Accessibility
- Opens on **hover** and on keyboard **focus** — works without a pointing device.
- The trigger receives `aria-describedby` pointing at the content while open.
- Content renders with `role="tooltip"` so assistive tech announces it as supplementary information.
- `prefers-reduced-motion` users get the same transition timing — there's no large motion to suppress, just a small opacity fade.
- The card is not focus-trapped; keep primary interactions in surrounding UI (or reach for `Popover`).
## API Reference
### ``
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | — | Controlled open state. |
| `defaultOpen` | `boolean` | `false` | Default open state (uncontrolled). |
| `placement` | `'top' \| 'top-start' \| 'top-end' \| 'right' \| 'right-start' \| 'right-end' \| 'bottom' \| 'bottom-start' \| 'bottom-end' \| 'left' \| 'left-start' \| 'left-end'` | `'bottom-start'` | Side relative to the trigger. |
| `offset` | `number` | `8` | Distance in pixels from the trigger. |
| `openDelay` | `number` | `400` | Delay before opening on hover, in ms. |
| `closeDelay` | `number` | `150` | Delay before closing once the cursor leaves, in ms. |
| `portal` | `boolean` | `true` | Render the content in a portal. |
| `disableSafePolygon` | `boolean` | `false` | Disable the safe polygon that lets the cursor move from trigger onto content without closing. |
| `disabled` | `boolean` | `false` | Disable the entire HoverCard (never opens). |
| `onOpenChange` | `(open: boolean) => void` | — | Callback when the open state changes. |
| `children` | `ReactNode` | — | `HoverCard.Trigger` + `HoverCard.Content` children. |
### ``
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactElement` | — | Trigger element — must be a single React element. The hover and focus listeners are attached to it. |
### ``
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | — | Content rendered inside the floating panel. |
| `width` | `number \| string` | — | Width of the content. Number → px, string → CSS value. |
| `maxHeight` | `number \| string` | — | Maximum height with internal scroll. Number → px, string → CSS value. |
| `padding` | `'none' \| 'sm' \| 'md' \| 'lg'` | `'md'` | Padding inside the content. |
| `className` | `string` | — | Additional CSS class names. |
| `style` | `CSSProperties` | — | Inline styles. |
| `testId` | `string` | — | Test identifier for automated testing. |
| `ref` | `Ref` | — | Ref to the content element. |
---
# Icon
> SVG icon wrapper component with standardized sizing and color theming for editor interfaces.
_Source: /components/primitives/icon_
SVG icon wrapper component providing standardized sizing and color theming. Wraps icon SVG children in a consistent `
}
>
Save
```
## In Menu Item
**In menu item**
```tsx