Foundations
FoundationsTier 1 · Tokens
Brand marks
WoohlLogo, WoohlBird, WoohlWordmark, WoohlBullet and WoohlAppIcon, drawn from the canonical path data. Never rebuild the logo from text.
bunx --bun shadcn@latest add @woohl/brandPreview
Reference
Installs this item, its registry dependencies and the token layer.
bunx --bun shadcn@latest add @woohl/brandregistry/tier-1-tokens/brand/woohl-logo.tsx
import type { CSSProperties } from "react";
import {
BIRD_PATH,
BIRD_VIEWBOX,
LOCKUP,
WORDMARK_BOX,
WORDMARK_PATH,
} from "./marks";
import { LOGO_MIN_SIZE } from "@/registry/tier-1-tokens/lib/constants";
import { cn } from "@/registry/tier-2-primitives/lib/utils";
/** The Woohl logo in horizontal, stacked, or bird lockup. */
/* --------------------------------------------------------------- tones ---- */
/**
* The only colour treatments the logo is ever allowed.
*
* brand Woohl Orange. The default, on any light surface.
* paper Reversed. On ink, on Woohl Orange, on photography (over a scrim).
* ink Single-colour ink. Engraving, letterpress, one-plate print.
*/
export type LogoTone = "brand" | "paper" | "ink";
const TONE_COLOR: Record<LogoTone, string> = {
brand: "var(--color-brand-600)",
paper: "#FFFFFF",
ink: "var(--color-ink)",
};
/** The five approved lockups. `auto` picks between horizontal and bird by size. */
export type LogoVariant =
"horizontal" | "stacked" | "bird" | "wordmark" | "auto";
const HORIZONTAL_RATIO = 374 / 105;
/**
* The smallest each mark may actually render, whatever CSS asks for.
*
* `wordmark` is not a published number: LOGO_MIN_SIZE has no entry for it,
* and it takes the horizontal floor because the wordmark IS the reason the
* horizontal floor exists — the letterforms stop resolving before the bird
* does. Publish a real one and this should read it instead.
*/
const MIN_HEIGHT = {
horizontal: LOGO_MIN_SIZE.horizontal,
stacked: LOGO_MIN_SIZE.stacked,
bird: LOGO_MIN_SIZE.bird,
wordmark: LOGO_MIN_SIZE.horizontal,
} as const;
type BaseProps = {
/** Rendered height in px. Width follows the mark's fixed ratio. */
size?: number;
tone?: LogoTone;
className?: string;
style?: CSSProperties;
/**
* Accessible name. Defaults to "Woohl" on a lockup.
*
* Pass `title=""` when the logo is decorative — sitting next to a text link
* that already says "Woohl", for instance — and it is hidden from screen
* readers instead of read out twice.
*/
title?: string;
};
/* ---------------------------------------------------------------- bird ---- */
/**
* The bird alone. Favicons, app icons, avatars, watermarks, slide footers.
* The one mark that still reads at 16px.
*/
export function WoohlBird({
size = 32,
tone = "brand",
className,
style,
title,
}: BaseProps) {
const labelled = title !== "";
return (
<svg
viewBox={`0 0 ${BIRD_VIEWBOX.w} ${BIRD_VIEWBOX.h}`}
height={size}
width={(size * BIRD_VIEWBOX.w) / BIRD_VIEWBOX.h}
className={cn("block", className)}
style={{ color: TONE_COLOR[tone], ...style, minHeight: MIN_HEIGHT.bird }}
role={labelled ? "img" : "presentation"}
aria-label={labelled ? (title ?? "Woohl") : undefined}
aria-hidden={labelled ? undefined : true}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d={BIRD_PATH}
fill="currentColor"
/>
</svg>
);
}
/* ------------------------------------------------------------ wordmark ---- */
/** WOOHL wordmark outlined vector artwork. */
export function WoohlWordmark({
size = 24,
tone = "ink",
className,
style,
title,
}: BaseProps) {
const labelled = title !== "";
return (
<svg
viewBox={`${WORDMARK_BOX.x} ${WORDMARK_BOX.y} ${WORDMARK_BOX.w} ${WORDMARK_BOX.h}`}
height={size}
width={(size * WORDMARK_BOX.w) / WORDMARK_BOX.h}
className={cn("block", className)}
style={{
color: TONE_COLOR[tone],
...style,
minHeight: MIN_HEIGHT.wordmark,
}}
role={labelled ? "img" : "presentation"}
aria-label={labelled ? (title ?? "Woohl") : undefined}
aria-hidden={labelled ? undefined : true}
>
<path d={WORDMARK_PATH} fill="currentColor" />
</svg>
);
}
/* --------------------------------------------------------------- stacked -- */
/** Bird above wordmark, centred. Square and near-square spaces. */
export function WoohlLogoStacked({
size = 72,
tone = "brand",
className,
style,
title,
}: BaseProps) {
const color = TONE_COLOR[tone];
const labelled = title !== "";
// Bird 96 tall, 28 gap, wordmark 42 tall — the master file's proportions.
const birdW = (96 * BIRD_VIEWBOX.w) / BIRD_VIEWBOX.h;
const wordW = (42 * WORDMARK_BOX.w) / WORDMARK_BOX.h;
const boxW = Math.max(birdW, wordW);
const totalH = 96 + 28 + 42;
return (
<svg
viewBox={`0 0 ${boxW} ${totalH}`}
height={size}
width={(size * boxW) / totalH}
className={cn("block", className)}
style={{ ...style, minHeight: MIN_HEIGHT.stacked }}
role={labelled ? "img" : "presentation"}
aria-label={labelled ? (title ?? "Woohl") : undefined}
aria-hidden={labelled ? undefined : true}
>
<svg
x={(boxW - birdW) / 2}
y={0}
width={birdW}
height={96}
viewBox={`0 0 ${BIRD_VIEWBOX.w} ${BIRD_VIEWBOX.h}`}
overflow="visible"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d={BIRD_PATH}
fill={color}
/>
</svg>
<svg
x={(boxW - wordW) / 2}
y={124}
width={wordW}
height={42}
viewBox={`${WORDMARK_BOX.x} ${WORDMARK_BOX.y} ${WORDMARK_BOX.w} ${WORDMARK_BOX.h}`}
overflow="visible"
>
<path d={WORDMARK_PATH} fill={color} />
</svg>
</svg>
);
}
/* --------------------------------------------------------------- lockup --- */
export type WoohlLogoProps = BaseProps & {
variant?: LogoVariant;
/** Reserve clear space around the mark (true = 1x bird height, "tight" = 0.5x). */
clearSpace?: boolean | "tight";
};
/** Horizontal logo lockup (bird left, wordmark right). */
export function WoohlLogo({
size = 32,
tone = "brand",
variant = "horizontal",
clearSpace = false,
className,
style,
title,
}: WoohlLogoProps) {
const resolved =
variant === "auto"
? size < LOGO_MIN_SIZE.horizontal
? "bird"
: "horizontal"
: variant === "horizontal" && size < LOGO_MIN_SIZE.horizontal
? "bird"
: variant;
const mark =
resolved === "bird" ? (
<WoohlBird
size={size}
tone={tone}
className={clearSpace ? undefined : className}
style={clearSpace ? undefined : style}
title={title}
/>
) : resolved === "stacked" ? (
<WoohlLogoStacked
size={size}
tone={tone}
className={clearSpace ? undefined : className}
style={clearSpace ? undefined : style}
title={title}
/>
) : resolved === "wordmark" ? (
<WoohlWordmark
size={size}
tone={tone}
className={clearSpace ? undefined : className}
style={clearSpace ? undefined : style}
title={title}
/>
) : (
<HorizontalLockup
size={size}
tone={tone}
className={clearSpace ? undefined : className}
style={clearSpace ? undefined : style}
title={title}
/>
);
if (!clearSpace) return mark;
// X = the height of the bird mark. In the horizontal lockup the bird is
// 83.15 of the 105-unit box, so X is derived from the rendered size rather
// than assumed equal to it.
const birdHeight =
resolved === "horizontal"
? size * (LOCKUP.bird.h / 105)
: resolved === "stacked"
? size * (96 / 166)
: size;
const pad = birdHeight * (clearSpace === "tight" ? 0.5 : 1);
return (
<span
className={cn("inline-flex items-center justify-center", className)}
style={{ padding: pad, ...style }}
>
{mark}
</span>
);
}
function HorizontalLockup({
size = 32,
tone = "brand",
className,
style,
title,
}: BaseProps) {
const color = TONE_COLOR[tone];
const labelled = title !== "";
return (
<svg
viewBox={LOCKUP.viewBox}
height={size}
width={size * HORIZONTAL_RATIO}
className={cn("block", className)}
style={{ ...style, minHeight: MIN_HEIGHT.horizontal }}
role={labelled ? "img" : "presentation"}
aria-label={labelled ? (title ?? "Woohl") : undefined}
aria-hidden={labelled ? undefined : true}
>
<svg
x={LOCKUP.bird.x}
y={LOCKUP.bird.y}
width={LOCKUP.bird.w}
height={LOCKUP.bird.h}
viewBox={`0 0 ${BIRD_VIEWBOX.w} ${BIRD_VIEWBOX.h}`}
preserveAspectRatio="xMidYMid meet"
overflow="visible"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d={BIRD_PATH}
fill={color}
/>
</svg>
<path d={WORDMARK_PATH} fill={color} />
</svg>
);
}
/* ------------------------------------------------------------- app icon --- */
/** The bird centred in a container for app icons and avatars. */
export function WoohlAppIcon({
size = 64,
variant = "paper-on-brand",
radius,
className,
title,
}: {
size?: number;
variant?:
"brand-on-paper" | "paper-on-brand" | "brand-on-ink" | "paper-on-ink";
radius?: number;
className?: string;
title?: string;
}) {
const map = {
"brand-on-paper": {
bg: "#FFFFFF",
fg: "var(--color-brand-600)",
ring: true,
},
"paper-on-brand": {
bg: "var(--color-brand-600)",
fg: "#FFFFFF",
ring: false,
},
"brand-on-ink": {
bg: "var(--color-ink)",
fg: "var(--color-brand-600)",
ring: false,
},
"paper-on-ink": { bg: "var(--color-ink)", fg: "#FFFFFF", ring: false },
}[variant];
return (
<div
className={cn("grid flex-none place-items-center", className)}
style={{
width: size,
height: size,
borderRadius: radius ?? size * 0.2237, // iOS squircle approximation
background: map.bg,
border: map.ring ? "1px solid var(--border-subtle)" : undefined,
}}
role={title === "" ? "presentation" : "img"}
aria-label={title === "" ? undefined : (title ?? "Woohl")}
aria-hidden={title === "" ? true : undefined}
>
<WoohlBird size={size * 0.52} style={{ color: map.fg }} title="" />
</div>
);
}
/** The Woohl bullet — a filled diamond in the accent tone. */
export function WoohlBullet({
size = 7,
className,
style,
}: {
size?: number;
className?: string;
style?: CSSProperties;
}) {
return (
<span
aria-hidden
className={cn(
"bg-surface-accent inline-block flex-none rotate-45",
className,
)}
style={{
width: size,
height: size,
borderRadius: Math.max(1, size * 0.18),
...style,
}}
/>
);
}
WoohlLogoProps
Also accepts everything from BaseProps.
| Prop | Type | Default |
|---|---|---|
clearSpaceReserve clear space around the mark (true = 1x bird height, "tight" = 0.5x). | boolean | "tight" | — |
variant | LogoVariant | — |
Registry items
npm packages
None beyond React itself.