Skip to content

Divider

Horizontal and vertical rules in three weights, with an optional interrupting label.

Layoutstarterlayoutseparatorruledivider

Live preview

full widthLive preview — open it in a new tab for the full-height version.
Open the preview in a new tab

Source

The exact file rendered in the preview above.

import { Divider } from '@/components/ui/layout'
import { DemoColumn, DemoStage } from './_kit'

export default function DividerDemo() {
  return (
    <DemoStage>
      <DemoColumn width="lg">
        <div className="space-y-3">
          <p className="text-sm text-ink-muted">Above a subtle rule.</p>
          <Divider weight="subtle" />
          <p className="text-sm text-ink-muted">Above a default rule.</p>
          <Divider />
          <p className="text-sm text-ink-muted">Above a strong rule.</p>
          <Divider weight="strong" />
        </div>

        <Divider label="or continue with" />

        <div className="flex h-16 items-center gap-4 rounded-lg border border-line px-4">
          <span className="text-sm text-ink-muted">Left</span>
          <Divider orientation="vertical" />
          <span className="text-sm text-ink-muted">Middle</span>
          <Divider orientation="vertical" />
          <span className="text-sm text-ink-muted">Right</span>
        </div>
      </DemoColumn>
    </DemoStage>
  )
}

components/demos/divider.tsx

import { Divider } from '@/components/ui/layout'
import { DemoColumn, DemoStage } from './_kit'

export default function DividerDemo() {
  return (
    <DemoStage>
      <DemoColumn width="lg">
        <div className="space-y-3">
          <p className="text-sm text-ink-muted">Above a subtle rule.</p>
          <Divider weight="subtle" />
          <p className="text-sm text-ink-muted">Above a default rule.</p>
          <Divider />
          <p className="text-sm text-ink-muted">Above a strong rule.</p>
          <Divider weight="strong" />
        </div>

        <Divider label="or continue with" />

        <div className="flex h-16 items-center gap-4 rounded-lg border border-line px-4">
          <span className="text-sm text-ink-muted">Left</span>
          <Divider orientation="vertical" />
          <span className="text-sm text-ink-muted">Middle</span>
          <Divider orientation="vertical" />
          <span className="text-sm text-ink-muted">Right</span>
        </div>
      </DemoColumn>
    </DemoStage>
  )
}

components/ui/layout.tsx

import type { ElementType, HTMLAttributes, ReactNode } from 'react'
import { cn } from '@/lib/cn'

/**
 * Layout primitives
 *
 * Five zero-JavaScript building blocks that account for the majority of
 * structure in the library. They exist so that spacing decisions are made once,
 * against density tokens, instead of being re-typed as ad-hoc utilities in
 * every section.
 */

export interface ContainerProps extends HTMLAttributes<HTMLDivElement> {
  /** `prose` narrows to a comfortable reading measure. */
  size?: 'prose' | 'narrow' | 'default' | 'wide' | 'full'
  as?: ElementType
  /** Removes the responsive horizontal gutter. */
  bleed?: boolean
}

const containerSizes = {
  prose: 'max-w-[var(--layout-prose-max)]',
  narrow: 'max-w-3xl',
  default: 'max-w-[var(--layout-content-max)]',
  wide: 'max-w-[110rem]',
  full: 'max-w-none',
} as const

export function Container({
  size = 'default',
  as: Tag = 'div',
  bleed = false,
  className,
  children,
  ...props
}: ContainerProps) {
  return (
    <Tag
      className={cn(
        'mx-auto w-full',
        containerSizes[size],
        !bleed && 'px-4 sm:px-6 lg:px-8',
        className,
      )}
      {...props}
    >
      {children}
    </Tag>
  )
}

export interface StackProps extends HTMLAttributes<HTMLDivElement> {
  direction?: 'row' | 'column'
  gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'
  align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline'
  justify?: 'start' | 'center' | 'end' | 'between' | 'around'
  wrap?: boolean
  as?: ElementType
}

const gapMap = {
  none: 'gap-0',
  xs: 'gap-1',
  sm: 'gap-2',
  md: 'gap-gap',
  lg: 'gap-stack',
  xl: 'gap-8',
} as const

