Skip to content

Separator ​

Separator is one honest boundary for the rare places where spacing is not enough. It renders a native hr horizontally and supplies the semantics HTML does not have when the boundary is vertical.

Use ordinary Tailwind for length, thickness, color, spacing, opacity, and responsive behavior. Separator has no visual variants and should not replace every border in an application.

Preview Live · Vue
Usage
vue
<script setup>
import Separator from '@/components/ui/separator/Separator.vue'
</script>

<template>
  <article>
    <section aria-labelledby="profile-title">
      <h2 id="profile-title">Profile</h2>
      <p>Personal account details.</p>
    </section>

    <Separator class="my-8" />

    <section aria-labelledby="security-title">
      <h2 id="security-title">Security</h2>
      <p>Sign-in and recovery settings.</p>
    </section>
  </article>
</template>
jsx
import Separator from '@/components/ui/separator/Separator.jsx'

export default function AccountSettings() {
  return (
    <article>
      <section aria-labelledby="profile-title">
        <h2 id="profile-title">Profile</h2>
        <p>Personal account details.</p>
      </section>

      <Separator className="my-8" />

      <section aria-labelledby="security-title">
        <h2 id="security-title">Security</h2>
        <p>Sign-in and recovery settings.</p>
      </section>
    </article>
  )
}
svelte
<script>
  import Separator from '$lib/components/ui/separator/Separator.svelte'
</script>

<article>
  <section aria-labelledby="profile-title">
    <h2 id="profile-title">Profile</h2>
    <p>Personal account details.</p>
  </section>

  <Separator class="my-8" />

  <section aria-labelledby="security-title">
    <h2 id="security-title">Security</h2>
    <p>Sign-in and recovery settings.</p>
  </section>
</article>
View source Separator.vue
Separator.vue
vue
<script setup>
import { computed, ref, useAttrs } from 'vue'
import { twMerge } from 'tailwind-merge'

defineOptions({ inheritAttrs: false })

const props = defineProps({
  orientation: {
    type: String,
    default: 'horizontal',
    validator: (value) => ['horizontal', 'vertical'].includes(value)
  }
})

const attrs = useAttrs()
const element = ref()
const isVertical = computed(() => props.orientation === 'vertical')
const baseClasses = computed(() =>
  isVertical.value ? 'w-px self-stretch' : 'h-px w-full'
)

const forwardedAttrs = computed(() => {
  const {
    class: _class,
    role: _role,
    'aria-orientation': _ariaOrientation,
    'data-orientation': _dataOrientation,
    'data-slot': _dataSlot,
    ...rest
  } = attrs
  return rest
})

defineExpose({ element })
</script>

<template>
  <component
    :is="isVertical ? 'div' : 'hr'"
    ref="element"
    v-bind="forwardedAttrs"
    data-slot="separator"
    :data-orientation="isVertical ? 'vertical' : 'horizontal'"
    :role="isVertical ? 'separator' : undefined"
    :aria-orientation="isVertical ? 'vertical' : undefined"
    :class="
      twMerge(
        'shrink-0 border-0 bg-gray-200 forced-colors:bg-current dark:bg-gray-800',
        baseClasses,
        attrs.class
      )
    "
  />
</template>

Installation ​

One command detects Vue, React, or Svelte and writes the matching one-file source into the conventional component directory:

Run from your application’s directory. Klean detects your framework and copies the matching source.

Terminal
npx klean-ui add separator

Source you own. Edit the installed files to make them yours.

Usage ​

Use Separator between adjacent regions only when the boundary carries meaning that spacing alone does not communicate.

Copy the example above for your framework.

API ​

InputDefaultPurpose
orientationhorizontalSemantic direction. Horizontal renders hr; vertical renders the correct ARIA separator bridge.
class / className—Ordinary Tailwind classes merged after the neutral monochrome baseline.
native attributes—IDs, titles, aria-hidden, data attributes, test hooks, and other ordinary attributes.
element reference—Framework-native access to the actual hr or vertical separator when genuinely needed.

orientation is not a visual variant. It determines which semantic element and accessibility attributes are correct.

Why this is a component ​

A horizontal rule alone would not justify an abstraction: native hr already has implicit separator semantics and a horizontal orientation.

The component earns its small API by safely joining two representations:

html
<!-- Separator renders this horizontally -->
<hr />

<!-- Separator renders this vertically -->
<div role="separator" aria-orientation="vertical"></div>

The vertical case is easy to make visually correct while forgetting role="separator" or aria-orientation="vertical". Klean protects that semantic boundary consistently in Vue, React, and Svelte while still letting the browser own the horizontal case.

Horizontal and vertical boundaries ​

The horizontal default is a real thematic break. Do not add a redundant explicit role or aria-orientation to it.

