Skip to main content
Nexus Design System
GitHub

ButtonGroup

ButtonGroup joins related buttons into one cluster with shared borders. Use it for a set of related actions or a split button. For an input with addons, use InputGroup.

'use client';

import { ButtonGroup } from '@/components/nxds/components/button-group/button-group';
import { Button } from '@/components/nxds/components/button/button';

export default function ButtonGroupDefault() {
  return (
    <ButtonGroup>
      <Button variant="outline">Day</Button>
      <Button variant="outline">Week</Button>
      <Button variant="outline">Month</Button>
    </ButtonGroup>
  );
}

This example also needs: Button.

Installation

npx nxds-cli@latest add button-group

Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.

Usage

import {
  ButtonGroup,
  ButtonGroupSeparator,
  ButtonGroupText,
} from '@/components/nxds/components/button-group/button-group';

Composition

ButtonGroup
├── ButtonGroupText
└── ButtonGroupSeparator

Examples

Small

Set size="sm" to shrink every member of the group.

View:
'use client';

import {
  ButtonGroup,
  ButtonGroupText,
} from '@/components/nxds/components/button-group/button-group';
import { Button } from '@/components/nxds/components/button/button';

export default function ButtonGroupSmall() {
  return (
    <ButtonGroup size="sm">
      <ButtonGroupText>View:</ButtonGroupText>
      <Button variant="outline">Day</Button>
      <Button variant="outline">Week</Button>
    </ButtonGroup>
  );
}

Large

Set size="lg" to enlarge every member of the group.

View:
'use client';

import {
  ButtonGroup,
  ButtonGroupText,
} from '@/components/nxds/components/button-group/button-group';
import { Button } from '@/components/nxds/components/button/button';

export default function ButtonGroupLarge() {
  return (
    <ButtonGroup size="lg">
      <ButtonGroupText>View:</ButtonGroupText>
      <Button variant="outline">Day</Button>
      <Button variant="outline">Week</Button>
    </ButtonGroup>
  );
}

Vertical

Stack the buttons with orientation="vertical".

'use client';

import { ButtonGroup } from '@/components/nxds/components/button-group/button-group';
import { Button } from '@/components/nxds/components/button/button';

export default function ButtonGroupVertical() {
  return (
    <ButtonGroup orientation="vertical" size="sm">
      <Button variant="outline">Top</Button>
      <Button variant="outline">Middle</Button>
      <Button variant="outline">Bottom</Button>
    </ButtonGroup>
  );
}

With text

Add a text prefix with ButtonGroupText.

https://
'use client';

import {
  ButtonGroup,
  ButtonGroupText,
} from '@/components/nxds/components/button-group/button-group';
import { Button } from '@/components/nxds/components/button/button';

export default function ButtonGroupWithText() {
  return (
    <ButtonGroup>
      <ButtonGroupText>https://</ButtonGroupText>
      <Button variant="outline">nxds.dev</Button>
    </ButtonGroup>
  );
}

With separator

Divide ghost buttons into sub-groups with ButtonGroupSeparator.

'use client';

import {
  LinkIcon,
  TextBIcon,
  TextItalicIcon,
  TextUnderlineIcon,
} from '@phosphor-icons/react/ssr';

import {
  ButtonGroup,
  ButtonGroupSeparator,
} from '@/components/nxds/components/button-group/button-group';
import { Button } from '@/components/nxds/components/button/button';

export default function ButtonGroupWithSeparator() {
  return (
    <ButtonGroup>
      <Button variant="ghost" size="icon" aria-label="Bold">
        <TextBIcon />
      </Button>
      <Button variant="ghost" size="icon" aria-label="Italic">
        <TextItalicIcon />
      </Button>
      <Button variant="ghost" size="icon" aria-label="Underline">
        <TextUnderlineIcon />
      </Button>
      <ButtonGroupSeparator />
      <Button variant="ghost" size="icon" aria-label="Add link">
        <LinkIcon />
      </Button>
    </ButtonGroup>
  );
}

Props

ButtonGroup

ButtonGroup props
PropTypeDefaultDescription
orientation"horizontal" | "vertical" | null"horizontal"No description
sizeButtonGroupSize"default"

Size for ButtonGroupText and any Button without its own size. It also reaches a Button inside a trigger wrapper, as in a split button.

ButtonGroupSeparator

ButtonGroupSeparator adds no props of its own; it accepts the props of the element or primitive it wraps.

ButtonGroupText

ButtonGroupText props
PropTypeDefaultDescription
asChildbooleanfalse

Renders the child element instead, such as a link.

sizeButtonGroupSizeNo default documented

Addon size. Inherits from ButtonGroup when omitted, or "default" outside one.

ButtonGroup joins related buttons into one cluster with shared borders. Use it for a set of related actions or a split button. For an input with addons, use InputGroup.

