Components
Sidebar help button with a promo card that glides into it when hidden: the button then wears a blue dot and bounces, and its menu keeps the docked card plus what's new.

"use client"
import * as React from "react"
import { HelpPopper, HelpPromoCard } from "@/components/ui/help-popper"
/**
* A schematic sidebar, only enough of one to say "bottom-left of an app", with
* the menu opening from its footer.
*/
export default function HelpPopperDemo() {
// The card starts in the rail; hiding it flies it into the help button,
// which is where its content now lives.
const [docked, setDocked] = React.useState(false)
const [bump, setBump] = React.useState(0)
// The menu renders inside this frame rather than on <body>, so the demo
// stays one self-contained rectangle.
const [frame, setFrame] = React.useState<HTMLDivElement | null>(null)
const triggerRef = React.useRef<HTMLButtonElement>(null)
return (
// Wide on purpose: the capture only zooms small demos, and zooming moves a
// floating menu away from its trigger.
<div
ref={setFrame}
className="relative flex h-[720px] w-[1040px] items-stretch gap-6 p-8"
>
<div className="flex w-[212px] shrink-0 flex-col rounded-xl border border-border bg-muted/30 p-2">
<div className="flex items-center gap-2 px-1 py-1.5">
<span className="size-5 rounded-md bg-foreground/20" />
<span className="h-2 flex-1 rounded-full bg-foreground/15" />
</div>
<div className="mt-4 flex flex-col gap-2 p-1">
{[64, 48, 72, 40, 56].map((width, index) => (
<span
key={width}
className={`h-2 rounded-full ${
index === 0 ? "bg-foreground/30" : "bg-foreground/[0.12]"
}`}
style={{ width }}
/>
))}
</div>
{/* The card sits above the footer and flies into the button below it,
which is where its content ends up living. */}
<div className="mt-auto flex flex-col gap-2 p-1">
{!docked && (
<HelpPromoCard
title="Using agents"
description="Enable agentic workflows with 21st's powerful MCP server."
actionLabel="Get started"
targetRef={triggerRef}
onDismissed={() => {
setDocked(true)
setBump((value) => value + 1)
}}
/>
)}
<div className="flex items-center gap-2">
<span className="size-5 rounded-full bg-foreground/15" />
<span className="h-2 flex-1 rounded-full bg-foreground/[0.12]" />
<HelpPopper
triggerRef={triggerRef}
side="top"
align="start"
avoidCollisions={false}
portalContainer={frame}
showDot={docked}
bumpSignal={bump}
highlight={
docked
? {
icon: <GetStartedIcon />,
label: "Using agents",
meta: "New",
onSelect: () => setDocked(false),
}
: undefined
}
items={[
{ icon: <BookIcon />, label: "Docs", external: true },
{ icon: <MailIcon />, label: "Contact us" },
{ icon: <LightbulbIcon />, label: "Share feedback" },
{ icon: <DiscordIcon />, label: "Community", external: true },
]}
sections={[
{
title: "What's new",
items: [
{ label: "Rolling amount slider" },
{ label: "Publishing from the terminal" },
{ label: "Full changelog", external: true },
],
},
]}
/>
</div>
</div>
</div>
<div className="flex min-w-0 flex-1 flex-col gap-4">
<div className="flex items-center gap-3">
<span className="h-2.5 w-32 rounded-full bg-foreground/15" />
{/* The slot is always here, so nothing shifts when the button
appears. */}
<button
type="button"
aria-hidden={!docked}
tabIndex={docked ? 0 : -1}
onClick={() => setDocked(false)}
className={`ml-auto h-8 rounded-md border border-border px-3 text-[13px] font-medium transition-[opacity,scale] duration-200 active:scale-[0.98] ${
docked ? "opacity-100" : "pointer-events-none opacity-0"
}`}
>
Bring the card back
</button>
</div>
<div className="grid flex-1 grid-cols-2 gap-4">
{[0, 1, 2, 3].map((tile) => (
<div
key={tile}
className="rounded-xl border border-border bg-muted/20"
/>
))}
</div>
</div>
</div>
)
}
// Exact marks from the product's icon set, inlined so the demo has no
// dependency on our internal icon module.
const svgProps = {
xmlns: "http://www.w3.org/2000/svg",
viewBox: "0 0 24 24",
fill: "none",
"aria-hidden": true,
className: "size-4",
} as const
function GetStartedIcon() {
return (
<svg {...svgProps} stroke="currentColor">
<path
d="M3.5 14.9655C3.17607 14.0369 3 13.039 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 13.039 20.8239 14.0369 20.5 14.9655"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M6.95797 18.5026L11.1269 11.058C11.5088 10.3762 12.4901 10.3762 12.872 11.058L17.0409 18.5026C17.5498 19.4114 16.5038 20.3956 15.6277 19.8324L12.5402 17.8476C12.2108 17.6358 11.7881 17.6358 11.4587 17.8476L8.37123 19.8324C7.49509 20.3956 6.44905 19.4114 6.95797 18.5026Z"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
function BookIcon() {
return (
<svg {...svgProps}>
<path
d="M9 3.5H7.5C5.84315 3.5 4.5 4.84315 4.5 6.5V17.5C4.5 19.1569 5.84315 20.5 7.5 20.5H9M9 3.5H16.5C18.1569 3.5 19.5 4.84315 19.5 6.5V17.5C19.5 19.1569 18.1569 20.5 16.5 20.5H9M9 3.5V20.5M13 8H15.5M13 12H15.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
function MailIcon() {
return (
<svg {...svgProps}>
<g transform="scale(1.15) translate(-1.8, -1.8)">
<path
d="M12.8944 12.5528L13.3416 13.4472L12.8944 12.5528ZM20 7V17H22V7H20ZM19 18H5.00002V20H19V18ZM4.00002 17V7H2.00002V17H4.00002ZM5.00002 6H19V4H5.00002V6ZM20.5528 7.60557L12.4472 11.6584L13.3416 13.4472L21.4472 9.39443L20.5528 7.60557ZM11.5528 11.6584L3.44723 7.60557L2.5528 9.39443L10.6584 13.4472L11.5528 11.6584ZM4.00002 7C4.00002 6.44772 4.44773 6 5.00002 6V4C3.34316 4 2.00002 5.34314 2.00002 7H4.00002ZM5.00002 18C4.44773 18 4.00002 17.5523 4.00002 17H2.00002C2.00002 18.6569 3.34316 20 5.00002 20V18ZM20 17C20 17.5523 19.5523 18 19 18V20C20.6569 20 22 18.6569 22 17H20ZM12.4472 11.6584C12.1657 11.7991 11.8343 11.7991 11.5528 11.6584L10.6584 13.4472C11.5029 13.8695 12.4971 13.8695 13.3416 13.4472L12.4472 11.6584ZM22 7C22 5.34315 20.6569 4 19 4V6C19.5523 6 20 6.44772 20 7H22Z"
fill="currentColor"
/>
</g>
</svg>
)
}
function LightbulbIcon() {
return (
<svg {...svgProps}>
<path
d="M10 21H14M15.6076 15C17.6404 13.7751 19 11.5463 19 9C19 5.13401 15.866 2 12 2C8.13401 2 5 5.13401 5 9C5 11.5463 6.35958 13.7751 8.39241 15M15.6076 15C15.4111 15.1184 15.2084 15.2274 15 15.3264V16.5C15 17.3284 14.3284 18 13.5 18H10.5C9.67157 18 9 17.3284 9 16.5V15.3264C8.7916 15.2274 8.58886 15.1184 8.39241 15M15.6076 15H8.39241"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
function DiscordIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
className="size-3.5"
>
<path d="M19.73 4.87a18.2 18.2 0 0 0-4.6-1.44c-.21.4-.4.8-.58 1.21-1.69-.25-3.4-.25-5.1 0-.18-.41-.37-.82-.59-1.2-1.6.27-3.14.75-4.6 1.43A19.04 19.04 0 0 0 .96 17.7a18.43 18.43 0 0 0 5.63 2.87c.46-.62.86-1.28 1.2-1.98-.65-.25-1.29-.55-1.9-.92.17-.12.32-.24.47-.37 3.58 1.7 7.7 1.7 11.28 0l.46.37c-.6.36-1.25.67-1.9.92.35.7.75 1.35 1.2 1.98 2.03-.63 3.94-1.6 5.64-2.87.47-4.87-.78-9.09-3.3-12.83ZM8.3 15.12c-1.1 0-2-1.02-2-2.27 0-1.24.88-2.26 2-2.26s2.02 1.02 2 2.26c0 1.25-.89 2.27-2 2.27Zm7.4 0c-1.1 0-2-1.02-2-2.27 0-1.24.88-2.26 2-2.26s2.02 1.02 2 2.26c0 1.25-.88 2.27-2 2.27Z" />
</svg>
)
}