HextaUI

Hairline

Borders and rings exactly one device pixel wide on every screen, through one variable the whole theme reads.

1px
inset-ring-1
hairline
inset-ring-(length:--hairline)
export function HairlineDemo() {
  return (
    <div className="grid w-full max-w-sm grid-cols-2 gap-4 text-sm">
      <div className="flex flex-col gap-2">
        <div className="flex h-24 items-center justify-center rounded-lg inset-ring-1 inset-ring-foreground/40">
          1px
        </div>
        <span className="text-center text-xs text-muted-foreground">
          inset-ring-1
        </span>
      </div>
      <div className="flex flex-col gap-2">
        <div className="flex h-24 items-center justify-center rounded-lg inset-ring-(length:--hairline) inset-ring-foreground/40">
          hairline
        </div>
        <span className="text-center text-xs text-muted-foreground">
          inset-ring-(length:--hairline)
        </span>
      </div>
    </div>
  )
}

Hairline is part of the HextaUI theme. If you've added the theme or any component, you have it.

pnpm dlx shadcn@latest add https://hextaui.com/r/theme.json

Or add it to your global CSS by hand.

app/globals.css
@theme inline {
  --default-border-width: var(--hairline);
}

@utility border-hairline {
  border-width: var(--hairline);
}

:root {
  --hairline: 1px;

  @media (min-resolution: 1.5dppx) {
    --hairline: calc(1px / 1.5);
  }

  @media (min-resolution: 2dppx) {
    --hairline: 0.5px;
  }

  @media (min-resolution: 3dppx) {
    --hairline: calc(1px / 3);
  }

  @media (min-resolution: 4dppx) {
    --hairline: 0.25px;
  }
}
<div className="border" />
<ul className="divide-y" />
<div className="border-2 sm:border-hairline" />
<button className="inset-ring-(length:--hairline) inset-ring-border" />
<hr className="h-(--hairline) border-0 bg-border" />

You rarely need to reach for it. The theme makes it the default border width, so border, border-t and divide-y all draw hairlines. Use the variable directly where Tailwind has no default, like rings, outlines and lines drawn with a background.

A CSS pixel covers two or three physical pixels on most screens, so a 1px border is really two or three device pixels thick. --hairline divides by the display's pixel ratio so every line is exactly one device pixel, the way native apps draw separators. On a standard display it stays 1px.

Pixel ratioDescription
1×1px
1.5×0.667px
2×0.5px
3×0.333px
4×0.25px

Lists and cards

Plain border and divide-y. Thinner separators group rows without boxing them in.

Plan
Pro
Seats
12
Billing
Yearly
Renews
Mar 4, 2027
const rows = [
  ["Plan", "Pro"],
  ["Seats", "12"],
  ["Billing", "Yearly"],
  ["Renews", "Mar 4, 2027"],
]

export function HairlineList() {
  return (
    <dl className="w-full max-w-xs divide-y rounded-xl border text-sm">
      {rows.map(([term, value]) => (
        <div key={term} className="flex justify-between px-4 py-3">
          <dt className="text-muted-foreground">{term}</dt>
          <dd>{value}</dd>
        </div>
      ))}
    </dl>
  )
}
  • HextaUI components draw edges with inset-ring-(length:--hairline) rather than borders. An inset ring takes no layout space, so a state that changes the ring's color or width never shifts the content.
  • Hairlines are lighter on the page, so a border color tuned for 1px lines may look faint. The theme's --border is tuned for hairlines.
  • Use border-2 or wider for emphasis, like a selected card. Those stay in CSS pixels.
NameDescription
--hairlineOne device pixel, in CSS pixels.
border-hairlineSets border-width to --hairline.
--default-border-widthSet to --hairline, so border and divide default to it.