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.
"use client"
import * as React from "react"
import { CheckIcon, CopyIcon } from "@phosphor-icons/react"
import { cn } from "cn"
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
} from "@/components/ui/input-group"
/**
* A command to copy, such as an install line in a hero or docs page: a `$`
* prompt, the command in monospace and a copy button, in the stock
* `InputGroup`. A long command is cut off with an ellipsis.
*
* After a copy, the button shows a check for two seconds and a hidden status
* says "Copied". 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 `InputGroup`.
*/
function InstallCommand({
className,
children,
...props
}: Omit<React.ComponentProps<typeof InputGroup>, "children"> & {
/**
* The command, as plain text or inline elements such as `span`s for
* highlighting. The button copies the text as it shows, so block elements
* would be copied with no space between them.
*/
children: React.ReactNode
}) {
const { copied, failed, copyToClipboard } = useCopyToClipboard()
const snippetRef = React.useRef<HTMLElement>(null)
// 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 (
<InputGroup
data-copied={copied || undefined}
className={cn(
"h-11 border-border bg-card font-mono text-card-foreground",
className
)}
{...props}
>
<InputGroupAddon aria-hidden="true" className="pr-2 pl-4 font-normal">
$
</InputGroupAddon>
<InputGroupText className="min-w-0 flex-1 text-sm/7 text-current">
<code ref={snippetRef} className="truncate">
{children}
</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>
)
}
export { InstallCommand }
export default InstallCommand
pnpm dlx shadcn add @01orbital/install-commandnpx shadcn add @01orbital/install-commandyarn shadcn add @01orbital/install-commandbunx --bun shadcn add @01orbital/install-commandFree under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.
It lands at components/code/install-command.tsx, as source you own and edit.
import { InstallCommand } from "@/components/code/install-command"
// Deep ground, painted with two washes: over art the command takes the dark theme.
const PALETTES = {
tidepool: {
ground: "oklch(0.179 0.029 240.3)",
upper:
"radial-gradient(ellipse 140% 110% at 50% 45%, oklch(0.413 0.082 166.3) 0%, oklch(0.413 0.082 166.3 / 0.5) 55%, transparent 100%)",
lower:
"radial-gradient(ellipse 130% 100% at 50% 55%, oklch(0.32 0.06 250) 0%, oklch(0.32 0.06 250 / 0.5) 55%, transparent 100%)",
},
} as const
export default function InstallCommandDemo() {
return (
<div className="grid w-full max-w-lg gap-6">
<InstallCommand>
pnpm dlx shadcn add @01orbital/moon-sphere
</InstallCommand>
<div className="relative overflow-hidden rounded-2xl p-8">
<div
aria-hidden="true"
className="absolute inset-0"
style={{
backgroundColor: PALETTES.tidepool.ground,
backgroundImage: `${PALETTES.tidepool.upper}, ${PALETTES.tidepool.lower}`,
}}
/>
<InstallCommand className="dark relative">
pnpm dlx shadcn add @01orbital/moon-sphere
</InstallCommand>
</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-clipboardCloud Dancer Slider
Base UI slider dressed as the Cloud Dancer pill: a teal-shaded off-white strip with a dark filled range and a dark orb thumb. Takes one value or a range, with a label per thumb.
Install Switcher
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.