const alignMap = {
  start: 'items-start',
  center: 'items-center',
  end: 'items-end',
  stretch: 'items-stretch',
  baseline: 'items-baseline',
} as const

const justifyMap = {
  start: 'justify-start',
  center: 'justify-center',
  end: 'justify-end',
  between: 'justify-between',
  around: 'justify-around',
} as const

export function Stack({
  direction = 'column',
  gap = 'md',
  align,
  justify,
  wrap = false,
  as: Tag = 'div',
  className,
  children,
  ...props
}: StackProps) {
  return (
    <Tag
      className={cn(
        'flex',
        direction === 'column' ? 'flex-col' : 'flex-row',
        gapMap[gap],
        align && alignMap[align],
        justify && justifyMap[justify],
        wrap && 'flex-wrap',
        className,
      )}
      {...props}
    >
      {children}
    </Tag>
  )
}

export interface GridProps extends HTMLAttributes<HTMLDivElement> {
  /** Column count at the largest breakpoint; smaller breakpoints step down. */
  cols?: 1 | 2 | 3 | 4 | 5 | 6
  gap?: StackProps['gap']
  as?: ElementType
  /** Auto-fit tracks with a minimum width instead of a fixed column count. */
  minItemWidth?: string
}

const colMap = {
  1: 'grid-cols-1',
  2: 'grid-cols-1 sm:grid-cols-2',
  3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
  4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
  5: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-5',
  6: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-6',
} as const

export function Grid({
  cols = 3,
  gap = 'lg',
  as: Tag = 'div',
  minItemWidth,
  className,
  style,
  children,
  ...props
}: GridProps) {
  return (
    <Tag
      className={cn('grid', minItemWidth ? undefined : colMap[cols], gapMap[gap], className)}
      style={
        minItemWidth
          ? {
              ...style,
              gridTemplateColumns: `repeat(auto-fit, minmax(min(${minItemWidth}, 100%), 1fr))`,
            }
          : style
      }
      {...props}
    >
      {children}
    </Tag>
  )
}

export interface DividerProps extends HTMLAttributes<HTMLDivElement> {
  orientation?: 'horizontal' | 'vertical'
  /** Renders a centred text label interrupting the rule. */
  label?: ReactNode
  weight?: 'subtle' | 'default' | 'strong'
}

const dividerWeight = {
  subtle: 'border-line-subtle',
  default: 'border-line',
  strong: 'border-line-strong',
} as const

export function Divider({
  orientation = 'horizontal',
  label,
  weight = 'default',
  className,
  ...props
}: DividerProps) {
  if (orientation === 'vertical') {
    return (
      <div
        role="separator"
        aria-orientation="vertical"
        className={cn('h-full w-px self-stretch border-l', dividerWeight[weight], className)}
        {...props}
      />
    )
  }

  if (label) {
    return (
      <div className={cn('flex items-center gap-3', className)} {...props}>
        <span className={cn('h-px flex-1 border-t', dividerWeight[weight])} role="separator" />
        <span className="label-caps text-ink-subtle">{label}</span>
        <span className={cn('h-px flex-1 border-t', dividerWeight[weight])} aria-hidden="true" />
      </div>
    )
  }

  return (
    <div
      role="separator"
      className={cn('w-full border-t', dividerWeight[weight], className)}
      {...props}
    />
  )
}

Demo source — adapt to your project. Foundry is not published as a package.

Usage

Foundry leans on lines rather than shadows, so the three weights matter: subtle inside a panel, default between panels, strong for structural boundaries.

  • The labelled variant is the "or continue with" pattern in authentication forms.
  • A vertical divider needs a parent with a definite height, usually a flex row.

Variants and states

Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.

  • Subtle
  • Default
  • Strong
  • Vertical
  • Labelled

Accessibility

Role
Rendered with `role="separator"` and, for vertical rules, `aria-orientation`.
Labelled rules
A labelled divider keeps its text in the accessibility tree while the rules either side are hidden.

Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.