Split Button
A main action paired with a menu of related actions.
Preview
Loading...
import { Pencil2Icon, TrashIcon } from "@radix-ui/react-icons";
import { SplitButton, SplitButtonAction, SplitButtonMenu } from "@/components/ui/SplitButton";
import { DropdownMenuItem } from "@/components/ui/DropdownMenu";
export function SplitButtonDemo() {
return (
<SplitButton>
<SplitButtonAction>Save</SplitButtonAction>
<SplitButtonMenu>
<DropdownMenuItem>
<Pencil2Icon className="icon" />
Save as draft
</DropdownMenuItem>
<DropdownMenuItem>
<TrashIcon className="icon" />
Discard
</DropdownMenuItem>
</SplitButtonMenu>
</SplitButton>
);
}Installation
Make sure that namespace is set in your component.json file. Namespace docs: Learn more about namespaces
pnpm dlx shadcn@latest add @aura/split-buttonManual
Install the following dependencies:
pnpm install @radix-ui/react-iconsCopy and paste the button component into your components/ui/Button.tsx file.
/**
* @description Displays a button or a component that looks like a button.
*/
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/utils/class-names";
import { Spinner } from "@/components/ui/Spinner";
const buttonVariants = cva("button", {
variants: {
variant: {
default: "button-fill",
fill: "button-fill",
pill: "button-pill border border-gray-6 text-gray-11 bg-gray-2 hover:bg-gray-3",
link: "button-link",
menu: "button-menu",
},
size: {
default: "h-4",
xs: "h-2.5",
sm: "h-3",
md: "h-4",
lg: "h-5",
xl: "h-6",
icon: "w-3 h-3 p-0",
"icon-md": "w-4 h-4 p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
interface ButtonProps
extends React.ComponentProps<"button">,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
isDisabled?: boolean;
isLoading?: boolean;
isLoadingText?: string | React.ReactNode;
mode?: VariantProps<typeof buttonVariants>["variant"];
label?: string | React.ReactNode;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(props: ButtonProps, ref) => {
const {
className,
variant,
mode,
size,
asChild = false,
isDisabled,
isLoading,
isLoadingText,
children,
label,
...rest
} = props;
const Comp = asChild ? Slot : "button";
const disabled = isDisabled || isLoading || props.disabled;
const effectiveVariant = variant ?? mode;
return (
<Comp
data-slot="button"
className={cn(
buttonVariants({ variant: effectiveVariant, size, className }),
disabled && "opacity-50 cursor-not-allowed"
)}
ref={ref}
disabled={disabled}
{...rest}
>
{asChild ? (
children
) : (
<>
{isLoading && isLoadingText ? (
isLoadingText
) : (
<>
{isLoading ? <Spinner /> : null}
{label}
{children}
</>
)}
</>
)}
</Comp>
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };
export type { ButtonProps };
export default Button;
Copy and paste the class names utility into your utils/class-names.ts file.
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Copy and paste the dropdown menu component into your components/ui/DropdownMenu.tsx file.
"use client";
/**
* @description Displays a menu of actions or options triggered by a button.
*/
import * as React from "react";
import { DropdownMenu as DropdownMenuRadix } from "radix-ui";
import {
CheckIcon,
ChevronRightIcon,
CircleIcon,
DotFilledIcon,
} from "@radix-ui/react-icons";
import { cn } from "@/utils/class-names";
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuRadix.Root>) {
return <DropdownMenuRadix.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuRadix.Portal>) {
return (
<DropdownMenuRadix.Portal data-slot="dropdown-menu-portal" {...props} />
);
}
function DropdownMenuTrigger({
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadix.Trigger>) {
return (
<DropdownMenuRadix.Trigger
data-slot="dropdown-menu-trigger"
className={cn(className)}
{...props}
/>
);
}
function DropdownMenuContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadix.Content>) {
return (
<DropdownMenuRadix.Portal>
<DropdownMenuRadix.Content
data-slot="dropdown-menu-content"
collisionPadding={8}
className={cn(
className,
"z-50 origin-(--radix-dropdown-menu-content-transform-origin) bg-gray-1 border border-gray-a6 rounded-sm relative shadow-md data-[state=open]:animate-popover-show data-[state=closed]:animate-popover-hide"
)}
{...props}
/>
</DropdownMenuRadix.Portal>
);
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuRadix.Group>) {
return <DropdownMenuRadix.Group data-slot="dropdown-menu-group" {...props} />;
}
function DropdownMenuItem({
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadix.Item>) {
return (
<DropdownMenuRadix.Item
data-slot="dropdown-menu-item"
className={cn(
className,
"p-0.5 px-2 hover:bg-accent-3 flex relative cursor-pointer"
)}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
children,
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadix.CheckboxItem>) {
return (
<DropdownMenuRadix.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
className,
"p-0.5 px-2 hover:bg-accent-3 flex relative cursor-pointer"
)}
{...props}
>
<span className="absolute left-0.5 top-0 bottom-0 items-center flex justify-center">
<DropdownMenuRadix.ItemIndicator>
<CheckIcon className="text-accent-9" />
</DropdownMenuRadix.ItemIndicator>
</span>
{children}
</DropdownMenuRadix.CheckboxItem>
);
}
function DropdownMenuRadioGroup({
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadix.RadioGroup>) {
return (
<DropdownMenuRadix.RadioGroup
data-slot="dropdown-menu-radio-group"
className={cn(className)}
{...props}
/>
);
}
function DropdownMenuRadioItem({
children,
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadix.RadioItem>) {
return (
<DropdownMenuRadix.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
className,
"p-0.5 px-2 hover:bg-accent-3 flex relative cursor-pointer"
)}
{...props}
>
<span className="absolute left-0.5 top-0 bottom-0 items-center flex justify-center">
<DropdownMenuRadix.ItemIndicator>
<DotFilledIcon className="fill-current text-accent-9" />
</DropdownMenuRadix.ItemIndicator>
</span>
{children}
</DropdownMenuRadix.RadioItem>
);
}
function DropdownMenuLabel({
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadix.Label>) {
return (
<DropdownMenuRadix.Label
data-slot="dropdown-menu-label"
className={cn(className, "p-0.5 px-2 text-gray-12")}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadix.Separator>) {
return (
<DropdownMenuRadix.Separator
data-slot="dropdown-menu-separator"
className={cn(className, "m-0.6 h-px bg-gray-a6")}
{...props}
/>
);
}
function DropdownMenuShortcut({ ...props }: React.ComponentProps<"span">) {
return <span data-slot="dropdown-menu-shortcut" {...props} />;
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuRadix.Sub>) {
return <DropdownMenuRadix.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
children,
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadix.SubTrigger>) {
return (
<DropdownMenuRadix.SubTrigger
data-slot="dropdown-menu-sub-trigger"
className={cn(
className,
"p-0.5 px-2 hover:bg-accent-3 flex relative cursor-pointer"
)}
{...props}
>
{children}
<div className="absolute right-0.5 top-0 bottom-0 items-center flex justify-center">
<ChevronRightIcon />
</div>
</DropdownMenuRadix.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadix.SubContent>) {
return (
<DropdownMenuRadix.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
className,
"z-50 origin-(--radix-dropdown-menu-content-transform-origin) bg-gray-1 border border-gray-a6 rounded-sm relative shadow-md data-[state=open]:animate-popover-show data-[state=closed]:animate-popover-hide"
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};
Copy and paste the SplitButton component into your components/ui/SplitButton.tsx file.
"use client";
import * as React from "react";
import { ChevronDownIcon } from "@radix-ui/react-icons";
import { cn } from "@/utils/class-names";
import { Button, type ButtonProps } from "@/components/ui/Button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/DropdownMenu";
function SplitButton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
role="group"
data-slot="split-button"
className={cn(
"inline-flex items-stretch overflow-hidden rounded-sm",
className,
)}
{...props}
/>
);
}
function SplitButtonAction({ className, ...props }: ButtonProps) {
return (
<Button
data-slot="split-button-action"
className={cn("rounded-r-none", className)}
{...props}
/>
);
}
function SplitButtonMenu({
className,
label = "More actions",
children,
...props
}: React.ComponentProps<typeof DropdownMenuContent> & { label?: string }) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
data-slot="split-button-menu"
aria-label={label}
className="w-4 rounded-none border-l border-accent-a7 px-0"
>
<ChevronDownIcon className="icon" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className={className} {...props}>
{children}
</DropdownMenuContent>
</DropdownMenu>
);
}
export { SplitButton, SplitButtonAction, SplitButtonMenu };