# Echo UI > A neo-skeuomorphic React component library you install into your own source tree with `npx echo-ui-cli`. Molded caps in recessed wells, emissive readouts, real press travel. Motion (GSAP) and sound (zero-dep WebAudio synth) are first-class token families alongside color and type. > IMPORTANT: The Base UI package is `@base-ui/react` (per-component imports, e.g. `@base-ui/react/dialog`), NOT `@base-ui-components/react` (the old name). Training data may be stale. ## Accessibility Accessibility is part of the grammar, not a review step. Echo guarantees WCAG 2.2 AA, and these are the parts a consumer can still get wrong: - Every control needs a name. `Switch`, `Checkbox`, `Slider`, `Segmented`, `Select`, `RadioGroup` and `Progress` take `aria-label` (or `aria-labelledby` pointing at visible text); `Field`, `Textarea` and `OtpField` take a visible `label`. Without one they do not compile. - An icon-only `HardwareButton` (`iconOnly`) needs `aria-label`: it has no text to read. - Forms: put the help text in `description` and the problem in `error`, both on the field. They are linked to the input and set `aria-invalid` for you. Never signal an error with colour or a red border alone. - Announcements: mount `` once and call `toast()` from `useToast()`. A message rendered into the page when it happens is usually never announced. - Sign-in: `Field` with `autoComplete` (`email`, `current-password`), and `OtpField` for a one-time code — it allows pasting, which people rely on. - State is never colour alone: Echo has one accent, and its components already carry state in shape and words. Do not add a red for danger; use `AlertDialog`, which asks before anything destructive. - Do not remove a focus ring, shrink a target below 24×24px, or set `aria-hidden` on something focusable. ## Components - [Modal](https://echo-ui.com/e/modal.json): Base UI Dialog (focus trap, Esc, scroll-lock, a11y) skinned as an Echo device panel. motion.summon enter, GSAP exit that completes before unmount, sound.transition on open. - [Menu](https://echo-ui.com/e/menu.json): Base UI Menu (typeahead, roving focus, submenu timing, checkbox/radio items) skinned as an Echo floating panel — the Select's molded cap trigger and the same lit dot for what is on. sound.transition on open, sound.select on item commit. Exports ContextMenu for the right-click case. - [Sheet](https://echo-ui.com/e/sheet.json): A molded chassis panel that slides in from an edge — the console side panel and the mobile nav. Built on Base UI's Drawer, so it carries swipe-to-dismiss and a backdrop that tracks the swipe. sound.transition on open. - [AlertDialog](https://echo-ui.com/e/alert-dialog.json): A destructive confirm. Modal covers the structure; this is the semantics — role=alertdialog, a required description, and no dismissal by clicking away. sound.caution on open, sound.error on a destructive confirm. Severity is the deep end of the accent ramp, never an imported red. - [HardwareButton](https://echo-ui.com/e/hardware-button.json): The molded tactile button: a cap in a recessed well with real press travel + sound. primary/neutral/ghost, sm/md/lg, icon slots, icon-only, loading, engraved. Built on semantic vars + useTactile. - [Popover](https://echo-ui.com/e/popover.json): Base UI Popover (anchoring, focus, dismiss, a11y) skinned as an Echo panel. useGsapPresence summon-in / GSAP exit before unmount; sound.select on open. - [Tooltip](https://echo-ui.com/e/tooltip.json): Base UI Tooltip (hover/focus, delay, a11y) skinned as a compact molded chip. Same useGsapPresence bridge as Modal/Popover. - [Select](https://echo-ui.com/e/select.json): Base UI Select (typeahead, keyboard, a11y) skinned as an Echo dropdown — a molded cap trigger and a floating panel with a lit dot on the selected option. CSS fade/scale; sound.select on change. - [Command](https://echo-ui.com/e/command.json): The command palette as a dot-matrix readout — the query line is the LcdChip's recessed screen well with an emissive caret and a scanline wash. Base UI Autocomplete for the input and listbox semantics, Base UI Dialog for the frame. Cmd/Ctrl+K by default; matches hidden keywords as well as labels. - [Tabs](https://echo-ui.com/e/tabs.json): Base UI Tabs (roving focus, arrow-key nav, a11y) marked with the library's one "you are here": a 2px lit rail slides under the open tab, over a hairline baseline. Labels stay on the chassis. sound.select on change. Compound API. - [Accordion](https://echo-ui.com/e/accordion.json): Base UI Accordion (keyboard, a11y, height measurement) skinned as molded device panels that expand. Click sound on toggle; height animates via CSS. Compound API. - [Switch](https://echo-ui.com/e/switch.json): Base UI Switch (role=switch, keyboard, a11y) with a hardware-toggle skin — a molded thumb sliding in a recessed track — and a click sound on toggle. - [Checkbox](https://echo-ui.com/e/checkbox.json): Base UI Checkbox (keyboard, indeterminate, a11y) with a molded box skin that fills accent and shows a check when set. Select sound on change. - [Slider](https://echo-ui.com/e/slider.json): Base UI Slider (keyboard, drag, track-press, a11y) with a recessed rail + molded knob and a scrubbing tick sound on each step. - [Segmented](https://echo-ui.com/e/segmented.json): A hardware segmented selector — molded uppercase segments in a recessed well, the active one on the lit primary cap. A radiogroup value selector (roving focus, arrow-key select, select sound), distinct from Tabs which switches content panels. - [Field](https://echo-ui.com/e/field.json): A text input sunk into a recessed well. The inner edge lights in the accent on focus; a mono eyebrow label and an optional machine (mono) value voice. - [Textarea](https://echo-ui.com/e/textarea.json): Field's recessed well, multi-line and autosizing. The height is measured from the browser's own layout rather than counted from newlines, so it stays right when lines wrap and when the container resizes. - [Badge](https://echo-ui.com/e/badge.json): A small molded/LCD status label: neutral, accent, lcd, outline. Static; composes var(--echo-*). - [Kbd](https://echo-ui.com/e/kbd.json): A molded keycap for keyboard hints. Uses the element; composes var(--echo-*). - [Divider](https://echo-ui.com/e/divider.json): A panel seam — a hairline with a faint light lip. Horizontal or vertical; role=separator. - [StatTile](https://echo-ui.com/e/stat-tile.json): A recessed LCD readout: a glowing value over a machine label in a dark well. Static; composes var(--echo-*). - [Chart](https://echo-ui.com/e/chart.json): StatTile's dot-matrix language applied to a series: a column per sample, cells lit from the bottom, in the recessed screen well LcdChip and Progress use. Quantises to whole cells by construction — a readout, not a charting library. - [AppShell](https://echo-ui.com/e/app-shell.json): The app frame: a molded chassis rail, a sticky sidebar, and the content between them. Slots, not props. Ships the skip link by construction and derives the sidebar's sticky offset from the rail's height. - [Panel](https://echo-ui.com/e/panel.json): The two halves of the device material law as a container: raised molded plastic (things you touch) or a recessed dark screen (things you read). - [Progress](https://echo-ui.com/e/progress.json): A discrete LED cell meter in a recessed strip — filled cells are lit accent LEDs with a bloom. Steppy by construction; role=progressbar. - [Loader](https://echo-ui.com/e/loader.json): Three accent LEDs breathing in sequence — the device thinking. role=status; reduced motion holds the dots steady. - [StatusDot](https://echo-ui.com/e/status-dot.json): A tiny LED — lit accent with a bloom, or dead plastic gray. Decorative (aria-hidden) unless given a label. - [Toast](https://echo-ui.com/e/toast.json): A raised glass notification slab with a status LED — lit accent when active. Presentational (role=status); queueing stays in the consumer. - [LcdChip](https://echo-ui.com/e/lcd-chip.json): An inline recessed LCD readout — a glowing accent value beside a machine label, sunk into a dark well. The chip form of StatTile. - [LedKey](https://echo-ui.com/e/led-key.json): A molded pill key with an indicator LED that breathes while lit. The latching sibling of HardwareButton — for a key whose own state is part of the answer. - [EngravedPlate](https://echo-ui.com/e/engraved-plate.json): A paper-coloured metal plate with text cut into it and rivets at the edges — the one non-emissive readout. For a fact fixed to the device, where LcdChip is for a live value. - [Textures](https://echo-ui.com/e/textures.json): The grain law as composable overlays: Dither, Noise, Halftone, Scanlines, plus GrainField and Textured. Inline SVG tiles, no asset files; decorative and pointer-events-free. - [ShaderField](https://echo-ui.com/e/shader-field.json): The live brand field — a WebGL domain-warped fbm gradient sampled from the accent ramp, blooming toward the cursor. Reduced motion draws one static frame; no WebGL falls back to a CSS gradient. - [RadioGroup](https://echo-ui.com/e/radio-group.json): One choice from a small set, as molded selector caps with a pip that lights accent when chosen. Base UI owns roving focus and arrow-key selection. - [OTP field](https://echo-ui.com/e/otp-field.json): The one-time code a sign-in asks for, as a row of lit digit windows. Built on Base UI's OTP Field: paste fills the row, arrow keys and Backspace walk it, and autocomplete="one-time-code" lets a phone offer the code. - [NumberField](https://echo-ui.com/e/number-field.json): A machined readout between two molded step caps, with the label doubling as a drag-to-scrub handle. Base UI owns typing, arrow keys, step sizes, and clamping. - [Avatar](https://echo-ui.com/e/avatar.json): A person or device as a molded disc — image in a well, or initials engraved into the plastic when there is no image or it fails to load. - [Tag](https://echo-ui.com/e/tag.json): A label the user applied, optionally removable. Badge reports what the system knows; Tag is a small plate that can be peeled off. - [CountBadge](https://echo-ui.com/e/count-badge.json): A number worth noticing — the lit sibling of Badge. Disappears at zero, caps its face at max while still announcing the true count. - [Skeleton](https://echo-ui.com/e/skeleton.json): The shape of content that has not arrived: a recess with a slow sheen. aria-hidden by default; one label announces the wait for the whole region. - [Banner](https://echo-ui.com/e/banner.json): A persistent inline message strip for a condition that is still true. Severity by luminance and a tone pip, not by hues the seed does not own. - [EmptyState](https://echo-ui.com/e/empty-state.json): Nothing here, and what to do about it — a quiet recess with the way forward built into the API. - [Table](https://echo-ui.com/e/table.json): Rows of data in a recessed screen well. Real table semantics, a required caption, tabular figures for numbers, and a scroll region that keyboard users can reach. - [Breadcrumb](https://echo-ui.com/e/breadcrumb.json): Where you are in the hierarchy, as an engraved trail. Labelled nav around an ordered list; the current page is text with aria-current, not a link. - [Pagination](https://echo-ui.com/e/pagination.json): Molded number caps with the current page lit. The window never elides a single page and never changes width as you page through. - [Steps](https://echo-ui.com/e/steps.json): How far through a sequence you are, as LED stations on a wire. aria-current on the current station; the done state reaches screen readers as words, not colour. - [Auth screen](https://echo-ui.com/e/auth.json): Sign-in and its 2FA step as a front panel: a nameplate, recessed wells to type into, one lit cap to commit. Proves the first-press moment. - [Settings screen](https://echo-ui.com/e/settings.json): The entire control set at rest, as labelled rows. Proves that the control language coheres across fifteen controls and that the sound design survives rapid input. - [Console screen](https://echo-ui.com/e/console.json): A device dashboard as a front panel: AppShell, six StatTile readouts, an LED level matrix, a takes Table and Pagination. Proves density and data at scale. - [Chat screen](https://echo-ui.com/e/chat.json): One conversation as a front panel: Avatar + StatusDot header, a message log, a Loader typing indicator, and a Textarea composer with one lit Send cap. Proves the material language survives running text. - [Theme Engine](https://echo-ui.com/e/theme.json): createEchoTheme(seed) — OKLCH derivation turning a ~6-value seed into the full semantic-role set, with dual { css, values } output and WCAG-AA contrast guardrails. The token foundation every Echo component depends on. - [Sound Engine](https://echo-ui.com/e/sound.json): SoundProvider + zero-dep WebAudio SynthBackend. Sound tokens are serializable synth recipes; lazy AudioContext, persisted mute, per-play jitter, blur guard, and reduced-motion coupling. - [Motion Tokens](https://echo-ui.com/e/motion.json): GSAP motion tokens (duration + ease as serializable data), each with an optional reduced-motion collapse variant. Glass moves smooth; pixels move steppy. - [useTactile](https://echo-ui.com/e/use-tactile.json): The fusion primitive: binds motion (GSAP) + sound per interaction phase, with pointer/keyboard parity and prefers-reduced-motion collapse. Accepts token names or raw GSAP vars / sound recipes as an escape hatch. - [useGsapPresence](https://echo-ui.com/e/use-gsap-presence.json): The GSAP-exit bridge (implemented once, reused by every Base UI wrapper): controls open state + actionsRef so the exit animation completes before Base UI unmounts. Runs the enter on mount. ## Tokens - Color roles: --echo-surface, --echo-cap, --echo-cap-highlight, --echo-cap-shadow, --echo-well, --echo-accent, --echo-accent-highlight, --echo-accent-pressed, --echo-accent-glow, --echo-primary-cap-top, --echo-primary-cap-mid, --echo-primary-cap-base, --echo-accent-fill-top, --echo-accent-fill-base, --echo-lcd, --echo-lcd-glow, --echo-text, --echo-text-dim, --echo-seam - Sound: tap, press, release, select, transition, notify, caution, error, celebrate - Motion: press, pop, summon, breath, settle, glitch, flood, drain ## Setup Set the project up once with `npx echo-ui-cli init`. It installs `echo-ui-core`, writes `echo.json`, `echo-lock.json` and `echo-theme.css`, and prints the lines to paste. It never edits your files, and `add` refuses to run before it. ```sh npx echo-ui-cli init ``` Components render with no colour until the theme is on the page. `init` writes `echo-theme.css` into the Echo folder — import it once from your global CSS or your root layout (or call `createEchoTheme().css` from `echo-ui-core/theme` to build it at runtime). Give the page a dark ground (`body { background: var(--echo-well); color: var(--echo-text); }`), and wrap the app in `SoundProvider` from `echo-ui-core/sound` — without it every component is silent. Echo components are client components. In the Next.js App Router, a file that passes them a function (`onSave`, `onSubmit`, `onClick`) must start with `"use client"`. ## Install ```sh npx echo-ui-cli add ``` Echo is a REGISTRY, not an npm component package. Components are installed with `npx echo-ui-cli add ` and then live in the consumer's own source tree, importing each other by relative path inside one Echo folder. Never suggest `npm install` for a component. The one npm package is `echo-ui-core`, the grammar they all import (theme, sound, motion, tactile, presence). Cross-component dependencies resolve automatically. Consume only Layer-3 semantic vars (`var(--echo-*)`) in your own CSS; theme by passing a seed to `createEchoTheme`. ## Conformance Every component is tested against WCAG 2.2 AA: axe, keyboard, focus-visible, forced-colors, target-size, reflow — and each item's `/e/` record carries the same block. Not covered: screen-reader: not yet verified by a person. --- # Full component reference ## Modal (`echo-modal`) Base UI Dialog (focus trap, Esc, scroll-lock, a11y) skinned as an Echo device panel. Enter uses motion.summon; exit is a GSAP tween that completes before unmount via useGsapPresence + actionsRef. sound.transition on open. *Feel:* A device panel that rises out of a dimmed room, and sinks back the way it came. - Type: registry:component - npm deps: gsap, @base-ui/react, echo-ui-core - Install: `npx echo-ui-cli add modal` ### Usage ```tsx import { HardwareButton } from "@/echo/hardware-button"; import { Modal } from "@/echo/modal"; Settings} title="Settings" description="Tune the device." >