Use orientation="vertical" only inside a layout with a real vertical boundary. The component changes to a div, owns role="separator", and fixes aria-orientation="vertical" so caller attributes cannot accidentally contradict it.

Preview Live · Vue
View source DocumentToolbar.vue
DocumentToolbar.vue
<script setup>
import Separator from '@/components/ui/separator/Separator.vue'
</script>

<template>
  <div
    role="toolbar"
    aria-label="Document actions"
    class="flex h-10 items-stretch"
  >
    <button type="button">Undo</button>
    <button type="button">Redo</button>
    <Separator
      orientation="vertical"
      aria-hidden="true"
      class="mx-2 h-6 self-center bg-gray-300 dark:bg-gray-700"
    />
    <button type="button">Share</button>
  </div>
</template>

The example is decorative because the toolbar's buttons are already understandable without the line. A vertical separator that conveys a meaningful grouping can remain exposed to accessibility APIs by omitting aria-hidden.

Semantic or decorative ​

Separator is semantic by default. Use it when the break helps describe a change in topic or grouping.

When the line is visual reinforcement only, use the native attribute:

vue
<Separator aria-hidden="true" class="my-4" />

Klean intentionally has no decorative prop. aria-hidden is already the platform vocabulary, remains visible in the markup, and works identically across the supported frameworks.

Do not use role="presentation" to override Separator. The component protects its orientation semantics; use aria-hidden="true" when the entire line should leave the accessibility tree.

Styling with Tailwind ​

The neutral baseline is one monochrome pixel. Caller classes merge after it and can replace every visual decision:

vue
<Separator class="my-8 h-0.5 bg-black dark:bg-white" />

<Separator
  orientation="vertical"
  class="mx-3 h-8 w-0.5 self-center bg-emerald-500"
/>

Repeated application treatments can become an application-owned wrapper or shared class recipe. They do not become Klean variants.

Layout recipes ​

Use Separator between distinct regions in an account panel or command surface. Keep borders on cards, fields, and rows as ordinary Tailwind classes.

Preview Live · Vue
View source Separator.vue
Separator.vue
<script setup>
import { computed, ref, useAttrs } from 'vue'
import { twMerge } from 'tailwind-merge'

defineOptions({ inheritAttrs: false })

const props = defineProps({
  orientation: {
    type: String,
    default: 'horizontal',
    validator: (value) => ['horizontal', 'vertical'].includes(value)
  }
})

const attrs = useAttrs()
const element = ref()
const isVertical = computed(() => props.orientation === 'vertical')
const baseClasses = computed(() =>
  isVertical.value ? 'w-px self-stretch' : 'h-px w-full'
)

const forwardedAttrs = computed(() => {
  const {
    class: _class,
    role: _role,
    'aria-orientation': _ariaOrientation,
    'data-orientation': _dataOrientation,
    'data-slot': _dataSlot,
    ...rest
  } = attrs
  return rest
})

defineExpose({ element })
</script>

<template>
  <component
    :is="isVertical ? 'div' : 'hr'"
    ref="element"
    v-bind="forwardedAttrs"
    data-slot="separator"
    :data-orientation="isVertical ? 'vertical' : 'horizontal'"
    :role="isVertical ? 'separator' : undefined"
    :aria-orientation="isVertical ? 'vertical' : undefined"
    :class="
      twMerge(
        'shrink-0 border-0 bg-gray-200 forced-colors:bg-current dark:bg-gray-800',
        baseClasses,
        attrs.class
      )
    "
  />
</template>

The same semantic seam accepts both product voices. The account and command layouts remain application markup.

Use border-t, border-b, or divide-y for row boundaries, field underlines, card edges, and tab indicators. Those lines describe the elements they belong to.

Accessibility ​

  • Prefer native HTML. Horizontal Separator is an hr and keeps its implicit separator role.
  • Do not redundantly add role="separator" or aria-orientation="horizontal" to the native hr.
  • Vertical Separator renders role="separator" with aria-orientation="vertical" because HTML has no native vertical rule.
  • Add aria-hidden="true" only when the boundary is entirely decorative.
  • Do not put Separator in the tab order. A static separator is not a resizer, slider, or other control.
  • Do not rely on the line alone to label regions. Keep real headings, landmarks, lists, fieldsets, and other document structure.
  • Caller colors should remain visible in light, dark, and forced-colors modes.

Durable behavior ​

Separator owns no client state and needs none. It has no mount-time state, storage, URL parameter, event listener, animation, focus work, or cleanup lifecycle.

Its durability is structural: server rendering and hydration produce the same native element and orientation semantics. State belongs to the surrounding Tabs, Menu, toolbar, page region, or application feature—not to the line between them.

When to use ​

