macbetter

Content

Separator

Divide groups of content with a hairline.

Hollis Grant
hollis@macbetter.dev

Designs the billing and onboarding flows, and reviews every new pricing page.

import { Separator } from "macbetter/separator"

export default function Example() {
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 12, width: "100%", maxWidth: 360 }}>
      <div>
        <div style={{ fontWeight: 600 }}>Hollis Grant</div>
        <div style={{ color: "var(--mb-label-secondary)" }}>hollis@macbetter.dev</div>
      </div>
      <Separator />
      <p style={{ margin: 0 }}>Designs the billing and onboarding flows, and reviews every new pricing page.</p>
    </div>
  )
}

When to use it

Use a separator between groups that a reader should take as separate, like a profile's name and its bio, or between links in a row. Apple's separators are thin and quiet: one physical pixel in a translucent gray, so they divide without drawing the eye.

Prefer space and grouping first. Inset grouped lists, cards and sections already separate content; add a line only when space alone doesn't.

Usage

import { Separator, SeparatorLabel } from "macbetter/separator"

Vertical

Set orientation="vertical" between items in a row. It stretches to the row's height.

24Projects
1,280Followers
312Following
import { Separator } from "macbetter/separator"

const stats = [
  { value: "24", label: "Projects" },
  { value: "1,280", label: "Followers" },
  { value: "312", label: "Following" },
]

export default function Example() {
  return (
    <div style={{ display: "flex", gap: 20, alignItems: "center" }}>
      {stats.map((stat, index) => (
        <div key={stat.label} style={{ display: "contents" }}>
          {index > 0 ? <Separator orientation="vertical" /> : null}
          <div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
            <span style={{ fontWeight: 600, fontVariantNumeric: "tabular-nums" }}>{stat.value}</span>
            <span style={{ color: "var(--mb-label-secondary)" }}>{stat.label}</span>
          </div>
        </div>
      ))}
    </div>
  )
}

Inset

inset="leading" starts the line at the row's text instead of its edge, as between the rows of an iOS list. inset alone insets both ends. Both use the platform's row padding; set --mb-separator-inset to line up with text that sits after an icon.

PlanPro
Seats8 of 10
RenewsDecember 1, 2026
import { Separator } from "macbetter/separator"

const rows = [
  { title: "Plan", value: "Pro" },
  { title: "Seats", value: "8 of 10" },
  { title: "Renews", value: "December 1, 2026" },
]

export default function Example() {
  return (
    <div
      style={{
        width: "100%",
        maxWidth: 420,
        borderRadius: "var(--mb-group-radius)",
        background: "var(--mb-bg-grouped-secondary)",
        boxShadow: "0 0 0 0.5px var(--mb-separator)",
      }}
    >
      {rows.map((row, index) => (
        <div key={row.title}>
          {index > 0 ? <Separator inset="leading" /> : null}
          <div
            style={{
              display: "flex",
              justifyContent: "space-between",
              alignItems: "center",
              minHeight: "var(--mb-row-height)",
              paddingInline: "var(--mb-row-padding)",
            }}
          >
            <span>{row.title}</span>
            <span style={{ color: "var(--mb-label-secondary)" }}>{row.value}</span>
          </div>
        </div>
      ))}
    </div>
  )
}

With a label

SeparatorLabel puts a short word between two lines. The word carries the meaning, so the lines are hidden from screen readers.

or
import { Button } from "macbetter/button"
import { SeparatorLabel } from "macbetter/separator"

export default function Example() {
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 14, width: "100%", maxWidth: 320 }}>
      <Button variant="filled" block>
        Sign In with Passkey
      </Button>
      <SeparatorLabel>or</SeparatorLabel>
      <Button block>Email Me a Link</Button>
    </div>
  )
}

Platform behavior

The line is the same hairline everywhere. Its inset follows the platform's row padding: 10 points on macOS and 16 on iOS and iPadOS, so an inset separator lines up with list text on each.

Accessibility

  • A separator is announced with the separator role and its orientation, so a screen reader hears the groups as separate. It never takes focus.
  • Pass decorative when the surrounding content already shows the division; the separator gets role="none".
  • In Increase Contrast, the separator token darkens on its own.

API

Separator

PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"Which way the line runs.
insetboolean | "leading"—`leading` insets the start by the row padding; `true` insets both ends.
decorativebooleanfalseHides the line from assistive technology.

SeparatorLabel

PropTypeDefaultDescription
childrenReactNode—The label, like "or".
insetboolean—Insets both ends by the row padding.

Data attributes and variables

Attribute or variableOnDescription
data-orientation.mb-separatorhorizontal or vertical, from ui-sdk.
data-inset.mb-separatorboth or leading.
--mb-separator-color.mb-separatorThe line's color. Defaults to --mb-separator.
--mb-separator-inset.mb-separatorHow far an inset line starts from the edge.