Logo
Renders a raw inline SVG mark at a size normalized against its own aspect ratio, so a wide wordmark, a square badge, and a tall crest read as consistent side by side. With no SVG it falls back to the brand name as text.
Import
import { Logo } from "@hyzer-labs/ui"Demo
svg takes a raw SVG string. It is usually a page-local constant from a ?raw import:
const acmeSvg =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 64 190 62">…</svg>';
<Logo name="ACME Corp" svg={acmeSvg} />Sizing a wall by equal height alone stretches a flat wordmark very wide and squeezes a
tall crest down to a sliver. Neither one reads as belonging beside the others. Logo's
normalization interpolates toward equal height instead of matching it exactly, so every
mark lands at a comparable size with no distortion. Logo stamps its width factor inline,
and an inline value cannot be overridden from outside, so the naive row below uses plain <svg> elements rather than Logo: the same fixed-height
CSS you would otherwise reach for.
Naive sizing, every mark forced to the same height in plain SVG:
<div class="naive-row">
{#each logos as logo (logo.name)}
<span class="naive-logo">{@html logo.svg}</span>
{/each}
</div>
<style>
.naive-row { display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap; }
/* :global: the svg comes from {@html}, invisible to Svelte statically. */
.naive-logo :global(svg) { display: block; height: 4rem; width: auto; }
</style>Normalized, Logo's default, with the per-mark scale/brightness tuning the Appearance tab covers. The same config renders every wall on this page:
const logos = [
{ name: 'ACME Corp', svg: acmeSvg, scale: 1, brightness: 1 },
{ name: 'Umbrella Corporation', svg: umbrellaSvg, scale: 0.9, brightness: 1 },
{ name: 'Black Mesa Research Facility', svg: blackMesaSvg, scale: 1.15, brightness: 1 },
{ name: 'Lumon Industries', svg: lumonSvg, scale: 1.4, brightness: 1 },
{ name: 'Weyland-Yutani', svg: weylandSvg, scale: 1.3, brightness: 1 },
{ name: 'Delos', svg: delosSvg, scale: 1.2, brightness: 1 }
];
<Cluster gap="lg" justify="center">
{#each logos as logo (logo.name)}
<Logo name={logo.name} svg={logo.svg} scale={logo.scale} brightness={logo.brightness} />
{/each}
</Cluster>Header's and Footer's logo snippets are both
plain snippets, so a Logo drops in unchanged. The link, not the logo, owns the
click target and the focus ring:
<Header>
{#snippet logo()}
<a href="/" aria-label="Acme home">
<Logo name="Acme" svg={acmeSvg} />
</a>
{/snippet}
</Header>
<Footer>
{#snippet logo()}
<a href="/" aria-label="Acme home">
<Logo name="Acme" svg={acmeSvg} />
</a>
{/snippet}
</Footer>scale and brightness nudge a mark whose optical weight fights
its geometry. Both come from the same config array every wall on this page uses. A thin,
wide wordmark, such as a fashion house's masthead, feels this hardest: normalized by
width, its sliver of height shrinks even further, so at the default scale it reads like
a footnote next to anything bolder. Here is Delos untuned, then at the scale used everywhere else on this page:
<Cluster gap="lg" justify="center">
<Stack gap="sm" align="center">
<Logo name="Delos" svg={delosSvg} />
<span class="scale-label">scale 1 (default)</span>
</Stack>
<Stack gap="sm" align="center">
<Logo name="Delos" svg={delosSvg} scale={1.2} />
<span class="scale-label">scale 1.2</span>
</Stack>
</Cluster>The same idea applied to the whole wall: every mark's scale and brightness comes from one config array:
const logos = [
{ name: 'ACME Corp', svg: acmeSvg, scale: 1, brightness: 1 },
{ name: 'Umbrella Corporation', svg: umbrellaSvg, scale: 0.9, brightness: 1 },
{ name: 'Black Mesa Research Facility', svg: blackMesaSvg, scale: 1.15, brightness: 1 },
{ name: 'Lumon Industries', svg: lumonSvg, scale: 1.4, brightness: 1 },
{ name: 'Weyland-Yutani', svg: weylandSvg, scale: 1.3, brightness: 1 },
{ name: 'Delos', svg: delosSvg, scale: 1.2, brightness: 1 }
];
<Grid columns={3} gap="away" padding="sm" align="center" style="--hz-logo-size: 5rem">
{#each logos as logo (logo.name)}
<Logo
class="wall-logo"
name={logo.name}
svg={logo.svg}
scale={logo.scale}
brightness={logo.brightness}
/>
{/each}
</Grid>
<style>
/* Grid items default to justify-items: start; center each mark so the
wall reads as balanced instead of left-hugging its column. */
:global(.wall-logo) {
justify-self: center;
}
</style>monochrome (the default) recolors a mark that declares no fill to var(--hz-logo-color, currentColor). The wall below sets --hz-logo-color to the theme's primary intent color, so compare it with the
plain wall above: ACME, Black Mesa, and Lumon change color. Umbrella's eight segments
keep their hard-coded brand red and white, while Weyland-Yutani and Delos declare currentColor directly on every path rather than inheriting it. A declared
fill cannot be overridden by an inherited one, and declaring currentColor is still declaring a fill. monochrome=false turns the recolor off by prop instead
of markup:
<Grid
columns={3}
gap="away"
padding="sm"
align="center"
style="--hz-logo-size: 5rem; --hz-logo-color: var(--hz-intent-primary, #2563eb)"
>
{#each logos as logo (logo.name)}
<Logo
class="wall-logo"
name={logo.name}
svg={logo.svg}
scale={logo.scale}
brightness={logo.brightness}
/>
{/each}
</Grid>
<style>
/* Grid items default to justify-items: start; center each mark so the
wall reads as balanced instead of left-hugging its column. */
:global(.wall-logo) {
justify-self: center;
}
</style>Editing someone else's brand mark
fill attributes so currentColor can reach them) or check the brand's guidelines first.
Many brands ship an official single-color variant for exactly this case, and using it beats
hand-editing someone else's mark.Build a logo wall by composition: Grid (or Cluster, Carousel, and so on) plus one --hz-logo-size override. It can
go on the Grid itself or on any ancestor, since custom properties inherit
either way. A wall only reads as balanced if every mark sits mid-column rather than
hugging one edge, so each Logo gets justify-self: center.
Grid's items sit at their column's start edge by default.
<Grid columns={3} gap="away" padding="sm" align="center" style="--hz-logo-size: 5rem">
{#each logos as logo (logo.name)}
<Logo
class="wall-logo"
name={logo.name}
svg={logo.svg}
scale={logo.scale}
brightness={logo.brightness}
/>
{/each}
</Grid>
<style>
/* Grid items default to justify-items: start; center each mark so the
wall reads as balanced instead of left-hugging its column. */
:global(.wall-logo) {
justify-self: center;
}
</style>An <svg> written directly as markup works too. Logo measures the svg prop while rendering on the server, so the sizing is already right in
the server HTML. It can only measure this form after the page hydrates, so the mark
renders square until then. Reach for children when you want the ergonomics, and for svg when server-rendered sizing matters.
<Logo name="ACME Corp">
<svg viewBox="0 64 190 62" fill-rule="evenodd" clip-rule="evenodd">
<path d="M23.454 68.785l-20.62 55.488h15.768l2.729-7.883H33.46v7.883h15.464V68.785h-25.47zm9.703 31.535h-6.368l6.368-13.342v13.342zM94.71 68.178H70.149c-8.188 0-10.916 10.613-10.916 10.613l-5.761 33.051s-2.123 12.432 8.49 12.432h23.045l2.426-16.373H75.001s-5.458 1.213-4.548-4.852c.303-.91 2.426-13.039 2.426-13.039s.606-4.245 3.335-4.245h15.464l3.032-17.587zM101.988 68.482l-11.523 55.791h15.162l5.154-23.65 1.213 23.65h8.188l9.095-23.65-4.548 23.65h13.644l11.221-56.095h-19.406l-9.401 23.348V68.482h-18.799zM145.955 124.273l10.611-56.095h31.233l-2.426 16.072H169l-.91 5.457h15.767l-2.425 12.129-16.071-.303-.91 5.457h16.07l-3.335 17.283h-31.231zM185.07 123.365c-2.123 0-3.336-1.518-3.336-3.639 0-2.123 1.213-3.639 3.336-3.639s3.639 1.516 3.639 3.639c0 2.122-1.516 3.639-3.639 3.639zm0 1.213c2.729 0 4.852-2.426 4.852-4.852 0-2.729-2.123-4.852-4.852-4.852s-4.852 2.123-4.852 4.852c.001 2.426 2.124 4.852 4.852 4.852zm1.213-4.549c.91 0 1.516-.303 1.516-1.516s-.91-1.516-2.123-1.516h-2.426v5.154h.91v-2.123h.91l1.213 2.123h1.213l-1.213-2.122zm-2.123-.607v-1.516h1.213c.607 0 1.213 0 1.213.607 0 .605-.303.908-.91.908h-1.516v.001z"/>
</svg>
</Logo>With neither svg nor children, Logo renders name as plain text
instead: selectable, searchable, and never a degraded image of text. Treat it as a
placeholder until the real brand asset is in place; style the interim text through the class prop or your own .hz-logo-text rule.
<Logo name="Soylent" />Logo takes a raw SVG string because the library cannot see your asset directory. It
renders that string verbatim, with no sanitizing, so pass only static assets you
control, never user input. Import the file with ?raw and pass it straight through:
import acmeSvg from '$lib/assets/acme.svg?raw';
<Logo name="Acme" svg={acmeSvg} />Forgetting ?raw is an easy mistake. The import then resolves to a URL string
rather than markup, and Logo warns in development that the value does not look like SVG:
// Missing ?raw. This imports the URL string Vite serves the file at,
// not its markup. Logo renders it anyway and warns in development that
// the value does not look like SVG.
import acmeSvg from '$lib/assets/acme.svg';With more than a couple of logos, resolve them all at once:
const logoModules = import.meta.glob('$lib/assets/logos/*.svg', {
query: '?raw',
import: 'default',
eager: true
});
const logos = [
{ slug: 'acme', name: 'Acme', scale: 1, brightness: 1 },
{ slug: 'globex', name: 'Globex', scale: 0.85, brightness: 1 }
].map((entry) => ({
...entry,
svg: logoModules[`/src/lib/assets/logos/${entry.slug}.svg`]
}));Optimize before you ship
?raw imports.Props
| Name | Type | Default | Note |
|---|---|---|---|
name | string | — | Required. Names the mark for assistive technology, and is the visible text when no SVG is given. |
svg | string | — | Raw SVG markup, rendered as-is and never sanitized. Pass only a static asset you control — a `?raw` import or a build-time constant, never user input or a fetched string. Missing (or whitespace-only) renders `name` as text instead. |
children | Snippet | — | Another way to supply the mark: an `<svg>` written directly as markup. Logo can only measure it once it mounts in the browser, while `svg` is measured during server rendering. If both are given, `svg` wins. |
scale | number | 1 | A multiplier on top of the normalized width — the optical-weight knob. A positive number, typically 0.7-1.4. |
brightness | number | 1 | A filter: brightness() nudge for a mark that reads too dark or too light. Written inline only when it differs from 1. |
decorative | boolean | false | Set when adjacent text already names the brand — hides the mark from assistive technology entirely. |
monochrome | boolean | true | Recolors the mark to one color that follows the surrounding text. Set to false to keep the SVG's own colors. |
class | string | — | Merged after the hz-logo class. |
Theme hooks
What this component promises your CSS. The reference theme styles exactly these — from @layer hz-theme, so your unlayered rules win. See Styling Components for the how.
Root class: .hz-logo
Data attributes
| Hook | Values | Styles |
|---|---|---|
data-fallback | present with no svg | The text form — no injected SVG, .hz-logo-text renders the name instead. |
data-monochrome | present by default | Absent when monochrome={false}. Gates the fill rule below, so an unset SVG keeps its own colours. |
Custom properties
| Hook | Values | Styles |
|---|---|---|
--hz-logo-size | <length> — default clamp(4rem, 8vw, 6rem) | The normalization base every logo scales against. Override it on a wall container (Cluster/Grid) to resize every logo inside at once. |
--hz-logo-brightness | <number> — default 1 | Applied as filter: brightness(). Usually set through the brightness prop, which stamps it inline only when != 1 — the hook stays directly settable in CSS for container-wide muting. |
--hz-logo-color | <color> — default currentColor | Read, not declared, in the monochrome fill rule — a wall mutes itself with one override, the Container --hz-breakout-shift precedent for a read-only hook. |
Part classes
| Hook | Values | Styles |
|---|---|---|
.hz-logo-text | child element | The text fallback. |
Accessibility
An informative logo (the default) gets role="img" and an aria-label taken from name. Assistive technology reads one graphic with one name, however many paths or groups the SVG holds inside. Set decorative when nearby text already says the brand name; the mark is then hidden from assistive technology. With no svg, name renders as real, selectable text and needs no role at all.
References: MDN: role="img" · WAI: An alt Decision Tree