Skip to content
Facade UI
Colour mode
GitHub (opens in a new tab)
uiatom

Logo mark

One customer logo, optically sized and optionally linked, with the company name exposed as text.

Preview

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

Installation

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

Pulls in types, utils. The CLI installs them for you.

Usage

The exact source of the preview above.

demos/logo-mark.tsx
import { LogoMark } from "@registry/ui/logo-mark"

const logos = ["Northwind", "Contoso", "初音 Labs", "Umbrella", "Globex"]

export function Demo() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-x-10 gap-y-6">
      {logos.map((name) => (
        <LogoMark key={name} name={name} />
      ))}
    </div>
  )
}

Source

What the CLI copies into your project, byte for byte.

components/ui/logo-mark.tsx
/**
 * LogoMark — one customer or partner logo, optionally linked.
 *
 * Handles the two things logo walls always need: a consistent optical size
 * across wildly different aspect ratios (height-constrained, width-auto), and a
 * desaturated resting state that does not also wash out the dark theme.
 *
 * a11y: a logo in a "trusted by" wall is decorative *as an image* — the company
 * name is the information. So the image carries `alt=""` and the name is exposed
 * as visually hidden text, which keeps the wall readable without a screen reader
 * announcing "image, Acme logo" for every item. Set `showName` to make it
 * visible instead.
 *
 * The `muted` resting treatment (desaturate and fade) is applied to the image
 * only, never to a shared wrapper. On the wrapper it also dims the text
 * fallback and the visible name, pushing both under 4.5:1 — which the axe run
 * caught, and which is why the class list below is split the way it is.
 *
 * Dependencies: react, @/lib/types, @/lib/utils.
 */

import type { ElementType } from "react"

import type { ImageComponent, LinkComponent } from "@/lib/types"
import { cn } from "@/lib/utils"

export interface LogoItem {
  /** Company name. Always exposed to assistive tech. */
  name: string
  src?: string
  width?: number
  height?: number
  /** Links the logo. Omit for a non-interactive wall. */
  href?: string
}

export interface LogoMarkProps extends LogoItem {
  /** Drop-in for `next/image`. Defaults to `"img"`. */
  image?: ImageComponent
  /** Drop-in for `next/link`. Defaults to `"a"`. */
  link?: LinkComponent
  /** Rendered height in px. Width follows the intrinsic ratio. */
  size?: "sm" | "md" | "lg"
  /** Desaturate until hover/focus. Default `true`. */
  muted?: boolean
  /** Show the name as text instead of hiding it. */
  showName?: boolean
  className?: string
}

const heights = { sm: "h-6", md: "h-8", lg: "h-10" } as const

export function LogoMark({
  name,
  src,
  href,
  width,
  height,
  image,
  link,
  size = "md",
  muted = true,
  showName = false,
  className,
}: LogoMarkProps) {
  const Image = (image ?? "img") as ElementType
  const Link = (link ?? "a") as ElementType

  // Only ever applied to the image. Text keeps its full contrast.
  const imageTreatment = cn(
    "w-auto object-contain",
    heights[size],
    muted &&
      "duration-facade-base ease-facade-out opacity-70 grayscale transition-[opacity,filter] group-hover:opacity-100 group-hover:grayscale-0 group-focus-visible:opacity-100 group-focus-visible:grayscale-0",
  )

  const content = src ? (
    <>
      <Image
        src={src}
        alt=""
        width={width}
        height={height}
        loading="lazy"
        decoding="async"
        className={imageTreatment}
      />
      <span
        className={showName ? "text-muted-foreground text-sm font-medium" : "sr-only"}
      >
        {name}
      </span>
    </>
  ) : (
    <span
      className={cn(
        "text-muted-foreground text-lg font-semibold tracking-tight",
        heights[size],
        "flex items-center",
      )}
    >
      {name}
    </span>
  )

  const shared = cn("group inline-flex items-center gap-2", className)

  if (!href) {
    return <span className={shared}>{content}</span>
  }

  return (
    <Link
      href={href}
      className={cn(
        shared,
        "focus-visible:ring-ring focus-visible:ring-offset-background rounded-md outline-none focus-visible:ring-2 focus-visible:ring-offset-4",
      )}
    >
      {content}
    </Link>
  )
}

Props

LogoMarkProps

Extends LogoItem.

Props for LogoMarkProps
PropTypeDefault
imageDrop-in for `next/image`. Defaults to `"img"`.ImageComponent
linkDrop-in for `next/link`. Defaults to `"a"`.LinkComponent
sizeRendered height in px. Width follows the intrinsic ratio."sm" | "md" | "lg""md"
mutedDesaturate until hover/focus. Default `true`.booleantrue
showNameShow the name as text instead of hiding it.booleanfalse
classNamestring