<script setup lang="ts">
import { Button } from '@/components/button';
import { ButtonGroup } from '@/components/button-group';
</script>

<template>
  <ButtonGroup>
    <Button variant="outline">Day</Button>
    <Button variant="outline">Week</Button>
    <Button variant="outline">Month</Button>
  </ButtonGroup>
</template>

This example also needs: Button.

Installation

  1. Install these components first:

    Separator.

  2. Install the following dependencies:

    npm install class-variance-authority@^0.7.1
    
  3. Copy and paste the following code into your project.

    components/button-group/ButtonGroup.vue
    <script setup lang="ts">
    import { type HTMLAttributes, provide } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    import { ButtonGroupKey, type ButtonGroupSize } from './button-group-context';
    
    import { type ButtonGroupVariants, buttonGroupVariants } from '.';
    
    export interface ButtonGroupProps {
      orientation?: ButtonGroupVariants['orientation'];
      /**
       * Size for ButtonGroupText and any Button without its own `size`. It also reaches a Button inside a trigger wrapper, as in a split button.
       */
      size?: ButtonGroupSize;
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<ButtonGroupProps>(), {
      orientation: 'horizontal',
      size: 'default',
    });
    
    provide(ButtonGroupKey, props);
    </script>
    
    <template>
      <div
        role="group"
        data-slot="button-group"
        :data-orientation="orientation"
        :data-size="size"
        :class="cn(buttonGroupVariants({ orientation }), props.class)"
      >
        <slot />
      </div>
    </template>
    
    components/button-group/ButtonGroupSeparator.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import type { SeparatorProps as RekaSeparatorProps } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    import { Separator } from '../separator';
    
    export interface ButtonGroupSeparatorProps extends RekaSeparatorProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<ButtonGroupSeparatorProps>(), {
      orientation: 'vertical',
      decorative: true,
    });
    
    const delegated = reactiveOmit(props, 'class');
    </script>
    
    <template>
      <Separator
        v-bind="delegated"
        data-slot="button-group-separator"
        :class="
          cn(
            'nx:relative nx:self-stretch nx:data-[orientation=vertical]:h-auto',
            props.class
          )
        "
      />
    </template>
    
    components/button-group/ButtonGroupText.vue
    <script setup lang="ts">
    import { Primitive, type PrimitiveProps } from 'reka-ui';
    import { computed, type HTMLAttributes, inject } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    import { ButtonGroupKey, type ButtonGroupSize } from './button-group-context';
    
    import { buttonGroupTextVariants } from '.';
    
    export interface ButtonGroupTextProps extends PrimitiveProps {
      /**
       * Addon size. Inherits from ButtonGroup when omitted, or `"default"` outside one.
       */
      size?: ButtonGroupSize;
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<ButtonGroupTextProps>(), {
      as: 'div',
    });
    
    const group = inject(ButtonGroupKey, undefined);
    const size = computed(() => props.size ?? group?.size ?? 'default');
    </script>
    
    <template>
      <Primitive
        :as="as"
        :as-child="asChild"
        data-slot="button-group-text"
        :data-size="size"
        :class="
          cn(
            buttonGroupTextVariants({ size }),
            asChild &&
              'nx:cursor-pointer nx:hover:bg-container-hover nx:active:bg-container-active',
            props.class
          )
        "
      >
        <slot />
      </Primitive>
    </template>
    
    components/button-group/button-group-context.ts
    import type { InjectionKey } from 'vue';
    
    /** Size shared from a ButtonGroup to its members. */
    export type ButtonGroupSize = 'sm' | 'default' | 'lg';
    
    /**
     * A ButtonGroup provides its `size` through this key; `Button` and
     * `ButtonGroupText` inject it. Injection (not a walk over direct children) is
     * what lets a grouped Button inherit the group size even when it is nested
     * inside a trigger wrapper the group cannot reach directly. An explicit `size`
     * on the child wins over the group's.
     */
    export const ButtonGroupKey: InjectionKey<{ size: ButtonGroupSize }> =
      Symbol('button-group');
    
    components/button-group/index.ts
    import { cva, type VariantProps } from 'class-variance-authority';
    
    export type { ButtonGroupSize } from './button-group-context';
    export {
      default as ButtonGroup,
      type ButtonGroupProps,
    } from './ButtonGroup.vue';
    export {
      default as ButtonGroupSeparator,
      type ButtonGroupSeparatorProps,
    } from './ButtonGroupSeparator.vue';
    export {
      default as ButtonGroupText,
      type ButtonGroupTextProps,
    } from './ButtonGroupText.vue';
    
    export const buttonGroupVariants = cva(
      'nx:flex nx:w-fit nx:items-stretch nx:*:focus-visible:relative nx:*:focus-visible:z-10',
      {
        variants: {
          orientation: {
            horizontal:
              'nx:[&>*:not(:first-child)]:rounded-l-none nx:[&>*:not(:first-child)]:border-l-0 nx:[&>*:not(:last-child)]:rounded-r-none',
            vertical:
              'nx:flex-col nx:[&>*:not(:first-child)]:rounded-t-none nx:[&>*:not(:first-child)]:border-t-0 nx:[&>*:not(:last-child)]:rounded-b-none',
          },
        },
        defaultVariants: {
          orientation: 'horizontal',
        },
      }
    );
    
    export const buttonGroupTextVariants = cva(
      'nx:text-inherit nx:no-underline nx:flex nx:items-center nx:gap-2 nx:rounded-md nx:border-default nx:border-border-default nx:bg-control-background nx:shadow-xs nx:transition-control nx:duration-fast nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default nx:[&_svg]:pointer-events-none nx:[&_svg]:size-4',
      {
        variants: {
          size: {
            sm: 'nx:h-8 nx:px-2.5 nx:typography-label-default',
            default: 'nx:h-10 nx:px-3 nx:typography-label-default',
            lg: 'nx:h-12 nx:px-3.5 nx:typography-label-default',
          },
        },
        defaultVariants: {
          size: 'default',
        },
      }
    );
    
    export type ButtonGroupVariants = VariantProps<typeof buttonGroupVariants>;
    
  4. Keep each file at the path shown above it.

    Paths are relative to packages/vue/src/. Keep the same folders under your @/ root, and the imports between files work without edits.

