Button Group
Group multiple button-like elements together.
Demo
Related Keys
This requires the following keys to be installed:
Related Theme
This requires the following theme to be installed:
Component
vue
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import { buttonGroupInjectionKey } from '@/ui/keys/button-group'
import theme from '@/ui/theme/button-group'
import { Primitive } from 'reka-ui'
import { tv } from 'tailwind-variants'
import { computed, provide } from 'vue'
const buttonGroup = tv(theme)
type ButtonGroupVariants = VariantProps<typeof buttonGroup>
export interface ButtonGroupProps {
as?: any
size?: ButtonGroupVariants['size']
orientation?: ButtonGroupVariants['orientation']
class?: any
}
export interface ButtonGroupSlots {
default: (props?: object) => any
}
</script>
<script setup lang="ts">
const props = withDefaults(defineProps<ButtonGroupProps>(), {
orientation: 'horizontal',
})
defineSlots<ButtonGroupSlots>()
provide(buttonGroupInjectionKey, computed(() => ({
orientation: props.orientation,
size: props.size,
})))
</script>
<template>
<Primitive :as="as" :class="buttonGroup({ orientation, class: props.class })">
<slot />
</Primitive>
</template>vue
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import { buttonGroupInjectionKey } from '@/UI/Keys/button-group'
import theme from '@/UI/Theme/button-group'
import { Primitive } from 'reka-ui'
import { tv } from 'tailwind-variants'
import { computed, provide } from 'vue'
const buttonGroup = tv(theme)
type ButtonGroupVariants = VariantProps<typeof buttonGroup>
export interface ButtonGroupProps {
as?: any
size?: ButtonGroupVariants['size']
orientation?: ButtonGroupVariants['orientation']
class?: any
}
export interface ButtonGroupSlots {
default: (props?: object) => any
}
</script>
<script setup lang="ts">
const props = withDefaults(defineProps<ButtonGroupProps>(), {
orientation: 'horizontal',
})
defineSlots<ButtonGroupSlots>()
provide(buttonGroupInjectionKey, computed(() => ({
orientation: props.orientation,
size: props.size,
})))
</script>
<template>
<Primitive :as="as" :class="buttonGroup({ orientation, class: props.class })">
<slot />
</Primitive>
</template>Theme
ts
export const buttonGroupVariant = {
buttonGroup: {
horizontal: 'not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none',
vertical: 'not-only:first:rounded-b-none not-only:last:rounded-t-none not-last:not-first:rounded-none',
},
}
export const buttonGroupVariantWithRoot = {
buttonGroup: {
horizontal: {
root: 'group',
base: 'group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none',
},
vertical: {
root: 'group',
base: 'group-not-only:group-first:rounded-b-none group-not-only:group-last:rounded-t-none group-not-last:group-not-first:rounded-none',
},
},
}
export default {
base: '',
variants: {
size: {
xs: '',
sm: '',
md: '',
lg: '',
xl: '',
},
orientation: {
horizontal: '',
vertical: '',
},
},
}View Nuxt UI theme
ts
export const buttonGroupVariant = {
buttonGroup: {
horizontal: 'not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none',
vertical: 'not-only:first:rounded-b-none not-only:last:rounded-t-none not-last:not-first:rounded-none',
},
}
export const buttonGroupVariantWithRoot = {
buttonGroup: {
horizontal: {
root: 'group',
base: 'group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none',
},
vertical: {
root: 'group',
base: 'group-not-only:group-first:rounded-b-none group-not-only:group-last:rounded-t-none group-not-last:group-not-first:rounded-none',
},
},
}
export default {
base: 'relative',
variants: {
size: {
xs: '',
sm: '',
md: '',
lg: '',
xl: '',
},
orientation: {
horizontal: 'inline-flex -space-x-px',
vertical: 'flex flex-col -space-y-px',
},
},
}Test
To test this component, you can use the following test file:
ts
import type { RenderOptions } from '@testing-library/vue'
import Button from '@/ui/components/Button.vue'
import ButtonGroup from '@/ui/components/ButtonGroup.vue'
import Input from '@/ui/components/Input.vue'
import theme from '@/ui/theme/button-group'
import { render, screen } from '@testing-library/vue'
import { describe, expect, it } from 'vitest'
describe('button-group', () => {
const sizes = Object.keys(theme.variants.size) as any
it.each<[string, RenderOptions<typeof ButtonGroup>]>([
// Props
['with as', { props: { as: 'section' } }],
['with class', { props: { class: 'absolute' } }],
// Slots
['with default slot', {
slots: {
default: {
components: { Input, Button },
template: `<Input /> <Button> Click me! </Button>`,
},
},
}],
['orientation vertical with default slot', {
props: { orientation: 'vertical' },
slots: {
default: {
components: { Input, Button },
template: `<Input /> <Button> Click me! </Button>`,
},
},
}],
...sizes.map((size: string) =>
[`with size ${size}`, { props: { size }, slots: {
default: {
components: { Input, Button },
template: `<Input /> <Button> Click me! </Button>`,
},
} }],
),
])('renders %s correctly', (name, options) => {
render(ButtonGroup, {
attrs: {
'data-testid': 'button-group',
},
...options,
})
expect(screen.getByTestId('button-group')).matchSnapshot()
})
})ts
import type { RenderOptions } from '@testing-library/vue'
import Button from '@/UI/Components/Button.vue'
import ButtonGroup from '@/UI/Components/ButtonGroup.vue'
import Input from '@/UI/Components/Input.vue'
import theme from '@/UI/Theme/button-group'
import { render, screen } from '@testing-library/vue'
import { describe, expect, it } from 'vitest'
describe('button-group', () => {
const sizes = Object.keys(theme.variants.size) as any
it.each<[string, RenderOptions<typeof ButtonGroup>]>([
// Props
['with as', { props: { as: 'section' } }],
['with class', { props: { class: 'absolute' } }],
// Slots
['with default slot', {
slots: {
default: {
components: { Input, Button },
template: `<Input /> <Button> Click me! </Button>`,
},
},
}],
['orientation vertical with default slot', {
props: { orientation: 'vertical' },
slots: {
default: {
components: { Input, Button },
template: `<Input /> <Button> Click me! </Button>`,
},
},
}],
...sizes.map((size: string) =>
[`with size ${size}`, { props: { size }, slots: {
default: {
components: { Input, Button },
template: `<Input /> <Button> Click me! </Button>`,
},
} }],
),
])('renders %s correctly', (name, options) => {
render(ButtonGroup, {
attrs: {
'data-testid': 'button-group',
},
...options,
})
expect(screen.getByTestId('button-group')).matchSnapshot()
})
})