Separator
Separates content horizontally or vertically.
Demo
Vue UI
Related Components
This requires the following components to be installed:
Related Theme
This requires the following theme to be installed:
Component
vue
<script lang="ts">
import type { AvatarProps } from '@/ui/components/Avatar.vue'
import type { SeparatorProps as _SeparatorProps } from 'reka-ui'
import type { VariantProps } from 'tailwind-variants'
import Avatar from '@/ui/components/Avatar.vue'
import Icon from '@/ui/components/Icon.vue'
import theme from '@/ui/theme/separator'
import { reactivePick } from '@vueuse/core'
import { Separator, useForwardProps } from 'reka-ui'
import { tv } from 'tailwind-variants'
import { computed } from 'vue'
const separator = tv(theme)
type SeparatorVariants = VariantProps<typeof separator>
export interface SeparatorProps extends Pick<_SeparatorProps, 'decorative'> {
as?: any
label?: string
avatar?: AvatarProps
icon?: string
color?: SeparatorVariants['color']
size?: SeparatorVariants['size']
type?: SeparatorVariants['type']
orientation?: SeparatorVariants['orientation']
class?: any
ui?: Partial<typeof separator.slots>
}
export interface SeparatorSlots {
default: (props?: object) => any
}
</script>
<script setup lang="ts">
const props = withDefaults(defineProps<SeparatorProps>(), {
orientation: 'horizontal',
})
const slots = defineSlots<SeparatorSlots>()
const rootProps = useForwardProps(reactivePick(props, 'as', 'decorative', 'orientation'))
const ui = computed(() => separator({
color: props.color,
size: props.size,
type: props.type,
orientation: props.orientation,
}))
</script>
<template>
<Separator v-bind="rootProps" :class="ui.root({ class: [props.class, props.ui?.root] })">
<div :class="ui.border({ class: props.ui?.border })" />
<template v-if="label || icon || avatar || !!slots.default">
<div :class="ui.container({ class: props.ui?.container })">
<slot>
<span v-if="label" :class="ui.label({ class: props.ui?.label })">{{ label }}</span>
<Icon v-else-if="icon" :name="icon" :class="ui.icon({ class: props.ui?.icon })" />
<Avatar v-else-if="avatar" :size="((props.ui?.avatarSize || ui.avatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.avatar({ class: props.ui?.avatar })" />
</slot>
</div>
<div :class="ui.border({ class: props.ui?.border })" />
</template>
</Separator>
</template>vue
<script lang="ts">
import type { AvatarProps } from '@/UI/Components/Avatar.vue'
import type { SeparatorProps as _SeparatorProps } from 'reka-ui'
import type { VariantProps } from 'tailwind-variants'
import Avatar from '@/UI/Components/Avatar.vue'
import Icon from '@/UI/Components/Icon.vue'
import theme from '@/UI/Theme/separator'
import { reactivePick } from '@vueuse/core'
import { Separator, useForwardProps } from 'reka-ui'
import { tv } from 'tailwind-variants'
import { computed } from 'vue'
const separator = tv(theme)
type SeparatorVariants = VariantProps<typeof separator>
export interface SeparatorProps extends Pick<_SeparatorProps, 'decorative'> {
as?: any
label?: string
avatar?: AvatarProps
icon?: string
color?: SeparatorVariants['color']
size?: SeparatorVariants['size']
type?: SeparatorVariants['type']
orientation?: SeparatorVariants['orientation']
class?: any
ui?: Partial<typeof separator.slots>
}
export interface SeparatorSlots {
default: (props?: object) => any
}
</script>
<script setup lang="ts">
const props = withDefaults(defineProps<SeparatorProps>(), {
orientation: 'horizontal',
})
const slots = defineSlots<SeparatorSlots>()
const rootProps = useForwardProps(reactivePick(props, 'as', 'decorative', 'orientation'))
const ui = computed(() => separator({
color: props.color,
size: props.size,
type: props.type,
orientation: props.orientation,
}))
</script>
<template>
<Separator v-bind="rootProps" :class="ui.root({ class: [props.class, props.ui?.root] })">
<div :class="ui.border({ class: props.ui?.border })" />
<template v-if="label || icon || avatar || !!slots.default">
<div :class="ui.container({ class: props.ui?.container })">
<slot>
<span v-if="label" :class="ui.label({ class: props.ui?.label })">{{ label }}</span>
<Icon v-else-if="icon" :name="icon" :class="ui.icon({ class: props.ui?.icon })" />
<Avatar v-else-if="avatar" :size="((props.ui?.avatarSize || ui.avatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.avatar({ class: props.ui?.avatar })" />
</slot>
</div>
<div :class="ui.border({ class: props.ui?.border })" />
</template>
</Separator>
</template>Theme
ts
export default {
slots: {
root: '',
border: '',
container: '',
icon: '',
avatar: '',
avatarSize: '',
label: '',
},
variants: {
color: {
primary: { border: '' },
secondary: { border: '' },
success: { border: '' },
info: { border: '' },
warning: { border: '' },
error: { border: '' },
neutral: { border: '' },
},
orientation: {
horizontal: {
root: '',
border: '',
container: '',
},
vertical: {
root: '',
border: '',
container: '',
},
},
size: {
xs: '',
sm: '',
md: '',
lg: '',
xl: '',
},
type: {
solid: {
border: '',
},
dashed: {
border: '',
},
dotted: {
border: '',
},
},
},
compoundVariants: [],
defaultVariants: {
color: 'neutral',
size: 'xs',
type: 'solid',
} as const,
}View Nuxt UI theme
ts
export default {
slots: {
root: 'flex items-center align-center text-center',
border: '',
container: 'font-medium text-default flex',
icon: 'shrink-0 size-5',
avatar: 'shrink-0',
avatarSize: '2xs',
label: 'text-sm',
},
variants: {
color: {
primary: { border: 'border-primary' },
secondary: { border: 'border-secondary' },
success: { border: 'border-success' },
info: { border: 'border-info' },
warning: { border: 'border-warning' },
error: { border: 'border-error' },
neutral: { border: 'border-default' },
},
orientation: {
horizontal: {
root: 'w-full flex-row',
border: 'w-full',
container: 'mx-3 whitespace-nowrap',
},
vertical: {
root: 'h-full flex-col',
border: 'h-full',
container: 'my-2',
},
},
size: {
xs: '',
sm: '',
md: '',
lg: '',
xl: '',
},
type: {
solid: {
border: 'border-solid',
},
dashed: {
border: 'border-dashed',
},
dotted: {
border: 'border-dotted',
},
},
},
compoundVariants: [
{
orientation: 'horizontal',
size: 'xs',
class: { border: 'border-t' },
} as const,
{
orientation: 'horizontal',
size: 'sm',
class: { border: 'border-t-[2px]' },
} as const,
{
orientation: 'horizontal',
size: 'md',
class: { border: 'border-t-[3px]' },
} as const,
{
orientation: 'horizontal',
size: 'lg',
class: { border: 'border-t-[4px]' },
} as const,
{
orientation: 'horizontal',
size: 'xl',
class: { border: 'border-t-[5px]' },
} as const,
{
orientation: 'vertical',
size: 'xs',
class: { border: 'border-s' },
} as const,
{
orientation: 'vertical',
size: 'sm',
class: { border: 'border-s-[2px]' },
} as const,
{
orientation: 'vertical',
size: 'md',
class: { border: 'border-s-[3px]' },
} as const,
{
orientation: 'vertical',
size: 'lg',
class: { border: 'border-s-[4px]' },
} as const,
{
orientation: 'vertical',
size: 'xl',
class: { border: 'border-s-[5px]' },
} as const,
],
defaultVariants: {
color: 'neutral',
size: 'xs',
type: 'solid',
} as const,
}Test
To test this component, you can use the following test file:
ts
import type { RenderOptions } from '@testing-library/vue'
import Separator from '@/ui/components/Separator.vue'
import theme from '@/ui/theme/separator'
import { render } from '@testing-library/vue'
import { describe, expect, it } from 'vitest'
describe('separator', () => {
const types = Object.keys(theme.variants.type) as any
const sizes = Object.keys(theme.variants.size) as any
it.each<RenderOptions<typeof Separator>[]>([
// Props
['with label', { props: { label: '+1' } }],
['with icon', { props: { icon: 'i-lucide-image' } }],
['with avatar', { props: { avatar: { src: 'https://github.com/vue.png' } } }],
['with orientation vertical', { props: { orientation: 'vertical' as const } }],
['with decorative', { props: { decorative: true } }],
...types.map((type: string) => [`with type ${type}`, { props: { type } }]),
...sizes.map((size: string) => [`with size ${size}`, { props: { size } }]),
['with color primary', { props: { color: 'primary' } }],
['with as', { props: { as: 'span' } }],
['with class', { props: { class: 'flex-row-reverse' } }],
['with ui', { props: { ui: { label: 'text-lg' } } }],
])('renders %s correctly', (name, options) => {
const { html } = render(Separator, options)
expect(html()).toMatchSnapshot()
})
})ts
import type { RenderOptions } from '@testing-library/vue'
import Separator from '@/UI/Components/Separator.vue'
import theme from '@/UI/Theme/separator'
import { render } from '@testing-library/vue'
import { describe, expect, it } from 'vitest'
describe('separator', () => {
const types = Object.keys(theme.variants.type) as any
const sizes = Object.keys(theme.variants.size) as any
it.each<RenderOptions<typeof Separator>[]>([
// Props
['with label', { props: { label: '+1' } }],
['with icon', { props: { icon: 'i-lucide-image' } }],
['with avatar', { props: { avatar: { src: 'https://github.com/vue.png' } } }],
['with orientation vertical', { props: { orientation: 'vertical' as const } }],
['with decorative', { props: { decorative: true } }],
...types.map((type: string) => [`with type ${type}`, { props: { type } }]),
...sizes.map((size: string) => [`with size ${size}`, { props: { size } }]),
['with color primary', { props: { color: 'primary' } }],
['with as', { props: { as: 'span' } }],
['with class', { props: { class: 'flex-row-reverse' } }],
['with ui', { props: { ui: { label: 'text-lg' } } }],
])('renders %s correctly', (name, options) => {
const { html } = render(Separator, options)
expect(html()).toMatchSnapshot()
})
})