---
title: Install Command
description: An npm / pnpm / yarn / bun command block where only the words that differ change, and every block on the page follows your pick.
slug: install-command
tier: free
category: Text
dependencies: [motion]
exports: [InstallCommand, PACKAGE_MANAGERS, InstallCommandLabels, InstallCommandProps, PackageManager]
---

# Install Command

Use Install Command to show a package install command with an npm, pnpm, yarn and bun picker that every block on the page follows.

A command block for docs and READMEs with one tab per package manager and a copy button. Switching tabs animates only the words that change: `install` rolls out, `add` rolls in, and the package name glides into place. By default every block on the page shares the choice, follows it in a cascade outward from the block you clicked, and remembers it across visits and browser tabs.

## Quick answers

- **Dependencies:** `react`, `react-dom` and `motion` 12 or newer. Nothing else.
- **React version:** 19 or newer.
- **SSR and `'use client'`:** Renders on the server. The file starts with `'use client'`, so the Next.js App Router can import it straight into a Server Component. In the Pages Router, Vite, React Router and Astro that line does nothing. See [Troubleshooting](/docs/troubleshooting).
- **Licence:** MIT. Use it in any project, commercial or not.
- **Paid access:** None. No account and no key.

## Installation

### shadcn

```bash
npx shadcn@latest add https://brioui.com/r/install-command.json
```

### Manual

1. Copy `install-command.tsx` into your project, for example `components/brio/install-command.tsx`.
2. Install the peer dependency: `npm install motion`.
3. Requires React 19. The theme tokens (`--ui-*`) are optional; the component falls back to its own defaults.

## Usage

```tsx
import { InstallCommand } from '@/components/brio/install-command'

export function QuickStart() {
  return <InstallCommand add="motion" />
}
```

This renders `npm install motion`, `pnpm add motion`, `yarn add motion` and `bun add motion`.

## Examples

### Presets

Each preset maps to the right command for every manager. Use one preset per block.

```tsx
import { InstallCommand } from '@/components/brio/install-command'

export function Setup() {
  return (
    <>
      {/* npm install -D typescript @types/react / pnpm add -D ... */}
      <InstallCommand add={['typescript', '@types/react']} dev />

      {/* npx shadcn@latest add button / pnpm dlx ... / yarn dlx ... / bunx ... */}
      <InstallCommand exec="shadcn@latest add button" />

      {/* npm create vite@latest my-app / pnpm create ... */}
      <InstallCommand create="vite@latest my-app" />

      {/* npm run dev / pnpm dev / yarn dev / bun run dev */}
      <InstallCommand run="dev" />
    </>
  )
}
```

### Exact commands

`commands` overrides the preset per manager. Managers you leave out still use the preset.

```tsx
import { InstallCommand } from '@/components/brio/install-command'

export function CliInstall() {
  return (
    <InstallCommand
      add="my-cli"
      global
      commands={{ bun: 'bun add -g my-cli@canary' }}
      managers={['npm', 'pnpm', 'bun']}
      defaultManager="pnpm"
    />
  )
}
```

### Independent block with a copy callback

Set `sync={false}` so this block keeps its own tab and does not read or write storage. `onCopy` fires after a successful copy.

```tsx
import { InstallCommand, type PackageManager } from '@/components/brio/install-command'

export function TrackedInstall() {
  const track = (command: string, manager: PackageManager) => {
    console.log('copied', { command, manager })
  }
  return <InstallCommand add="motion" sync={false} prompt="" onCopy={track} />
}
```

### Translated labels

```tsx
import { InstallCommand } from '@/components/brio/install-command'

export function Installieren() {
  return (
    <InstallCommand
      add="motion"
      labels={{
        tabs: 'Paketmanager',
        copy: 'Befehl kopieren',
        copied: 'Kopiert',
        copyFailed: 'Kopieren fehlgeschlagen',
        copiedSpoken: 'In die Zwischenablage kopiert',
        copyFailedSpoken: 'Kopieren fehlgeschlagen. Befehl markieren und manuell kopieren.',
      }}
    />
  )
}
```