Use Separator for:

  • a real thematic change between adjacent content regions;
  • a dense menu or command surface where a visual group boundary remains useful;
  • a vertical boundary whose ARIA orientation should not be reimplemented at every call site;
  • removing a styled-divider dependency while preserving correct semantics and caller-owned design.

When not to use ​

  • Prefer spacing when proximity already makes the groups clear.
  • Use a background change when large page regions need stronger visual separation.
  • Keep ordinary borders on headers, footers, cards, inputs, tables, and rows when the line describes that element's edge.
  • Keep divide-y on dense lists or table bodies when it is the clearest row treatment.
  • Use Tabs for peer views; its active indicator is not a Separator.
  • Use Menu for actions and destinations; Separator adds no menu behavior.
  • Do not use Separator as a draggable pane resizer or slider. Those are interactive controls with different keyboard and value semantics.

Complete framework source ​

Copy, inspect, and change the complete one-file source for your framework.

vue
<script setup>
import { computed, ref, useAttrs } from 'vue'
import { twMerge } from 'tailwind-merge'

defineOptions({ inheritAttrs: false })

const props = defineProps({
  orientation: {
    type: String,
    default: 'horizontal',
    validator: (value) => ['horizontal', 'vertical'].includes(value)
  }
})

const attrs = useAttrs()
const element = ref()
const isVertical = computed(() => props.orientation === 'vertical')
const baseClasses = computed(() =>
  isVertical.value ? 'w-px self-stretch' : 'h-px w-full'
)

const forwardedAttrs = computed(() => {
  const {
    class: _class,
    role: _role,
    'aria-orientation': _ariaOrientation,
    'data-orientation': _dataOrientation,
    'data-slot': _dataSlot,
    ...rest
  } = attrs
  return rest
})

defineExpose({ element })
</script>

<template>
  <component
    :is="isVertical ? 'div' : 'hr'"
    ref="element"
    v-bind="forwardedAttrs"
    data-slot="separator"
    :data-orientation="isVertical ? 'vertical' : 'horizontal'"
    :role="isVertical ? 'separator' : undefined"
    :aria-orientation="isVertical ? 'vertical' : undefined"
    :class="
      twMerge(
        'shrink-0 border-0 bg-gray-200 forced-colors:bg-current dark:bg-gray-800',
        baseClasses,
        attrs.class
      )
    "
  />
</template>
jsx
import { forwardRef } from 'react'
import { twMerge } from 'tailwind-merge'

const BASE_CLASSES =
  'shrink-0 border-0 bg-gray-200 forced-colors:bg-current dark:bg-gray-800'

const Separator = forwardRef(function Separator(
  {
    orientation = 'horizontal',
    className,
    role: _role,
    'aria-orientation': _ariaOrientation,
    'data-orientation': _dataOrientation,
    'data-slot': _dataSlot,
    ...props
  },
  ref
) {
  const isVertical = orientation === 'vertical'
  const Component = isVertical ? 'div' : 'hr'

  return (
    <Component
      {...props}
      ref={ref}
      data-slot="separator"
      data-orientation={isVertical ? 'vertical' : 'horizontal'}
      role={isVertical ? 'separator' : undefined}
      aria-orientation={isVertical ? 'vertical' : undefined}
      className={twMerge(
        BASE_CLASSES,
        isVertical ? 'w-px self-stretch' : 'h-px w-full',
        className
      )}
    />
  )
})

export default Separator
svelte
<script>
  import { twMerge } from "tailwind-merge";

  const BASE_CLASSES =
    "shrink-0 border-0 bg-gray-200 forced-colors:bg-current dark:bg-gray-800";

  let {
    orientation = "horizontal",
    class: className,
    role: _role,
    "aria-orientation": _ariaOrientation,
    "data-orientation": _dataOrientation,
    "data-slot": _dataSlot,
    ...props
  } = $props();

  let element = $state();

  export function getElement() {
    return element;
  }
</script>

{#if orientation === "vertical"}
  <div
    {...props}
    bind:this={element}
    data-slot="separator"
    data-orientation="vertical"
    role="separator"
    aria-orientation="vertical"
    class={twMerge(BASE_CLASSES, "w-px self-stretch", className)}
  ></div>
{:else}
  <hr
    {...props}
    bind:this={element}
    data-slot="separator"
    data-orientation="horizontal"
    class={twMerge(BASE_CLASSES, "h-px w-full", className)}
  />
{/if}
  • Menu and Command — may contain rare visual group boundaries while retaining their own keyboard behavior.
  • Tabs — owns peer navigation and active indication rather than composing those from separators.
  • Card — provides a surface boundary when a line alone is not enough.
  • Table — preserves dense row and column relationships where native borders and divisions remain appropriate.
  • Breadcrumb — uses its own presentational path markers; those are not thematic separators.

All open source projects are released under the MIT License.