# Brand lockup

> A logo mark and brand name set as one unit, side by side or stacked, at three sizes, with an optional tagline and an optional link home. The mark keeps its ratio; the name is read once.

- ID: `cmp_brand_lockup_01`
- Slug: `brand-lockup-01`
- Version: `1.0.0` (current)
- Status: published
- Published: 2026-10-01T09:29:21Z
- Updated: 2026-10-01
- Available versions: `1.0.0`
- Kind: control
- Primary category: `typography`
- Detail page: https://pagesugar.com/components/brand-lockup-01
- Preview: https://pagesugar.com/preview/brand-lockup-01

## Variants

| Variant | Label | Default | Artifact digest |
| --- | --- | --- | --- |
| `default` | Inherited colour | yes | `sha256-bc60e81232fa9868cb1077349c43199225d70b1371fc1fd9e48d02606f6bfd48` |

## Runtime and compatibility

- Runtime: svelte
- Svelte: 5
- SvelteKit required: no (portable Svelte component)
- Tailwind CSS: 4
- SSR: supported
- Requires client-side JavaScript: no
- Integration level: presentational
- Appearance modes: light
- Suggested directory: `src/lib/components/brand-lockup-01`

## Dependencies

No third-party runtime packages.

## Services

No external services required.

## Usage

Pass the brand name and, usually, a logo (an image with its intrinsic width and height, or a snippet holding an inline SVG) and an href. It renders one link or a plain element holding the mark, the name and an optional tagline. It does not ship a logo, pick a light or dark logo file for you, enforce clear space beyond its fixed gap, or shorten a long name.

Required props: `name`

```svelte
<script lang="ts">
	import BrandLockup from '$lib/components/brand-lockup-01/BrandLockup.svelte';
</script>

<!-- A site header: mark and name, linked home. -->
<BrandLockup name="Halcyon" href="/" logo={{ src: '/logo-mark.svg', width: 32, height: 32 }} />

<!-- A sign-in card: stacked, large and centred, with a tagline. -->
<BrandLockup
	name="Halcyon"
	href="/"
	logo={{ src: '/logo-mark.svg', width: 32, height: 32 }}
	orientation="stacked"
	size="lg"
	align="center"
	tagline="Roadmaps and timelines for teams"
/>
```

Limitations:

- Light logo files only: there is no darkLogo prop. On a dark band, use a snippet mark drawn in currentColor, or swap the image yourself.
- The gap between mark and name is fixed per size; brand clear-space rules beyond it are the consumer's to apply around the lockup.
- A long name wraps; it is never shortened or truncated. If a header has no room for it, hide the wordmark at that width or pass a shorter name.
- The vertical centring of mark and name is tuned for Inter-like fonts, whose line box centre sits at cap-height middle. Fonts with unusual metrics may want the mark nudged.
- The name inherits font-sans. A brand with its own display face sets the font on the lockup or an ancestor.

## Usage guide

### Brand lockup

The site's mark and name as one unit. Give it the name, a logo and, in a header, `href="/"`.

```svelte
<script lang="ts">
	import BrandLockup from '$lib/components/brand-lockup-01/BrandLockup.svelte';
</script>

<BrandLockup name="Halcyon" href="/" logo={{ src: '/logo-mark.svg', width: 32, height: 32 }} />
```

#### Sizes

| Size | Mark  | Name  | Gap   | Tagline | Where                            |
| ---- | ----- | ----- | ----- | ------- | -------------------------------- |
| `sm` | 24 px | 16 px | 8 px  | 12 px   | footers, dense app bars          |
| `md` | 32 px | 18 px | 12 px | 13 px   | site headers (the default)       |
| `lg` | 40 px | 24 px | 16 px | 14 px   | sign-in screens, stacked lockups |

The mark is as tall as the name's line, so the two centre on the capitals. A logo image's
`width` and `height` are its own intrinsic size; the component sets the height and lets the
width follow, so a 4:1 logo stays 4:1.

#### An inline SVG mark

Pass a snippet when the mark should follow the text colour, on a dark footer band say. (Hover retones only the name; the mark stays as it is.)

```svelte
{#snippet mark()}
	<svg viewBox="0 0 32 32" fill="currentColor">
		<rect width="32" height="32" rx="8" />
	</svg>
{/snippet}

<BrandLockup name="Halcyon" href="/" logo={mark} />
```

The SVG fills the mark's height and keeps its ratio. The component hides it from assistive
technology, so don't give it a `<title>`.

#### Mark only

`showWordmark={false}` hides the name visually and keeps it in the markup. Add
`linkSuffix="home"` if you want the link read as "Halcyon home". For a mark-only lockup on
phones and a full one above, render both and hide one at each width:

