Logo cloud parts: a labelled list laid out as a centred grid, a single wrapping baseline, two columns or muted tiles. Each logo takes an img, svg or text wordmark at three sizes, with a greyscale muted variant.
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
// grid: centered cells, one row at xl (six fit at 1280). wrap: one baseline.
// columns: two start-aligned columns. tiles: muted tiles at the tile gap,
// the mark above the name. grid and columns drop to one column below 360px.
const logoGroupVariants = cva("group/logo-group mx-auto w-full", {
variants: {
/**
* The layout. `grid` (the default) centers each logo in a cell: one
* column below 360px, two from 360px, three from the 640px `sm`
* breakpoint, and all on one row from the 1280px `xl` breakpoint. `wrap`
* flows the logos along one baseline and wraps them. `columns` sets two
* start-aligned columns, one below 360px. `tiles` sets each logo on its
* own shaded tile with its contents stacked and centered, in two columns,
* three from the 768px `md` breakpoint.
*/
variant: {
grid: "grid grid-cols-1 place-items-center gap-x-6 gap-y-10 min-[22.5rem]:grid-cols-2 sm:grid-cols-3 sm:gap-x-10 xl:inline-grid xl:auto-cols-fr xl:grid-flow-col xl:grid-cols-1 xl:gap-12",
wrap: "flex flex-wrap items-baseline gap-x-8 gap-y-3",
columns:
"grid grid-cols-1 items-center justify-items-start gap-x-8 gap-y-12 min-[22.5rem]:grid-cols-2",
tiles: "grid grid-cols-2 gap-(--tile-gap) md:grid-cols-3",
},
},
defaultVariants: { variant: "grid" },
})
// Preflight strips list markers, so role="list" keeps the count announced; the
// kit's convention is that the list is also named (aria-label or -labelledby).
type LogoGroupLabel =
| {
/**
* The list's name for screen readers. Pass this or `aria-labelledby`,
* not both.
*/
"aria-label": string
/**
* The `id` of an element whose text names the list for screen readers.
* Pass this or `aria-label`, not both.
*/
"aria-labelledby"?: never
}
| {
/**
* The `id` of an element whose text names the list for screen readers.
* Pass this or `aria-label`, not both.
*/
"aria-labelledby": string
/**
* The list's name for screen readers. Pass this or `aria-labelledby`,
* not both.
*/
"aria-label"?: never
}
/**
* A list of customer or partner logos, rendered as a `<ul>`. Put a `Logo`
* inside for each one. It requires a name for screen readers: pass
* `aria-label` or `aria-labelledby`.
*
* `grid` (the default) centers each logo in a cell and sets them all on one
* row from the `xl` breakpoint. `wrap` lets them flow along one baseline.
* `columns` sets two start-aligned columns. `tiles` sets each logo on its own
* shaded tile, with a mark stacked above the name. `grid` and `columns` drop
* to one column on screens narrower than 360px.
*/
function LogoGroup({
className,
variant = "grid",
render,
...props
}: Omit<useRender.ComponentProps<"ul">, "aria-label" | "aria-labelledby"> &
VariantProps<typeof logoGroupVariants> &
LogoGroupLabel) {
return useRender({
defaultTagName: "ul",
render,
state: { slot: "logo-group", variant },
props: mergeProps<"ul">(
{
role: "list",
className: cn(logoGroupVariants({ variant }), className),
},
props
),
})
}
// muted steps a wordmark's ink down to 80% opacity and an img to full greyscale
// at 80% opacity; an inline svg keeps its own fills. An aria-hidden svg is a
// mark beside the name, sized to 1.2em of it; any other svg or img is the
// wordmark, as tall as the size and never wider than its cell. In a columns
// or tiles group the name wraps; a tile pads and paints its own cell.
const logoVariants = cva(
"flex max-w-full min-w-0 items-center justify-center gap-2 whitespace-nowrap group-data-[variant=columns]/logo-group:h-auto group-data-[variant=columns]/logo-group:wrap-normal group-data-[variant=columns]/logo-group:whitespace-normal group-data-[variant=tiles]/logo-group:h-auto group-data-[variant=tiles]/logo-group:flex-col group-data-[variant=tiles]/logo-group:gap-4 group-data-[variant=tiles]/logo-group:rounded-md group-data-[variant=tiles]/logo-group:bg-foreground/2.5 group-data-[variant=tiles]/logo-group:px-4 group-data-[variant=tiles]/logo-group:py-8 group-data-[variant=tiles]/logo-group:text-center group-data-[variant=tiles]/logo-group:wrap-normal group-data-[variant=tiles]/logo-group:whitespace-normal dark:group-data-[variant=tiles]/logo-group:bg-foreground/5 *:[img]:h-(--logo-h) *:[img]:w-auto *:[img]:max-w-full *:[img]:object-contain *:[svg]:h-(--logo-h) *:[svg]:w-auto *:[svg]:max-w-full *:[svg[aria-hidden=true]]:h-[1.2em] *:[svg[aria-hidden=true]]:shrink-0",
{
variants: {
/**
* The logo's weight and color. `default` (the default) sets the name in
* bold. `muted` sets it in regular weight in `foreground` at 80%
* opacity, and turns an `<img>` placed directly inside greyscale at 80%
* opacity.
*/
variant: {
default: "font-bold",
muted:
"font-normal text-foreground/80 *:[img]:opacity-80 *:[img]:grayscale",
},
/**
* The logo's height and text size. `sm` is 24px tall with 16px text,
* `md` (the default) 32px with 20px text, and `lg` 40px with 24px
* text. An `<img>` or `<svg>` wordmark placed directly inside takes the
* same height; an `<svg aria-hidden="true">` mark stays 1.2em. In a
* `columns` or `tiles` group the item grows with its content, and the
* wordmark keeps the height.
*/
size: {
sm: "h-6 text-base [--logo-h:--spacing(6)]",
md: "h-8 text-xl [--logo-h:--spacing(8)]",
lg: "h-10 text-2xl [--logo-h:--spacing(10)]",
},
},
defaultVariants: { variant: "default", size: "md" },
}
)
/**
* One logo inside `LogoGroup`, rendered as an `<li>`: an `<img>`, an inline
* `<svg>` or the company's name as text. An `<img>` or `<svg>` placed
* directly inside is the wordmark: as tall as `size` and never wider than its
* cell. Give an `<img>` its `alt` text.
*
* For a mark beside the name, add an `<svg aria-hidden="true">` next to the
* text; it sizes to 1.2em. `muted` turns the text down to 80% opacity and an
* `<img>` to greyscale at 80% opacity.
*/
function Logo({
className,
variant = "default",
size = "md",
render,
...props
}: useRender.ComponentProps<"li"> & VariantProps<typeof logoVariants>) {
return useRender({
defaultTagName: "li",
render,
state: { slot: "logo", variant, size },
props: mergeProps<"li">(
{ className: cn(logoVariants({ variant, size }), className) },
props
),
})
}
export { Logo, LogoGroup }
pnpm dlx shadcn add @01orbital/logonpx shadcn add @01orbital/logoyarn shadcn add @01orbital/logobunx --bun shadcn add @01orbital/logoFree under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.
It lands at components/editorial/logo.tsx, as source you own and edit.
import { Logo, LogoGroup } from "@/components/editorial/logo"
const COPY = {
label: "Trusted by",
zobeide: "Zobeide",
octavia: "Octavia",
ersilia: "Ersilia",
}
// A wordmark drawn as an svg, a mark beside text, and plain text.
export default function LogoDemo() {
return (
<LogoGroup aria-label={COPY.label} className="max-w-3xl">
<Logo>
<svg viewBox="0 0 120 32" role="img" aria-label={COPY.zobeide}>
<circle cx="16" cy="16" r="12" fill="currentColor" />
<rect
x="36"
y="12"
width="76"
height="8"
rx="4"
fill="currentColor"
/>
</svg>
</Logo>
<Logo>
<svg viewBox="0 0 32 32" aria-hidden="true">
<path d="M16 2 30 30H2Z" fill="currentColor" />
</svg>
{COPY.octavia}
</Logo>
<Logo>{COPY.ersilia}</Logo>
</LogoGroup>
)
}Logo is built from 2 parts. In the example they nest like this:
LogoGroup
└─ LogoLogoGroup: A list of customer or partner logos, rendered as a <ul>. Put a Logo inside for each one. It requires a name for screen readers: pass aria-label or aria-labelledby.Logo: One logo inside LogoGroup, rendered as an <li>: an <img>, an inline <svg> or the company's name as text. An <img> or <svg> placed directly inside is the wordmark: as tall as size and never wider than its cell. Give an <img> its alt text.A list of customer or partner logos, rendered as a <ul>. Put a Logo inside for each one. It requires a name for screen readers: pass aria-label or aria-labelledby.
grid (the default) centers each logo in a cell and sets them all on one row from the xl breakpoint. wrap lets them flow along one baseline. columns sets two start-aligned columns. tiles sets each logo on its own shaded tile, with a mark stacked above the name. grid and columns drop to one column on screens narrower than 360px.
Prop
Type
It also accepts the props of the Base UI part it wraps, and the HTML attributes of the element it renders.
One logo inside LogoGroup, rendered as an <li>: an <img>, an inline <svg> or the company's name as text. An <img> or <svg> placed directly inside is the wordmark: as tall as size and never wider than its cell. Give an <img> its alt text.
For a mark beside the name, add an <svg aria-hidden="true"> next to the text; it sizes to 1.2em. muted turns the text down to 80% opacity and an <img> to greyscale at 80% opacity.
Prop
Type
It also accepts the props of the Base UI part it wraps, and the HTML attributes of the element it renders.
npm: @base-ui/react, class-variance-authority, cn. The CLI installs them for you.
No other registry pieces.
Heading
Section heading in the display face, Hanken Grotesk through the --font-display variable, at four sizes that step up with the Section container. Renders an h2 by default; the tag can be swapped.
Milestones
Roadmap or company history as an ordered list on Grid, in grid or rows layout. Rules can be off, a top border per item, or one fading horizon line with dots; the current milestone's dot is filled.