Install command with the package manager as a gliding segmented switch inside the pill: pick pnpm, npm, yarn or bun and the command rewrites; the copy button copies the one showing. Narrow, the switch takes the pill's top row. Add the dark class over art.
"use client"
import * as React from "react"
import { CheckIcon, CopyIcon } from "@phosphor-icons/react"
import { cn } from "cn"
import {
SegmentedControl,
SegmentedControlItem,
} from "@/components/toggle/segmented-control"
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
} from "@/components/ui/input-group"
// The runners a `shadcn add …` command can start with. Add one here to add a
// segment; the label is what the segment shows.
const MANAGERS = [
{ id: "pnpm", label: "pnpm", run: "pnpm dlx" },
{ id: "npm", label: "npm", run: "npx" },
{ id: "yarn", label: "yarn", run: "yarn dlx" },
{ id: "bun", label: "bun", run: "bunx" },
] as const
type Manager = (typeof MANAGERS)[number]["id"]
/**
* An install command with a package manager switch in the same pill, for a
* hero or docs page. Picking pnpm, npm, yarn or bun rewrites the command's
* runner, and the copy button copies the command as it shows.
*
* When the component is narrower than 32rem, the switch takes its own row
* above the command. After a copy, the button shows a check for two
* seconds; if copying fails, the command is selected and a message below
* asks the reader to press ⌘C or Ctrl+C. Over artwork, add
* `className="dark"`. `className` lands on the outer `div`.
*/
function InstallSwitcher({
command,
defaultManager = "pnpm",
className,
...props
}: Omit<React.ComponentProps<"div">, "children"> & {
/**
* Everything after the runner, such as `shadcn add @01orbital/stars`. The
* chosen manager's runner, such as `pnpm dlx` or `npx`, goes in front.
*/
command: string
/** The package manager shown first: `pnpm`, `npm`, `yarn` or `bun`. */
defaultManager?: Manager
}) {
const [manager, setManager] = React.useState<Manager>(defaultManager)
const { copied, failed, copyToClipboard } = useCopyToClipboard()
const snippetRef = React.useRef<HTMLElement>(null)
const run = MANAGERS.find((m) => m.id === manager)?.run ?? MANAGERS[0].run
// On failure the command is selected, so one keystroke copies it by hand.
const copy = async () => {
const code = snippetRef.current
if (!code) return
if (!(await copyToClipboard(code.textContent ?? ""))) {
window.getSelection()?.selectAllChildren(code)
}
}
return (
<div
data-slot="install-switcher"
className={cn("@container/install w-full min-w-0", className)}
{...props}
>
<InputGroup
data-copied={copied || undefined}
className="h-11 border-border bg-card font-mono text-card-foreground @max-lg/install:h-auto @max-lg/install:flex-wrap @max-lg/install:pt-1.5 @max-lg/install:pb-1"
>
<InputGroupAddon className="pl-1.5 @max-lg/install:w-full @max-lg/install:pb-1">
<SegmentedControl
aria-label="Package manager"
value={[manager]}
onValueChange={(next) => {
if (next[0]) setManager(next[0] as Manager)
}}
className="h-8 gap-0.5 p-0.5 font-sans"
>
{MANAGERS.map((m) => (
<SegmentedControlItem
key={m.id}
value={m.id}
className="h-7 px-2.5 text-xs"
>
{m.label}
</SegmentedControlItem>
))}
</SegmentedControl>
</InputGroupAddon>
<InputGroupAddon aria-hidden="true" className="pr-2 pl-2 font-normal">
$
</InputGroupAddon>
<InputGroupText className="min-w-0 flex-1 text-sm/7 text-current">
<code ref={snippetRef} className="truncate">
{run} {command}
</code>
</InputGroupText>
<InputGroupAddon align="inline-end">
<InputGroupButton
size="icon-sm"
onClick={copy}
aria-label={
copied ? "Copied" : failed ? "Copy failed" : "Copy command"
}
>
{copied ? (
<CheckIcon aria-hidden="true" />
) : (
<CopyIcon aria-hidden="true" />
)}
</InputGroupButton>
</InputGroupAddon>
<span
role="status"
className={
failed
? "absolute top-full left-0 mt-2 px-4 font-sans text-xs"
: "sr-only"
}
>
{copied
? "Copied"
: failed
? "Couldn't copy. It's selected: press ⌘C or Ctrl+C."
: ""}
</span>
</InputGroup>
</div>
)
}
export { InstallSwitcher, MANAGERS }
export default InstallSwitcher
pnpm dlx shadcn add @01orbital/install-switchernpx shadcn add @01orbital/install-switcheryarn shadcn add @01orbital/install-switcherbunx --bun shadcn add @01orbital/install-switcherFree under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.
It lands at components/code/install-switcher.tsx, as source you own and edit.
The example also uses conic-glow. Install it too:
pnpm dlx shadcn add @01orbital/conic-glownpx shadcn add @01orbital/conic-glowyarn shadcn add @01orbital/conic-glowbunx --bun shadcn add @01orbital/conic-glowimport { InstallSwitcher } from "@/components/code/install-switcher"
import { ConicGlow } from "@/components/glow/conic-glow"
const COMMAND = "shadcn add @01orbital/conic-glow"
// The second command sits on a dark panel, so it takes the dark theme.
export default function InstallSwitcherDemo() {
return (
<div className="grid w-full max-w-2xl gap-6">
<InstallSwitcher command={COMMAND} />
<div className="dark relative isolate overflow-hidden rounded-2xl bg-background p-8">
<ConicGlow className="-z-10" />
<InstallSwitcher className="relative" command={COMMAND} />
</div>
</div>
)
}Prop
Type
It also accepts the HTML attributes of the element it renders.
npm: @phosphor-icons/react, cn. The CLI installs them for you.
Registry pieces it installs with it:
font-geist-monoinput-groupuse-copy-to-clipboardInstall Command
Copyable shell command in an InputGroup: a $ prompt, the command in Geist Mono and a copy button. If copying fails it selects the text and shows a hint; add the dark class over art.
Segmented Control
Pill segmented control on Base UI ToggleGroup: a muted track where a lifted card-ground thumb glides to the pressed item, still under reduced motion. Horizontal only; values are arrays, as in Base UI.