```svelte
<BrandLockup class="sm:hidden" name="Halcyon" href="/" {logo} showWordmark={false} />
<BrandLockup class="max-sm:hidden" name="Halcyon" href="/" {logo} />
```

#### Dark bands

The name takes the surrounding text colour, so set the band's colour and leave the tokens
alone. A logo image keeps its own colours; use a snippet in `currentColor` or a light file.

## Props

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `name` | `string` | yes |  | Brand name: the wordmark and the accessible name. Never truncated. |
| `logo` | { src: string; width: number; height: number } \| Snippet | no |  | The mark. An image object (width and height are its intrinsic size, used for the ratio; it renders with alt="") or a snippet holding an inline SVG, which fills the mark's height. Omitted, the name stands alone. |
| `href` | `string` | no |  | Destination, usually "/". Set, the whole lockup is one link with one focus ring and a 44 px minimum height. |
| `orientation` | 'horizontal' \| 'stacked' | no | `'horizontal'` | Mark beside the name, or above it. |
| `size` | 'sm' \| 'md' \| 'lg' | no | `'md'` | Mark height 24, 32 or 40 px with a 16, 18 or 24 px name; the gap scales with them. |
| `showWordmark` | `boolean` | no | `true` | False hides the name and tagline visually and keeps the name for assistive technology. Ignored when there is no logo. |
| `tagline` | `string` | no |  | Short line set small and muted under the name. When linked it describes the link rather than joining its name. |
| `align` | 'start' \| 'center' | no | `'start'` | Stacked only: line the mark, name and tagline up on the start edge, or centre them. |
| `linkSuffix` | `string` | no |  | Words read after the name when linked, such as "home", giving "Halcyon home". Never shown. |
| `class` | `string` | no |  | Extra classes on the root element, for margins and placement. |

## Customization

Size, orientation and alignment through props; the name, tagline and focus ring follow the surrounding text colour unless --brand-lockup-ink, --brand-lockup-muted or --brand-lockup-accent are set.

- Colour: by default the name is the surrounding text colour and the tagline a 66% mix of it, so the lockup works on a white header, a dark footer band and a tinted page. Set --brand-lockup-ink for a brand-coloured name and --brand-lockup-muted for the tagline.
- Dark or tinted page: set the container's text colour (text-zinc-50 on bg-zinc-950, say) and draw the mark as an inline SVG snippet in currentColor so it follows too. A logo image does not change colour; swap the file for a light version.
- Focus ring: it is the text colour by default; set --brand-lockup-accent to your accent. Keep it at 3:1 against the background.
- Logo files: pass the image's own width and height, not the size you want. The component sets the height from size and lets the width follow.
- A logo that already contains the name (a wide 4:1 lockup image): pass it as logo with showWordmark={false}; the name stays for screen readers.
- Sizes: the STEPS map at the top of the script holds each step's mark height, type and gap as complete classes. Change a step there and keep the mark as tall as the name's line height so they stay centred.
- Responsive: for a mark-only lockup on phones, render two lockups and hide one with sm:hidden / max-sm:hidden, or pass showWordmark from your own media query state.

| Token | Public CSS variable |
| --- | --- |
| `accent` | `--brand-lockup-accent` |
| `ink` | `--brand-lockup-ink` |
| `muted` | `--brand-lockup-muted` |

## Accessibility

- When linked, the lockup is one anchor: one tab stop and one focus ring around the mark, name and tagline. Its accessible name is the brand name exactly once, plus linkSuffix if you set it.
- The mark is always aria-hidden and a logo image has alt="", because the name is already there in text; a screen reader never hears "Halcyon Halcyon".
- With showWordmark={false} the name stays in the markup as visually hidden text, so a mark-only lockup still has a name.
- A linked tagline is hidden from the link's name and attached as its description (aria-describedby), so the name stays short and exact.
- The link is at least 44 px tall at every size. The focus ring is a 2 px outline, 4 px out, on :focus-visible only, in --brand-lockup-accent (the text colour by default).
- Contrast follows the page: the name is the surrounding text colour and the tagline a 66% mix of it (about 5.9:1 for zinc-950 on white). Re-check if you set the tokens or put the lockup on a mid-tone fill.
- A logo image must be legible on its background; the component cannot check that.

Known limitations:

- No dark logo variant: a logo image keeps its own colours on a dark band.
- Hover changes only the name's tone; an image mark does not change.

## License

- Declared source: MIT
- Default license approval is pending. See https://pagesugar.com/docs/license.

## Source

- Palette: Inherited colour (`default`)
- Entry: `BrandLockup.svelte`
- Suggested directory: `src/lib/components/brand-lockup-01`
- Files: 1
- Artifact digest: `sha256-bc60e81232fa9868cb1077349c43199225d70b1371fc1fd9e48d02606f6bfd48`

