Components
Loading preview...
A container for grouping a set of buttons and controls.
npx shadcn@latest add https://21st.dev/r/coss.com/toolbar"use client";
import {
AlignCenterIcon,
AlignLeftIcon,
AlignRightIcon,
DollarSignIcon,
PercentIcon,
ChevronsUpDownIcon,
} from "lucide-react";
import { Toggle as TogglePrimitive } from "@base-ui/react/toggle";
import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group";
import {
Toolbar,
ToolbarButton,
ToolbarGroup,
ToolbarSeparator,
} from "@/components/ui/component";
import { cn } from "@/lib/utils";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
// Inline Button
const buttonVariants = cva(
"relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg border font-medium text-base outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 sm:text-sm [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-mx-0.5 [&_svg]:shrink-0",
{
defaultVariants: { size: "default", variant: "default" },
variants: {
size: {
default: "h-9 px-[calc(--spacing(3)-1px)] sm:h-8",
icon: "size-9 sm:size-8",
},
variant: {
default:
"not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-primary bg-primary text-primary-foreground shadow-primary/24 shadow-xs hover:bg-primary/90 data-pressed:bg-primary/90 [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none",
ghost:
"border-transparent text-foreground hover:bg-accent data-pressed:bg-accent",
},
},
},
);
function Button({
className,
variant,
size,
children,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>) {
return (
<button
type="button"
className={cn(buttonVariants({ className, size, variant }))}
data-slot="button"
{...props}
>
{children}
</button>
);
}
// Inline Toggle
const toggleVariants = cva(
"relative inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-2 whitespace-nowrap rounded-lg border font-medium text-base text-foreground outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 data-pressed:bg-input/64 data-pressed:text-accent-foreground sm:text-sm [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-mx-0.5 [&_svg]:shrink-0",
{
defaultVariants: { size: "default", variant: "default" },
variants: {
size: {
default: "h-9 min-w-9 px-[calc(--spacing(2)-1px)] sm:h-8 sm:min-w-8",
},
variant: {
default: "border-transparent",
},
},
},
);
function Toggle({
className,
variant,
size,
...props
}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive
className={cn(toggleVariants({ className, size, variant }))}
data-slot="toggle"
{...props}
/>
);
}
// Inline ToggleGroup
const ToggleGroupContext = React.createContext<VariantProps<typeof toggleVariants>>({
size: "default",
variant: "default",
});
function ToggleGroup({
className,
variant = "default",
size = "default",
children,
...props
}: ToggleGroupPrimitive.Props & VariantProps<typeof toggleVariants>) {
return (
<ToggleGroupPrimitive
className={cn(
"flex w-fit gap-0.5",
className,
)}
data-slot="toggle-group"
{...props}
>
<ToggleGroupContext.Provider value={{ size, variant }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive>
);
}
function ToggleGroupToggle({
className,
children,
variant,
size,
...props
}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext);
return (
<Toggle
className={className}
size={context.size || size}
variant={context.variant || variant}
{...props}
>
{children}
</Toggle>
);
}
// Inline Select (simplified)
function FontSelect({ defaultValue, items }: { defaultValue: string; items: { label: string; value: string }[] }) {
const [value, setValue] = React.useState(defaultValue);
const [open, setOpen] = React.useState(false);
const ref = React.useRef<HTMLDivElement>(null);
const selected = items.find((i) => i.value === value);
React.useEffect(() => {
if (!open) return;
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, [open]);
return (
<div ref={ref} className="relative">
<button
type="button"
onClick={() => setOpen(!open)}
className="relative inline-flex min-h-9 min-w-36 select-none items-center justify-between gap-2 rounded-lg border border-input bg-background not-dark:bg-clip-padding px-[calc(--spacing(3)-1px)] text-left text-base text-foreground shadow-xs/5 outline-none sm:min-h-8 sm:text-sm"
>
<span className="flex-1 truncate">{selected?.label}</span>
<ChevronsUpDownIcon className="-me-1 size-4.5 opacity-80 sm:size-4" />
</button>
{open && (
<div className="absolute top-full left-0 z-50 mt-1 min-w-full rounded-lg border bg-popover shadow-lg/5 p-1">
{items.map(({ label, value: v }) => (
<button
key={v}
type="button"
onClick={() => { setValue(v); setOpen(false); }}
className={cn(
"flex w-full min-h-8 items-center gap-2 rounded-sm px-2 py-1 text-base outline-none hover:bg-accent hover:text-accent-foreground sm:min-h-7 sm:text-sm",
v === value && "bg-accent text-accent-foreground",
)}
>
{label}
</button>
))}
</div>
)}
</div>
);
}
const fontItems = [
{ label: "Helvetica", value: "helvetica" },
{ label: "Arial", value: "arial" },
{ label: "Times New Roman", value: "times-new-roman" },
];
export default function ToolbarDemo() {
return (
<div className="flex items-center justify-center w-full min-h-screen bg-background p-8">
<Toolbar>
<ToggleGroup className="border-none p-0" defaultValue={["left"]}>
<ToolbarButton
aria-label="Align left"
render={<ToggleGroupToggle value="left" />}
>
<AlignLeftIcon />
</ToolbarButton>
<ToolbarButton
aria-label="Align center"
render={<ToggleGroupToggle value="center" />}
>
<AlignCenterIcon />
</ToolbarButton>
<ToolbarButton
aria-label="Align right"
render={<ToggleGroupToggle value="right" />}
>
<AlignRightIcon />
</ToolbarButton>
</ToggleGroup>
<ToolbarSeparator />
<ToolbarGroup>
<ToolbarButton
aria-label="Format as currency"
render={<Button size="icon" variant="ghost" />}
>
<DollarSignIcon />
</ToolbarButton>
<ToolbarButton
aria-label="Format as percent"
render={<Button size="icon" variant="ghost" />}
>
<PercentIcon />
</ToolbarButton>
</ToolbarGroup>
<ToolbarSeparator />
<ToolbarGroup>
<FontSelect defaultValue="helvetica" items={fontItems} />
</ToolbarGroup>
<ToolbarSeparator />
<ToolbarGroup>
<ToolbarButton render={<Button />}>Save</ToolbarButton>
</ToolbarGroup>
</Toolbar>
</div>
);
}