### Separate remembered choice

Give a site its own `storageKey` so its remembered choice does not collide with other sites on the same origin.

```tsx
import { InstallCommand } from '@/components/brio/install-command'

const KEY = 'acme-docs:pm'

export function Steps() {
  return (
    <>
      <InstallCommand add="@acme/sdk" storageKey={KEY} />
      <InstallCommand run="dev" storageKey={KEY} />
    </>
  )
}
```

All synced blocks share one in-memory choice, whatever their `storageKey`. The key only controls what is saved and which other-tab changes are followed.

## Props

### InstallCommand

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `add` | `string \| string[]` | — | Package(s) to add: `add="motion"` → npm install motion / pnpm add motion … Arrays are joined with spaces. Used when no other preset or exact command applies. |
| `dev` | `boolean` | `false` | With `add`: install as a dev dependency (`-D`). Ignored when `global` is set (warns in development). @default false Ignored when `global` is set, with a console warning in development. |
| `global` | `boolean` | `false` | With `add`: install globally (`npm install -g`, `pnpm add -g`, `yarn global add`, `bun add -g`). @default false |
| `exec` | `string` | — | One-off binary: `exec="shadcn@latest add button"` → npx / pnpm dlx / yarn dlx / bunx |
| `create` | `string` | — | Scaffolder: `create="vite@latest my-app"` → npm create / pnpm create … Scaffolder: `<manager> create <value>`. |
| `run` | `string` | — | package.json script: `run="dev"` → npm run dev / pnpm dev / yarn dev / bun run dev |
| `commands` | `Partial<Record<PackageManager, string>>` | — | Exact command per manager. Wins over every preset. Line breaks are kept (e.g. a `\` + newline continuation shows on two lines, exactly as it copies). Exact commands per manager; wins over presets. Managers left out fall back to the preset. |
| `managers` | `readonly PackageManager[]` | `PACKAGE_MANAGERS` | Which tabs to show, in order. Unknown names are dropped; an empty list falls back to all four (dev warning). @default PACKAGE_MANAGERS (npm, pnpm, yarn, bun) |
| `defaultManager` | `PackageManager` | `'npm'` | Tab selected before the user picks one (or nothing is remembered). Falls back to the first of `managers` when not listed there. @default 'npm' |
| `sync` | `boolean` | `true` | Share the choice with every synced block on the page, save it to `localStorage`, and follow changes from other browser tabs. Share the choice with every block on the page and remember it in localStorage (also follows other tabs). @default true |
| `storageKey` | `string` | `'install-command:pm'` | localStorage key for the remembered choice when `sync` is on. @default 'install-command:pm' |
| `prompt` | `string` | `'$'` | Shown before the command, never copied. Pass `''` to hide it. At most 40% of the row: a long shell prompt ends in “…” (full text on hover). At most 40% of the row (ends in "…"). @default '$' |
| `onCopy` | `(command: string, manager: PackageManager) => void` | — | Called after a successful copy with the exact command text and the active manager. Not called when copying fails. |
| `tabStyle` | `'labels' \| 'icons'` | `'labels'` | How the package-manager tabs read. `'icons'` shows only the logos (the name stays as the tab's accessible name and tooltip), for tight rows. |
| `labels` | `Partial<InstallCommandLabels>` | — | Override any of the UI strings (tab list name, copy button, copy feedback). Override any UI string. See [InstallCommandLabels](#installcommandlabels). |
| `className` | `string` | — | Extra class on the root element (`.ic-root`). |
| `style` | `CSSProperties` | — | Inline styles on the root element; handy for `--install-command-*` variables. |

Preset precedence: `commands` > `exec` > `create` > `run` > `add`.

### InstallCommandLabels

| Field | Type | Default |
| --- | --- | --- |
| `tabs` | `string` | `'Package manager'` (accessible name of the tab list) |
| `copy` | `string` | `'Copy command'` (copy button's accessible name) |
| `copied` | `string` | `'Copied'` (shown beside the button after a copy, and the button's name meanwhile) |
| `copyFailed` | `string` | `'Copy failed'` (shown beside the button when copying fails) |
| `copiedSpoken` | `string` | `'Copied to clipboard'` (announced) |
| `copyFailedSpoken` | `string` | `'Copy failed. Select the command and copy it manually.'` (announced) |

### PackageManager

`'npm' | 'pnpm' | 'yarn' | 'bun'`

### PACKAGE_MANAGERS

`readonly ['npm', 'pnpm', 'yarn', 'bun']`. The default value of `managers`.

## Customisation

- **Theme tokens:** `--ui-ink`, `--ui-surface`, `--ui-accent`, `--ui-danger`, `--ui-font-mono`. Corners: `--ui-radius` (surfaces and inner cards, scaled from it), `--ui-radius-sm` (buttons, fields, rows and tags, scaled from it).
- **Component CSS variables:** `--install-command-ink`, `--install-command-surface`, `--install-command-accent` (focus ring and copy highlight), `--install-command-danger` ("Copy failed" text), `--install-command-font` (monospace font). `--install-command-radius` sets the corner of the block (default 0.875 × `--ui-radius`, 14px); the other corners scale from it.
- **Class names:** `className` goes on `.ic-root`. Stable inner classes: `.ic-head`, `.ic-tabs`, `.ic-tab`, `.ic-pill` (sliding tab background), `.ic-logo`, `.ic-copy`, `.ic-copy-label`, `.ic-panel`, `.ic-prompt`, `.ic-line`, `.ic-code`.
- **Long content:** a long command scrolls sideways under an edge fade, with a thin scrollbar while hovered or focused. Letters animate per grapheme (emoji stay whole); words in scripts that need shaping (Arabic, Hebrew, Devanagari, Thai…) animate as whole words. The copy feedback label uses the free space next to the tabs (at least ~10 characters) and ends in “…” when longer. A preset that is set but blank (`add=""`, `run="  "`) logs a dev warning.
- **Labels:** every UI string (tab list name, copy button, visible and announced copy feedback) can be replaced through `labels`; see [InstallCommandLabels](#installcommandlabels).

## Responsive behaviour

- On narrow blocks the package-manager tabs scroll sideways, so the copy button always stays on the card.
- On touch screens the tabs and the copy button are 44px tall.

## Keyboard

| Key | Action |
| --- | --- |
| `Tab` | Moves between the active tab, the copy button and the command panel. |
| `ArrowRight` / `ArrowLeft` | On the tabs: select the next / previous manager (wraps around). |
| `Home` / `End` | On the tabs: select the first / last manager. |
| `Enter` / `Space` | On the copy button: copy the command. |
| `Cmd+C` / `Ctrl+C` | On the focused command panel: copy the command when no text is selected. |

## Accessibility

- The tabs are a real `tablist` with `tab` buttons (roving `tabIndex`) and one `tabpanel`, labelled by the active tab.
- The command is read once as plain text; the per-letter animated copy is `aria-hidden`.
- Copy success ("Copied to clipboard") and failure ("Copy failed. Select the command and copy it manually.") are announced in a polite `role="status"` region.
- One click on the command selects all of it. The prompt is never selected or copied.
- If the Clipboard API is unavailable, it falls back to `document.execCommand('copy')`.
- Reduced motion: words crossfade instead of rolling, the tab pill jumps instead of stretching, the cascade delay and copy highlight are off.

## Agent prompt

```text
Add the Brio UI "Install Command" component to this project.
1. Run `npx shadcn@latest add https://brioui.com/r/install-command.json`.
2. Make sure `motion` is installed and React is 19 or newer.
3. Import it from the file the CLI created and render it with one preset: add="pkg" (plus dev or global), exec="bin args", create="template", or run="script"; use commands={{ ... }} for exact per-manager text.
4. Blocks sync their package-manager choice by default; pass the same storageKey to every block on the site, or sync={false} for a standalone block.
Full docs: https://brioui.com/docs/components/install-command.md
```
