Kosmesis
Components

Button Group

Visually groups adjacent buttons into one control. Purely presentational.

Installation

npx kosmesis add button-group
pnpm dlx kosmesis add button-group
yarn dlx kosmesis add button-group
bunx kosmesis add button-group

Copy and paste the following code into your project.

button-group.tsx
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { cn } from "@/lib/utils";export interface ButtonGroupProps {  orientation?: "horizontal" | "vertical";  class?: string;  id?: string;  children?: Children;}/** Purely presentational — no Morphos equivalent, same as upstream shadcn/ui. */@Component()export class ButtonGroup extends StatelessComponent<ButtonGroupProps> {  render() {    const { orientation = "horizontal", class: cls, id, children } = this.props;    return (      <div        id={id}        role="group"        data-orientation={orientation}        class={cn(          "flex w-fit items-stretch",          "[&>*]:rounded-none [&>*:first-child]:rounded-l-md [&>*:last-child]:rounded-r-md",          "[&>*:not(:first-child)]:-ml-px",          "data-[orientation=vertical]:flex-col",          "data-[orientation=vertical]:[&>*]:rounded-none data-[orientation=vertical]:[&>*:first-child]:rounded-t-md data-[orientation=vertical]:[&>*:last-child]:rounded-b-md",          "data-[orientation=vertical]:[&>*:not(:first-child)]:mt-[-1px] data-[orientation=vertical]:[&>*:not(:first-child)]:ml-0",          cls,        )}      >        {children}      </div>    );  }}export interface ButtonGroupSeparatorProps {  class?: string;}@Component()export class ButtonGroupSeparator extends StatelessComponent<ButtonGroupSeparatorProps> {  render() {    const { class: cls } = this.props;    return (      <div        role="separator"        class={cn(          "w-px self-stretch bg-border in-data-[orientation=vertical]:h-px in-data-[orientation=vertical]:w-auto in-data-[orientation=vertical]:self-auto",          cls,        )}      />    );  }}export interface ButtonGroupTextProps {  class?: string;  children?: Children;}@Component()export class ButtonGroupText extends StatelessComponent<ButtonGroupTextProps> {  render() {    const { class: cls, children } = this.props;    return (      <div        class={cn(          "flex items-center gap-2 rounded-md border bg-muted px-4 text-sm font-medium",          cls,        )}      >        {children}      </div>    );  }}

Install the following dependencies:

npm install @praxisjs/css
pnpm add @praxisjs/css
yarn add @praxisjs/css
bun add @praxisjs/css

Copy and paste the following code into your project.

button-group.tsx
import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class ButtonGroupStyles extends Stylesheet {  // `& > *[class]` (not plain `& > *`): every real child here (`Button`, `ButtonGroupText`,  // `ButtonGroupSeparator`) sets its own `border-radius` at the same specificity as a bare  // descendant-class selector (one class each). Without the `[class]` attribute bump, this reset  // ties with the child's own rule and the winner is decided by `@Styled` injection order — which  // component class happens to be defined first in its file — instead of by which rule is actually  // meant to win. The attribute selector adds specificity so the group's reset always wins.  $root = this.css({ display: "flex", width: "fit-content", alignItems: "stretch" })    .on("& > *[class]", { borderRadius: "0" })    .on("& > *:first-child", { borderTopLeftRadius: "0.375rem", borderBottomLeftRadius: "0.375rem" })    .on("& > *:last-child", { borderTopRightRadius: "0.375rem", borderBottomRightRadius: "0.375rem" })    .on("& > *:not(:first-child)", { marginLeft: "-1px" })    .on('&[data-orientation="vertical"]', { flexDirection: "column" })    .on('&[data-orientation="vertical"] > *[class]', { borderRadius: "0" })    .on('&[data-orientation="vertical"] > *:first-child', {      borderTopLeftRadius: "0.375rem",      borderTopRightRadius: "0.375rem",      borderBottomLeftRadius: "0",    })    .on('&[data-orientation="vertical"] > *:last-child', {      borderBottomLeftRadius: "0.375rem",      borderBottomRightRadius: "0.375rem",      borderTopRightRadius: "0",    })    .on('&[data-orientation="vertical"] > *:not(:first-child)', { marginLeft: "0", marginTop: "-1px" });  $separator = this.css({ width: "1px", alignSelf: "stretch", backgroundColor: t.border }).on(    '[data-orientation="vertical"] &',    { height: "1px", width: "auto", alignSelf: "auto" },  );  $text = this.css({    display: "flex",    alignItems: "center",    gap: "0.5rem",    borderRadius: `calc(${t.radius} - 2px)`,    border: `1px solid ${t.border}`,    backgroundColor: t.muted,    padding: "0 1rem",    fontSize: "0.875rem",    fontWeight: 500,  });}export interface ButtonGroupProps {  orientation?: "horizontal" | "vertical";  class?: string;  id?: string;  children?: Children;}/** Purely presentational — no Morphos equivalent, same as upstream shadcn/ui. */@Component()export class ButtonGroup extends StatelessComponent<ButtonGroupProps> {  @Styled(ButtonGroupStyles) $s!: ButtonGroupStyles;  render() {    const { orientation = "horizontal", class: cls, id, children } = this.props;    return (      <div id={id} role="group" data-orientation={orientation} class={cx(this.$s.$root, cls)}>        {children}      </div>    );  }}export interface ButtonGroupSeparatorProps {  class?: string;}@Component()export class ButtonGroupSeparator extends StatelessComponent<ButtonGroupSeparatorProps> {  @Styled(ButtonGroupStyles) $s!: ButtonGroupStyles;  render() {    const { class: cls } = this.props;    return <div role="separator" class={cx(this.$s.$separator, cls)} />;  }}export interface ButtonGroupTextProps {  class?: string;  children?: Children;}@Component()export class ButtonGroupText extends StatelessComponent<ButtonGroupTextProps> {  @Styled(ButtonGroupStyles) $s!: ButtonGroupStyles;  render() {    const { class: cls, children } = this.props;    return <div class={cx(this.$s.$text, cls)}>{children}</div>;  }}

Examples

Usage

import { Button } from "@/components/ui/button";
import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText } from "@/components/ui/button-group";

<ButtonGroup>
  <Button variant="outline">Copy</Button>
  <Button variant="outline">Paste</Button>
  <Button variant="outline">Cut</Button>
</ButtonGroup>

<ButtonGroup>
  <ButtonGroupText>https://</ButtonGroupText>
  <Button variant="outline">example.com</Button>
</ButtonGroup>

<ButtonGroup orientation="vertical">
  <Button variant="outline">Up</Button>
  <ButtonGroupSeparator />
  <Button variant="outline">Down</Button>
</ButtonGroup>

On this page