Horizontal Rule
A semantic horizontal rule with a container-width breakout variant that spans the --container token edge-to-edge regardless of parent padding, plus a full-width variant.
Installation
$
Usage
import { Hr } from "@/components/ui/hr"Examples
Default
width="full" fills the immediate content box. The default width="container" variant breaks out to span the full --container token edge-to-edge, ignoring any parent padding.
The quick brown fox jumps over the lazy dog.
A second paragraph, separated by the rule above.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| width | "container" | "full" | "container" | container breaks out edge-to-edge to the --container token width regardless of parent padding; full fills the immediate content box. |
| thickness | "hairline" | "hairline" | Paints 0.5px so the rule stays one device pixel on 2x displays. |
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/registry/lib/utils";
// Semantic horizontal rule. Variant-driven width so the same primitive can span
// the site container, the immediate content box, or a custom width.
//
// `width: "container"` is the key behavior: a hairline that spans the full
// `--container` token width, ignoring ANY interior/parent X padding. It does
// this by positioning, not by countering a hardcoded padding value: it breaks
// out to the viewport (`w-screen`), caps at `max-w-[var(--container)]`, and
// re-centers (`left-1/2 -translate-x-1/2`). Because every layout wrapper is
// horizontally centered, the rule lands exactly on the container's edges. It
// tracks `--container` automatically, so it's correct at 512px, 1024px, 2048px,
// etc., with no per-padding tuning.
//
// GOTCHA: because it paints wider than its parent, an ancestor with
// `overflow-hidden` (or `overflow-x-hidden`) BETWEEN this and the container will
// clip the overhang: the layout box still measures full width (so the inspector
// looks right) but the visible line is clipped back to the padded content box.
// If the rule looks inset, check for a clipping ancestor, not this component.
// A full-container rule at 0.5px needs a step more contrast than
// component-internal borders; border-strong is neutral-500 at 40% instead
// of 30%, so it reads lighter on dark and darker on light from one value.
const hrVariants = cva("m-0 border-0 bg-border-strong", {
variants: {
width: {
// Edge to edge of the --container width, regardless of parent padding.
container:
"relative left-1/2 w-screen max-w-[var(--container)] -translate-x-1/2",
// Full width of the immediate (padded) content box.
full: "w-full",
},
thickness: {
// 0.5px paints one device pixel on 2x displays where 1px doubles to two;
// 1x displays round it to a normal or slightly lighter single pixel.
hairline: "h-[0.5px]",
},
},
defaultVariants: {
width: "container",
thickness: "hairline",
},
});
type HrProps = React.ComponentProps<"hr"> & VariantProps<typeof hrVariants>;
function Hr({ className, width, thickness, ...props }: HrProps) {
return (
<hr className={cn(hrVariants({ width, thickness }), className)} {...props} />
);
}
export { Hr, hrVariants };