Skip to content
Facade UI
uiatom

Eyebrow

The small label above a section heading. It is a paragraph, not a heading.

Open in v0 (opens in a new tab)

Preview

Open full width (opens in a new tab)
Preview width

Installation

Package manager
pnpm dlx shadcn@latest add https://facadeui.dev/r/eyebrow.json

This component needs utils. The CLI installs them for you.

Usage

This is the code for the preview above. To install it as a file, run npx shadcn@latest add @facade/eyebrow-demo.

demos/eyebrow.tsx
import { Eyebrow } from "@registry/ui/eyebrow"
import { Heading } from "@registry/ui/heading"

export function Demo() {
  return (
    <div className="flex flex-col gap-8">
      <div className="flex flex-col gap-2">
        <Eyebrow>Platform</Eyebrow>
        <Heading level={2} className="text-display-sm font-semibold">
          One place for everything
        </Heading>
      </div>
      <div className="flex flex-col gap-2">
        <Eyebrow tone="primary">Changelog</Eyebrow>
        <Heading level={2} className="text-display-sm font-semibold">
          Shipped this week
        </Heading>
      </div>
    </div>
  )
}

Source

These are the files the CLI copies into your project.

components/ui/eyebrow.tsx
/**
 * Eyebrow — the small label that sits above a section heading.
 *
 * The `primary` tone puts the brand colour in a tick, not in the letters: a
 * brand colour need only clear 3:1 as a shape, but 4.5:1 as 14px text.
 *
 * a11y: never a heading. An eyebrow reads as a category, not as an outline
 * level, and promoting it to `<h3>` above an `<h2>` would invert the document
 * structure for screen-reader users navigating by heading. It renders a `<p>`
 * by default, or a `<span>` when it is already inside flow text. The tick is a
 * pseudo-element, so it is not announced.
 *
 * Dependencies: react, @/lib/utils.
 */

import type { ComponentPropsWithoutRef, ElementType, ReactNode } from "react"

import { cn } from "@/lib/utils"

export interface EyebrowProps extends Omit<ComponentPropsWithoutRef<"p">, "children"> {
  as?: "p" | "span" | "div"
  /** `muted` is the default; `primary` leads with a tick in the brand colour. */
  tone?: "muted" | "primary" | "foreground"
  children?: ReactNode
}

const tones: Record<NonNullable<EyebrowProps["tone"]>, string> = {
  muted: "text-muted-foreground",
  // Inline, so the tick follows the text's alignment and its first line.
  primary:
    "text-foreground before:bg-primary before:mr-[0.75em] before:inline-block before:size-[0.6em] before:rounded-[0.125em] before:content-['']",
  foreground: "text-foreground",
}

export function Eyebrow({
  as = "p",
  tone = "muted",
  className,
  children,
  ...props
}: EyebrowProps) {
  const Component = as as ElementType

  return (
    <Component
      className={cn(
        "text-sm font-semibold uppercase tracking-[0.12em]",
        tones[tone],
        className,
      )}
      {...props}
    >
      {children}
    </Component>
  )
}

Props

EyebrowProps

Extends Omit<ComponentPropsWithoutRef<"p">, "children">.

Props for EyebrowProps
PropTypeDefault
as"p" | "span" | "div""p"
tone`muted` is the default; `primary` leads with a tick in the brand colour."muted" | "primary" | "foreground""muted"
childrenReactNode—