Inkbox

Component

Button Group

Buttons, fields and text segments joined into one bar with shared edges and outer corners only.

npx -y github:anwarlemu/inkbox-brand-cli#release add button-group
Preview
With field
@inboxmail.com
Split
2
Vertical

Usage

Use to join related actions into one control (View as formatted beside Copy as markdown), a field with an action, or a field with a fixed part (the @inboxmail.com suffix as ButtonGroupText). Nest groups to keep space between sets.

Avoid

  • Joining a purple pill with an outline key
  • More than five buttons in a group
  • Joining unrelated actions

Exports: ButtonGroup, ButtonGroupSeparator, ButtonGroupText, buttonGroupVariants. Read the examples for composition and props.

Dependencies: radix-ui, class-variance-authority.

Example

examples/button-group.tsx
import { CaretDownIcon, CopyIcon, FileTextIcon, MinusIcon, PlusIcon } from "@phosphor-icons/react/ssr";​import { Button } from "@/components/ui/button";import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText } from "@/components/ui/button-group";import { Input } from "@/components/ui/input";​export function Preview() {  return (    <ButtonGroup aria-label="Prompt actions">      <Button variant="outline">        <FileTextIcon weight="fill" />        View as formatted      </Button>      <Button variant="outline">        <CopyIcon weight="duotone" />        Copy as markdown      </Button>    </ButtonGroup>  );}​export function WithField() {  return (    <div className="grid w-full max-w-115 gap-4">      <ButtonGroup className="w-full">        <Input aria-label="Mailbox name" defaultValue="acme-support" />        <ButtonGroupText>@inboxmail.com</ButtonGroupText>      </ButtonGroup>      <ButtonGroup className="w-full">        <Input aria-label="Search contacts" placeholder="Search contacts" />        <Button variant="outline">Search</Button>      </ButtonGroup>    </div>  );}​export function Split() {  return (    <div className="flex flex-wrap items-center gap-6">      <ButtonGroup>        <Button>Create Agent</Button>        <ButtonGroupSeparator />        <Button size="icon-lg" aria-label="More create options">          <CaretDownIcon />        </Button>      </ButtonGroup>      <ButtonGroup aria-label="Phone lines">        <Button variant="outline" size="icon" className="size-9" aria-label="Remove a line">          <MinusIcon />        </Button>        <ButtonGroupText className="min-w-12 justify-center text-fg">2</ButtonGroupText>        <Button variant="outline" size="icon" className="size-9" aria-label="Add a line">          <PlusIcon />        </Button>      </ButtonGroup>    </div>  );}​export function Vertical() {  return (    <ButtonGroup orientation="vertical" aria-label="Zoom">      <Button variant="outline" size="icon" className="size-9" aria-label="Zoom in">        <PlusIcon />      </Button>      <Button variant="outline" size="icon" className="size-9" aria-label="Zoom out">        <MinusIcon />      </Button>    </ButtonGroup>  );}

Source

components/ui/button-group.tsx
import * as React from "react";import { Slot } from "radix-ui";import { cva, type VariantProps } from "class-variance-authority";​import { cn } from "@/lib/utils";​// Buttons joined into one control (extrapolated from the Inkbox console, Figma 427:2715: the outline keys of the// created dialog and the split field of the Email channel card). Inner corners square off and the shared edges merge,// so outline keys read as one 36px bar with 8px outer corners; a field, select or text segment can sit in the bar.// ButtonGroupText is the lilac segment with muted 14px text (@inboxmail.com); ButtonGroupSeparator is a 1px rule// between filled buttons.const buttonGroupVariants = cva(  "group/button-group flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[>input]:[&>[data-slot=button]]:h-10 has-[>input]:[&>[data-slot=button-group-text]]:h-10 [&>input]:flex-1",  {    variants: {      orientation: {        horizontal: "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",        vertical: "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",      },    },    defaultVariants: { orientation: "horizontal" },  },);​function ButtonGroup({ className, orientation, ...props }: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {  return <div role="group" data-slot="button-group" data-orientation={orientation ?? "horizontal"} className={cn(buttonGroupVariants({ orientation }), className)} {...props} />;}​function ButtonGroupText({ className, asChild = false, ...props }: React.ComponentProps<"div"> & { asChild?: boolean }) {  const Comp = asChild ? Slot.Root : "div";  return (    <Comp      data-slot="button-group-text"      className={cn("flex items-center gap-2 rounded-control border border-line-field bg-subtle px-3.5 font-sans text-body leading-tight whitespace-nowrap text-fg-muted [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-5.5", className)}      {...props}    />  );}​function ButtonGroupSeparator({ className, orientation = "vertical", ...props }: React.ComponentProps<"div"> & { orientation?: "horizontal" | "vertical" }) {  return (    <div      role="separator"      aria-orientation={orientation}      data-slot="button-group-separator"      data-orientation={orientation}      className={cn("relative shrink-0 self-stretch bg-surface/40", orientation === "vertical" ? "w-px" : "h-px", className)}      {...props}    />  );}​export { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, buttonGroupVariants };

Reference: Extrapolated from the Inkbox console (Figma 427:2715): the outline keys of the created dialog and the Email channel card's split field.