Body content…

``` ### Composition ```text Modal ``` ### Examples **Basic modal** — A trigger plus title/description/body. ```tsx Settings} title="Settings" description="Tune the device." >

Body content…

``` **With footer actions** — Footer for confirm/cancel. ```tsx Confirm} /> ``` ### Accessibility - Base UI Dialog: `role="dialog"` and `aria-modal`, named by `title` and described by `description`. - Focus moves into the panel, stays there while it is open, and returns to the trigger when it closes. The page behind cannot scroll. - Escape or a click on the backdrop closes it. ### Motion & sound - Motion tokens: `summon` - Reduced motion: The panel fades in and out without moving; the backdrop fades faster. - Sound: `transition` when it opens. ### Props | Prop | Type | Default | Notes | | --- | --- | --- | --- | | `trigger` | `ReactNode` | — | What opens the modal. An element (e.g. a HardwareButton) becomes the trigger itself; text is wrapped in a plain trigger. | | `title` | `ReactNode` | — | The dialog title — also its accessible name. Always pass one. | | `description` | `ReactNode` | — | One line under the title, announced with it. | | `footer` | `ReactNode` | — | Actions row under the body, e.g. confirm and cancel buttons. | | `size` | `"sm" | "md" | "lg"` | "md" | | | `showClose` | `boolean` | true | | | `defaultOpen` | `boolean` | false | | | `container` | `unknown` | — | Where the popup mounts; defaults to . Pass an element inside a scoped theme so the popup wears that theme, not the :root one. | ### Anti-patterns - Do not add a CSS transition to the popup — GSAP owns its transform/opacity; a CSS transition will fight it. - Do not skip actionsRef when animating exit with JS — Base UI can't detect GSAP and will unmount too early. - The package is `@base-ui/react`, not `@base-ui-components/react` (agents have stale training data). ## Menu (`echo-menu`) Base UI Menu (typeahead, roving focus, submenu timing, checkbox/radio item semantics) skinned as an Echo floating panel — the Select's molded cap trigger and the same lit dot for what is on. Two sounds: transition when the panel arrives, select when an item commits. Exports ContextMenu from the same item for the right-click case. Compound API. *Feel:* A short list that drops from its button; each choice clicks like a key. - Type: registry:component - npm deps: @base-ui/react, echo-ui-core - Install: `npx echo-ui-cli add menu` ### Usage ```tsx import { Menu } from "@/echo/menu"; Actions Rename Archive ``` ### Composition ```text Menu Menu.Trigger Menu.Content Menu.Item Menu.LinkItem Menu.CheckboxItem Menu.RadioGroup Menu.RadioItem Menu.Group Menu.GroupLabel Menu.Separator Menu.SubmenuTrigger ContextMenu ContextMenu.Content ``` ### Examples **Dropdown** — Content wraps Base UI's Portal, Positioner and Popup in one. ```tsx Actions Rename Duplicate Archive ``` **Your own trigger** — render swaps the default cap for any component, keeping the menu wiring. ```tsx Actions} /> ``` **Checkable items** — The lit dot marks what is on — the same indicator Select uses. ```tsx Show grid ``` **Right-click menu** — Same panel, same items; the Trigger is the region, not a button. ```tsx Split at playhead ``` ### Accessibility - Base UI Menu: the trigger is a button with `aria-haspopup`; the list is a `menu` of `menuitem`s, with `menuitemcheckbox` and `menuitemradio` for the marked items. - Arrow keys move through items, typing jumps to a match, Enter or Space activates, Escape closes and returns focus to the trigger. - `ContextMenu` opens on right-click and on long-press; give the same actions another way in too, since neither is discoverable. ### Motion & sound - Motion tokens: `--echo-duration-glide` - Reduced motion: The list appears with a short fade and does not slide. - Sound: `transition` when it opens. - Sound: `select` when an item is chosen. ### Props | Prop | Type | Default | Notes | | --- | --- | --- | --- | | `Content.side` | `"top" | "right" | "bottom" | "left"` | "bottom" | Preferred side of the trigger. Flips automatically when there is no room. | | `Content.align` | `"start" | "center" | "end"` | "start" | Alignment along that side. A menu hangs off its trigger, so start is the default. | | `Content.sideOffset` | `number` | 8 | Gap between trigger and panel, in px. | | `Content.container` | `unknown` | — | Where the panel mounts; defaults to . Pass an element inside a scoped theme so the panel wears that theme, not the :root one. | ### Anti-patterns - Do not use a Menu as navigation for a whole site — it hides every destination behind a press. A menu is for ACTIONS on the thing you already have; a nav is a list you can see. - Never put the destructive action adjacent to the routine one in the same group. Separate it, or better, send it through an AlertDialog — a menu item fires on the first click, with no second chance. - Do not reach for a Menu when the choice sets a value — that is Select, which reports the current value in its trigger. A Menu trigger says what it does, not what is chosen. - Do not nest more than one submenu level. Base UI will happily render it; a person will not find it. ## Sheet (`echo-sheet`) A molded chassis panel that slides in from an edge — the console side panel and the mobile nav. Built on Base UI's Drawer, so it carries swipe-to-dismiss and a backdrop that tracks the swipe rather than a Dialog translated sideways. sound.transition on open. Compound API. *Feel:* A panel that slides in from the edge and can be swiped back out. - Type: registry:component - npm deps: @base-ui/react, echo-ui-core - Install: `npx echo-ui-cli add sheet` ### Usage ```tsx import { Sheet } from "@/echo/sheet"; Channels ``` ### Composition ```text Sheet Sheet.Trigger Sheet.Content Sheet.Close ``` ### Examples **Side panel** — Defaults to the right edge, swiping right to dismiss. ```tsx Channels ``` **Mobile nav** — From the left, where a hamburger button lives. ```tsx Menu ``` **Bottom sheet** — Swipes down. Good for a one-handed action list on a phone. ```tsx ``` ### Accessibility - Base UI Drawer: a modal dialog named by `title` and described by `description`. - Focus is trapped inside while open and returns to the trigger when it closes. Escape or a click on the backdrop closes it. - Swiping it away is a touch extra; every way of closing it also works from the keyboard. ### Motion & sound - Motion tokens: `--echo-duration-pop` - Reduced motion: The panel and backdrop fade instead of sliding. - Sound: `transition` when it opens. ### Props | Prop | Type | Default | Notes | | --- | --- | --- | --- | | `side` | `"left" | "right" | "top" | "bottom"` | "right" | Edge the sheet is pinned to AND the direction it swipes away — one decision, so they cannot drift apart. | | `Content.title` | `unknown` | **required** | Accessible name for the panel. A drawer without one is an unlabelled region. | | `Content.description` | `unknown` | **required** | What the panel is for, under the title. | | `Content.showClose` | `boolean` | true | Show the corner close control. | | `Content.container` | `unknown` | — | Where the panel mounts; defaults to . Pass an element inside a scoped theme so the panel wears that theme, not the :root one. | ### Anti-patterns - Always pass a title. Without one the panel is an unlabelled region, and a screen-reader user is dropped into content with no idea what opened. - Do not put a form that needs a confirm-or-cancel decision in a Sheet — a sheet can be swiped away mid-edit by design. That is a Modal, which cannot. - Do not stack sheets. A second sheet over the first leaves no way back except dismissing both, and the swipe gesture becomes ambiguous. - Do not use a Sheet where a Popover would do. A sheet takes the whole edge of the screen and a backdrop; a small anchored panel does not need either. ## AlertDialog (`echo-alert-dialog`) A destructive confirm. Modal covers the structure; this is the semantics — role=alertdialog, a required description, and no dismissal by clicking away, so the question has to be answered. Its own sound: caution (the one rough waveform) on open, error on a destructive confirm. Severity reads as the deep end of the accent ramp, never an imported red. *Feel:* A warning that has to be answered: a rough buzz on arrival, and Cancel under your finger first. - Type: registry:component - npm deps: @base-ui/react, echo-ui-core - Install: `npx echo-ui-cli add alert-dialog` ### Usage ```tsx import { AlertDialog } from "@/echo/alert-dialog"; import { HardwareButton } from "@/echo/hardware-button"; Erase takes} title="Erase all takes?" description="Twelve recordings will be removed. This cannot be undone." confirmLabel="Erase" onConfirm={erase} /> ``` ### Composition ```text AlertDialog ``` ### Examples **Erase confirm** — Name the action in the button, state the consequence above it. ```tsx Erase takes} title="Erase all takes?" description="Twelve recordings will be removed. This cannot be undone." confirmLabel="Erase" onConfirm={erase} /> ``` **A serious but non-destructive decision** — destructive={false} keeps the modality and drops the heavy cap and the sound. ```tsx ``` ### Accessibility - `role="alertdialog"`, named by `title` and described by `description`, so a screen reader announces a decision, not a panel. - Clicking outside does not dismiss it — it has to be answered. - Cancel comes first in the tab order, so a reflexive Enter never confirms the destructive action. - Focus is trapped while it is open and returns to the trigger afterwards. ### Motion & sound - Motion tokens: `--echo-duration-glide`, `--echo-duration-swap` - Reduced motion: It appears in place with a short fade, and its buttons no longer move when pressed. - Sound: `caution` when it opens. - Sound: `error` when a destructive action is confirmed; nothing when a non-destructive one is. ### Props | Prop | Type | Default | Notes | | --- | --- | --- | --- | | `trigger` | `unknown` | **required** | Element or text that opens the dialog. | | `title` | `unknown` | **required** | The decision, phrased as a question. | | `description` | `unknown` | **required** | What happens if they say yes. Required — a confirm without its consequence has already decided for the user. | | `children` | `unknown` | **required** | Extra detail between the description and the answers. | | `confirmLabel` | `string` | "Confirm" | Name the ACTION ("Erase", "Discard"), never "OK" — the label is the last thing read before it happens. | | `cancelLabel` | `string` | "Cancel" | | | `destructive` | `boolean` | true | Defaults TRUE: this component exists for the destructive case, so the dangerous spelling is not the quiet one. | | `onConfirm` | `function` | — | Fired when the confirming answer is pressed. | | `onCancel` | `function` | — | | | `open` | `boolean` | — | Controlled open state. | | `defaultOpen` | `boolean` | — | | | `onOpenChange` | `function` | — | | | `container` | `unknown` | — | Where the dialog mounts; defaults to . Pass an element inside a scoped theme so the dialog wears that theme, not the :root one. | ### Anti-patterns - Never label the confirming answer "OK" or "Yes". By the time someone reads the button they have often stopped reading the prose — the label has to say what will happen. - Do not use an AlertDialog for anything reversible. It blocks the whole screen and cannot be dismissed by clicking away; spend that interruption only on decisions that cannot be undone. For a reversible action, act and offer an undo in a Toast. - Do not put the destructive answer first in the DOM. Cancel comes first so it is what Tab reaches and what a reflex Enter confirms. - Never restate the title in the description. The title is the question; the description is the consequence. If the description only rephrases the question, the user has been told nothing. - Do not import a red. Severity here is the deep end of the accent ramp so it survives a reseed — a hardcoded #d00 is the one thing that will not relight. ## HardwareButton (`echo-hardware-button`) A molded cap in a recessed well with real press travel and sound. primary/neutral/ghost, sm/md/lg, icon slots, loading, engraved. Press feel and sound come from useTactile; plastic color from createEchoTheme. *Feel:* A molded cap in a recessed well: it travels down under your finger with a click, then springs back. - Type: registry:component - npm deps: gsap, echo-ui-core - Install: `npx echo-ui-cli add hardware-button` ### Usage ```tsx import { HardwareButton } from "@/echo/hardware-button"; Save ``` ### Composition ```text HardwareButton ``` ### Examples **Primary record button** — The one place hardware turns accent. ```tsx Record ``` **Icon-only, accessible** — Always give icon-only buttons an aria-label. ```tsx } /> ``` **Loading** — Loading disables interaction and shows the dot pulse. ```tsx Saving ``` ### Accessibility - A native `