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.
Installation
One command detects Vue, React, or Svelte and writes the matching one-file source into the conventional component directory:
Run one command from a Boring Stack application. Klean detects the framework and conventional destination, then adds the framework-native source and its direct dependencies.
npx klean-ui add separator- No initializer or configuration file
- No framework, alias, or theme questions
- No Klean runtime dependency
There is no initializer, provider, klean-ui.json, class helper, barrel file, orientation package, or runtime Klean dependency.
Usage
Use Separator between adjacent regions only when the boundary carries meaning that spacing alone does not communicate.
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>
React
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>
API
| Input | Default | Purpose |
|---|---|---|
orientation | horizontal | Semantic 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. |
There is no variant, tone, color, size, thickness, length, decorative, inset, or theme API. Those decisions are Tailwind classes or native attributes at the call site.
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:
<!-- 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.
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:
<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:
<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.
Hagfish and Slipway recipes
Hagfish can replace its PrimeVue-backed Divider with native Klean source and preserve its expressive contrast. Slipway can keep compact command and operational boundaries. Most existing borders in both applications should remain ordinary Tailwind markup.
The component is not a migration target for every border-t, border-b, divide-y, table row, field underline, card edge, or Tabs indicator. Those lines belong to the element whose shape they describe.
Accessibility
- Prefer native HTML. Horizontal Separator is an
hrand keeps its implicit separator role. - Do not redundantly add
role="separator"oraria-orientation="horizontal"to the nativehr. - Vertical Separator renders
role="separator"witharia-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-yon 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 source
<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>
React source
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 source
<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}
Related components
- 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.