Usage

import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText } from '@/components/button-group';

Examples

Small

Set size="sm" to shrink every member of the group.

<script setup lang="ts">
import { Button } from '@/components/button';
import { ButtonGroup, ButtonGroupText } from '@/components/button-group';
</script>

<template>
  <ButtonGroup size="sm">
    <ButtonGroupText>View:</ButtonGroupText>
    <Button variant="outline">Day</Button>
    <Button variant="outline">Week</Button>
  </ButtonGroup>
</template>

Large

Set size="lg" to enlarge every member of the group.

<script setup lang="ts">
import { Button } from '@/components/button';
import { ButtonGroup, ButtonGroupText } from '@/components/button-group';
</script>

<template>
  <ButtonGroup size="lg">
    <ButtonGroupText>View:</ButtonGroupText>
    <Button variant="outline">Day</Button>
    <Button variant="outline">Week</Button>
  </ButtonGroup>
</template>

Vertical

Stack the buttons with orientation="vertical".

<script setup lang="ts">
import { Button } from '@/components/button';
import { ButtonGroup } from '@/components/button-group';
</script>

<template>
  <ButtonGroup orientation="vertical" size="sm">
    <Button variant="outline">Top</Button>
    <Button variant="outline">Middle</Button>
    <Button variant="outline">Bottom</Button>
  </ButtonGroup>
</template>

With text

Add a text prefix with ButtonGroupText.

<script setup lang="ts">
import { Button } from '@/components/button';
import { ButtonGroup, ButtonGroupText } from '@/components/button-group';
</script>

<template>
  <ButtonGroup>
    <ButtonGroupText>https://</ButtonGroupText>
    <Button variant="outline">nxds.dev</Button>
  </ButtonGroup>
</template>

With separator

Divide ghost buttons into sub-groups with ButtonGroupSeparator.

<script setup lang="ts">
import {
  PhLink,
  PhTextB,
  PhTextItalic,
  PhTextUnderline,
} from '@phosphor-icons/vue';

import { Button } from '@/components/button';
import { ButtonGroup, ButtonGroupSeparator } from '@/components/button-group';
</script>

<template>
  <ButtonGroup>
    <Button variant="ghost" size="icon" aria-label="Bold">
      <PhTextB />
    </Button>
    <Button variant="ghost" size="icon" aria-label="Italic">
      <PhTextItalic />
    </Button>
    <Button variant="ghost" size="icon" aria-label="Underline">
      <PhTextUnderline />
    </Button>
    <ButtonGroupSeparator />
    <Button variant="ghost" size="icon" aria-label="Add link">
      <PhLink />
    </Button>
  </ButtonGroup>
</template>

This example also needs:

npm install @phosphor-icons/vue@^2.2.1

Props

ButtonGroup

ButtonGroup props
PropTypeDefaultDescription
orientation"vertical" | "horizontal" | null"horizontal"No description
sizeButtonGroupSize"default"

Size for ButtonGroupText and any Button without its own size. It also reaches a Button inside a trigger wrapper, as in a split button.

ButtonGroupSeparator

ButtonGroupSeparator adds no props of its own; it accepts the props of the element or primitive it wraps.

ButtonGroupText

ButtonGroupText props
PropTypeDefaultDescription
sizeButtonGroupSizeNo default documented

Addon size. Inherits from ButtonGroup when omitted, or "default" outside one.