Paths below are relative to the suggested directory. Copy the files as they are;
they import nothing from this site.

#### `BrandLockup.svelte`

Role: entry · 7182 bytes · SHA-256 `08eaaab14d2463a2b005a956b57a95d03187db4dbfbd3e4011583f76fa3842b9`

```svelte
<script lang="ts" module>
	import type { Snippet } from 'svelte';

	/** A logo file. Width and height are its intrinsic size, used only for its aspect ratio. */
	export interface BrandLogoImage {
		src: string;
		width: number;
		height: number;
	}

	export type BrandLockupSize = 'sm' | 'md' | 'lg';
	export type BrandLockupOrientation = 'horizontal' | 'stacked';
	export type BrandLockupLogo = BrandLogoImage | Snippet;
</script>

<script lang="ts">
	interface Props {
		/** Brand name: the wordmark and the accessible name. */
		name: string;
		/** The mark: an image with its intrinsic size, or a snippet holding an inline SVG. */
		logo?: BrandLockupLogo;
		/** Destination, usually "/". Set, the whole lockup is one link. */
		href?: string;
		/** Mark beside the name, or above it. */
		orientation?: BrandLockupOrientation;
		/** Scales the mark, the gap and the type together. */
		size?: BrandLockupSize;
		/** Show the name beside the mark. False keeps it for assistive technology only. */
		showWordmark?: boolean;
		/** Short line set under the name. */
		tagline?: string;
		/** Stacked only: line everything up on the start edge, or centre it. */
		align?: 'start' | 'center';
		/** Words read after the name when linked, such as "home" ("Halcyon home"). Never shown. */
		linkSuffix?: string;
		/** Extra classes on the root element, for margins and placement. */
		class?: string;
	}

	let {
		name,
		logo,
		href,
		orientation = 'horizontal',
		size = 'md',
		showWordmark = true,
		tagline,
		align = 'start',
		linkSuffix,
		class: className
	}: Props = $props();

	const uid = $props.id();

	/*
	 * One ratio per step. A single line of the name, with its padding, is exactly as tall as the
	 * mark, so the mark centres on the capitals (Inter's line box centre sits at its cap-height
	 * middle) and stays level with the first line when a long name wraps. The padding, not the
	 * leading, makes up the height, so wrapped lines keep a reading leading; the larger taglines
	 * pull up 4 px past it. The gap is about a third of the mark. Complete class names only.
	 */
	const STEPS = {
		sm: {
			mark: 'h-6',
			name: 'text-base leading-6 tracking-[-0.011em]',
			tagline: 'text-xs leading-4',
			gap: 'gap-2'
		},
		md: {
			mark: 'h-8',
			name: 'text-lg leading-6 py-1 tracking-[-0.014em]',
			tagline: 'text-[0.8125rem] leading-4 -mt-1',
			gap: 'gap-3'
		},
		lg: {
			mark: 'h-10',
			name: 'text-2xl leading-8 py-1 tracking-[-0.019em]',
			tagline: 'text-sm leading-5 -mt-1',
			gap: 'gap-4'
		}
	} as const;

	const step = $derived(STEPS[size] ?? STEPS.md);
	const stacked = $derived(orientation === 'stacked');
	const centred = $derived(stacked && align === 'center');
	const image = $derived(typeof logo === 'object' && logo !== null ? logo : undefined);
	const snippet = $derived(typeof logo === 'function' ? logo : undefined);
	const hasMark = $derived(Boolean(image || snippet));
	// Without a mark the name has to show, or the lockup would render nothing visible.
	const wordmarkVisible = $derived(showWordmark || !hasMark);
	const taglineVisible = $derived(Boolean(tagline) && wordmarkVisible);
	const taglineId = `${uid}-tagline`;
	const suffix = $derived(linkSuffix?.trim());

	const layoutClass = $derived([
		'flex max-w-full min-w-0',
		step.gap,
		stacked ? 'flex-col' : 'flex-row items-start',
		centred ? 'items-center text-center' : stacked && 'items-start text-start'
	]);
</script>

{#snippet body()}
	<span class={layoutClass}>
		{#if hasMark}
			<span
				aria-hidden="true"
				class={['brand-lockup__mark flex max-w-full shrink-0 items-center', step.mark]}
			>
				{#if image}
					<img
						src={image.src}
						width={image.width}
						height={image.height}
						alt=""
						class="block h-full w-auto max-w-full object-contain"
					/>
				{:else if snippet}
					{@render snippet()}
				{/if}
			</span>
		{/if}
		{#if wordmarkVisible}
			<span class={['flex min-w-0 flex-col', centred ? 'items-center' : 'items-start']}>
				<span
					class={[
						'brand-lockup__name font-semibold text-balance [overflow-wrap:anywhere] text-(--_ink)',
						step.name,
						href &&
							'transition-colors duration-150 ease-(--_ease) group-hover:text-(--_ink-hover) motion-reduce:transition-none'
					]}
				>
					{name}{#if href && suffix}<span class="sr-only">{` ${suffix}`}</span>{/if}
				</span>
				{#if taglineVisible}
					<span
						id={href ? taglineId : undefined}
						aria-hidden={href ? 'true' : undefined}
						class={[
							'brand-lockup__tagline font-normal text-pretty [overflow-wrap:anywhere] text-(--_muted)',
							step.tagline
						]}
					>
						{tagline}
					</span>
				{/if}
			</span>
		{:else}
			<span class="sr-only">{href && suffix ? `${name} ${suffix}` : name}</span>
		{/if}
	</span>
{/snippet}

{#if href}
	<!--
		One link around the whole lockup: one tab stop, one focus ring. The tagline is hidden from
		the link's name and given as its description instead, so the name is the brand exactly once.
	-->
	<a
		{href}
		aria-describedby={taglineVisible ? taglineId : undefined}
		class={[
			'brand-lockup group inline-flex max-w-full min-w-11 items-center rounded-md align-middle text-(--_ink) outline-offset-4 transition-[scale] duration-80 ease-(--_ease) focus-visible:outline-2 focus-visible:outline-(--_accent) active:scale-[.98] motion-reduce:transition-none motion-reduce:active:scale-100',
			// A 44 px target however small the mark; the lockup centres inside it, held to the start.
			'min-h-11',
			className
		]}
	>
		{@render body()}
	</a>
{:else}
	<div
		class={['brand-lockup inline-flex max-w-full min-w-0 align-middle text-(--_ink)', className]}
	>
		{@render body()}
	</div>
{/if}

<style>
	/*
	 * Public tokens: set --brand-lockup-* on the lockup or any ancestor. By default the name takes
	 * the surrounding text colour and the tagline a softer mix of it, so one lockup sits on a white
	 * header, a dark footer band or a tinted page without a change.
	 */
	.brand-lockup {
		--_accent: var(--brand-lockup-accent, currentColor);
		--_ink: var(--brand-lockup-ink, currentColor);
		--_muted: var(--brand-lockup-muted, color-mix(in srgb, currentColor 66%, transparent));
		--_ink-hover: color-mix(in srgb, var(--_ink) 72%, transparent);
		--_ease: cubic-bezier(0.2, 0, 0, 1);
	}

	/*
	 * A snippet mark is consumer markup: an inline SVG fills the mark's height and keeps its own
	 * aspect ratio. Layered and wrapped in :where() so a class on the consumer's SVG still wins.
	 */
	@layer components {
		.brand-lockup__mark :global(:where(svg)) {
			display: block;
			height: 100%;
			width: auto;
			max-width: 100%;
		}
	}

	/* Arabic, Hebrew and other right-to-left scripts are never letter-spaced. */
	.brand-lockup__name:dir(rtl) {
		letter-spacing: 0;
	}

	/* Chinese, Japanese and Korean: no Latin tracking, and the name breaks at phrases. */
	.brand-lockup__name:lang(zh),
	.brand-lockup__name:lang(ja),
	.brand-lockup__name:lang(ko) {
		letter-spacing: 0;
		word-break: keep-all;
		line-break: strict;
	}

	@supports (word-break: auto-phrase) {
		.brand-lockup__name:lang(ja),
		.brand-lockup__tagline:lang(ja) {
			word-break: auto-phrase;
		}
	}
</style>
```

## Artifacts

### Inherited colour (`default`) (default)

- Artifact digest: `sha256-bc60e81232fa9868cb1077349c43199225d70b1371fc1fd9e48d02606f6bfd48`
- Entry: `BrandLockup.svelte`
- Receipt: https://pagesugar.com/artifacts/cmp_brand_lockup_01/1.0.0/default/sha256-bc60e81232fa9868cb1077349c43199225d70b1371fc1fd9e48d02606f6bfd48/manifest.json
- Bundle: https://pagesugar.com/artifacts/cmp_brand_lockup_01/1.0.0/default/sha256-bc60e81232fa9868cb1077349c43199225d70b1371fc1fd9e48d02606f6bfd48/bundle.zip (5860 bytes, sha256 `06ea7e1307e7a5fe62514baad2620acb3a007530d38654b6fbe13c8b7a5ab689`)

Files:

- `BrandLockup.svelte` (entry, 7182 bytes): https://pagesugar.com/artifacts/cmp_brand_lockup_01/1.0.0/default/sha256-bc60e81232fa9868cb1077349c43199225d70b1371fc1fd9e48d02606f6bfd48/source/BrandLockup.svelte
