/** * Separator — a rule between content. * * React Native has no `
` and no border shorthand that survives a flex row, * so a separator here is a view with a size on one axis and a stretch on the * other. That is why orientation is a prop rather than something the layout * infers: the component has to know which axis carries the thickness. * * ```tsx * * * or * ``` * * A vertical separator takes its length from the parent, so the parent needs a * height — inside a `flex-row` with `items-stretch`, or with an explicit `h-*` * on either the row or the separator itself. Without one it measures zero and * nothing draws. * * Passing children turns it into a *labelled* separator: the rule breaks around * centred content — the "or continue with" divider in a sign-in form. Only the * horizontal axis carries a label, because a stacked word is not a divider; a * vertical separator ignores its children and stays a plain hairline. */ import { forwardRef, isValidElement, type ReactNode } from 'react'; import { View, type ViewProps } from 'react-native'; import { tv, type VariantProps } from 'tailwind-variants'; import { Text } from '../../primitives/text'; import { cn } from '../../utils/cn'; const separatorVariants = tv({ base: 'bg-border', variants: { orientation: { horizontal: 'w-full', vertical: 'h-full', }, variant: { thin: '', thick: '', }, }, compoundVariants: [ // Thickness is on whichever axis is *not* the length, so it cannot be // expressed by the orientation and variant classes independently. { orientation: 'horizontal', variant: 'thin', class: 'h-px' }, { orientation: 'horizontal', variant: 'thick', class: 'h-1' }, { orientation: 'vertical', variant: 'thin', class: 'w-px' }, { orientation: 'vertical', variant: 'thick', class: 'w-1' }, ], defaultVariants: { orientation: 'horizontal', variant: 'thin', }, }); export interface SeparatorProps extends ViewProps, VariantProps { className?: string; /** * Thickness in pixels, overriding the variant. Sets the height of a * horizontal separator and the width of a vertical one. */ thickness?: number; /** * Whether the separator is only visual. A decorative separator is skipped by * screen readers; set false when the split itself carries meaning — between * two groups of menu items, say — and it is announced instead. A labelled * separator is never decorative: its label is content, so it is always read. */ decorative?: boolean; /** * Optional label sitting in the break of a horizontal rule — the "or" * between two sign-in paths. A bare string is wrapped in the muted label * style; an element is rendered as-is. Ignored on the vertical axis, where a * label is not a divider. */ children?: ReactNode; /** Styles the label text of a labelled separator. */ labelClassName?: string; } export const Separator = forwardRef( ( { className, labelClassName, orientation = 'horizontal', variant, thickness, decorative = true, children, style, ...props }, ref ) => { const hasLabel = orientation === 'horizontal' && children != null && children !== false; // Labelled: two rules flanking centred content. The label carries meaning, // so the row announces itself as a separator regardless of `decorative`, // and the flanking rules are hidden from the reader. Here `className` // styles the wrapping row and `labelClassName` the text. if (hasLabel) { const rule = separatorVariants({ orientation, variant }); const ruleStyle = thickness !== undefined ? { height: thickness } : undefined; return ( {isValidElement(children) ? ( children ) : ( {children} )} ); } return ( ); } ); Separator.displayName = 'Separator'; const styles = { flex: { flex: 1 } } as const;