← All components

Separator

Half a pixel of translucent grey, decorative unless it divides something.

Mihir Modi

Design engineer.

or

The idea

Half a pixel in the alpha border grey, so it is one device pixel on a retina screen and reads on page, card or glass without becoming a painted stripe. Most lines only confirm a gap that spacing already made, so decorative is the default and a screen reader is not told about them. A label breaks the line around itself instead of sitting on a patch of background, so it works on any surface.

Composes Button.

Install

terminal1 line
npm i mihirmodi-ui motion

The tokens, the theme script and the routing provider are a one-time job — see Setup. Or skip the package entirely and take the source at the foot of this page.

Usage

Separator — usage13 lines
import { Separator } from "mihirmodi-ui";

<Separator className="my-4" />

<nav className="flex h-4 items-center gap-3">
  <a href="/work">Work</a>
  <Separator orientation="vertical" />
  <a href="/writing">Writing</a>
</nav>

// Decorative by default. When the line really divides content,
// make it a separator a screen reader announces.
<Separator label="or" decorative={false} />

Props

PropNotes
orientation"horizontal" | "vertical"Vertical stretches to its flex row.
decorativeboolean`false` makes it a real `separator` with an orientation.
labelReactNodeA word held in the middle — “or”, a date.

Source

Exactly what the package ships — this is read out of the source at build time, not a copy. Take it and adapt it if you would rather not add a dependency.

packages/ui/src/components/layout/Separator.tsx54 lines
import type { ReactNode } from "react";

export type SeparatorProps = {
  orientation?: "horizontal" | "vertical";
  /**
   * `true` (the default) when the line only draws a gap that spacing already
   * makes, so assistive tech skips it. `false` when it divides content a
   * reader should hear divided — then it is a real `separator`.
   */
  decorative?: boolean;
  /** A word held in the middle of the line — "or", a date. Horizontal only. */
  label?: ReactNode;
  className?: string;
};

/**
 * A hairline between two things.
 *
 * Half a pixel in the translucent border grey, so it is a full device pixel on
 * a retina screen and reads on any surface it is laid over — page, card or
 * glass — without turning into a painted stripe. Most lines on a page are
 * decoration: spacing has already grouped the content and the rule only
 * confirms it, so that is the default, and a screen reader is not told about a
 * line it could not have needed.
 *
 * A label sits in the faint tier with the line broken around it, rather than
 * on a patch of background laid over the line, so it works on any surface.
 * On a decorative line the label is ordinary text and is read; on a semantic
 * one a separator's children are presentational, so a string label becomes
 * its accessible name instead.
 */
export function Separator({ orientation = "horizontal", decorative = true, label, className = "" }: SeparatorProps) {
  const a11y = decorative
    ? ({ role: "none" } as const)
    : ({ role: "separator", "aria-orientation": orientation } as const);

  if (orientation === "vertical") {
    return <div {...a11y} className={`w-0 self-stretch border-l-[0.5px] border-gray-alpha-400 ${className}`} />;
  }
  if (label) {
    return (
      <div
        {...a11y}
        aria-label={!decorative && typeof label === "string" ? label : undefined}
        className={`flex items-center gap-3 ${className}`}
      >
        <span aria-hidden className="h-0 flex-1 border-t-[0.5px] border-gray-alpha-400" />
        <span className="shrink-0 text-[12px] leading-none text-faint">{label}</span>
        <span aria-hidden className="h-0 flex-1 border-t-[0.5px] border-gray-alpha-400" />
      </div>
    );
  }
  return <div {...a11y} className={`h-0 w-full border-t-[0.5px] border-gray-alpha-400 ${className}`} />;
}