SiteLogo

One brand identity for your navigation, footer, product cards and sign-in screens. Combine a mark, a wordmark and an optional subtitle, then choose the layout and treatment that fit the surrounding interface.

SiteLogo

View source · API reference

For the original bordered logo with uppercase, individually colored letters, use ClassicLogo. TopBar can select either component through logo.component in site configuration or its logoComponent prop.

Interactive playground

Change the brand, switch layouts, and explore the mark and wordmark treatments. Every control updates the same component. The live configuration is the object you can pass to another SiteLogo or save in your site configuration.

The preview starts without a link so you can explore it freely. Enable Link the preview to the home page to try navigation. Switch the site's theme to compare light and dark appearances.

Use Mark-to-title spacing to bring the icon and brand name closer together or give them more room. Default (compact) uses 6px horizontally and 8px when stacked, at a 16px root font size. The control is disabled for mark-only and wordmark-only layouts, which have no mark-to-title gap.

<RegorApp id="logo-playground" src="./playground.ts"/>
import {
  defineBadgeComponents,
  defineButtonComponents,
  defineFlexComponents,
  defineFormComponents,
  defineFormInputField,
  defineFormSelectField,
  defineGridComponents,
  defineIconComponents,
  defineLogoComponents,
  definePanelComponents,
  type FormSelectOption,
  type LogoConfig,
} from '@purestack/ts-components'
import { lucide_chevron_down, tabler_stack_2 } from '@purestack/ts-svg-icons'
import {
  type ComputedRef,
  computed,
  createApp,
  defineComponent,
  html,
  type Ref,
  ref,
} from 'regor'

export interface LogoPlayground {
  brandName: Ref<string>
  tagline: Ref<string>
  punctuation: Ref<string>
  logoLayout: Ref<NonNullable<LogoConfig['layout']>>
  logoSize: Ref<NonNullable<LogoConfig['size']>>
  treatment: Ref<NonNullable<LogoConfig['appearance']>>
  markTreatment: Ref<NonNullable<LogoConfig['markStyle']>>
  wordmarkTreatment: Ref<NonNullable<LogoConfig['wordmarkStyle']>>
  markShape: Ref<NonNullable<LogoConfig['shape']>>
  logoTone: Ref<NonNullable<LogoConfig['tone']>>
  markSource: Ref<string>
  customAccent: Ref<string>
  spacing: Ref<string>
  linked: Ref<boolean>
  previewConfig: ComputedRef<LogoConfig>
  configSource: ComputedRef<string>
  layouts: FormSelectOption[]
  sizes: FormSelectOption[]
  spacings: FormSelectOption[]
  treatments: FormSelectOption[]
  marks: FormSelectOption[]
  wordmarks: FormSelectOption[]
  shapes: FormSelectOption[]
  tones: FormSelectOption[]
  sources: FormSelectOption[]
  reset: () => void
}

const logoPlaygroundTemplate = html`<Flex direction="column">
  <Panel variant="surfaceAlt" bodyClass="p-4 min-w-0">
    <Flex justify="between" align="center" wrap="true">
      <p class="text-eyebrow m-0">Your brand, your presentation</p>
      <Badge tone="accent" variant="surface">Live preview</Badge>
    </Flex>
    <Flex align="center" justify="center" class="py-5">
      <SiteLogo id="logo-preview" :config="previewConfig"/>
    </Flex>
    <p class="text-muted m-0">Switch the site theme to see the same logo in light and dark.</p>
  </Panel>
  <Grid columns="1" columnsMd="3">
    <FormInputField id="logo-brand" label="Brand name" :model="brandName"/>
    <FormInputField id="logo-subtitle" label="Subtitle" :model="tagline" placeholder="Optional tagline"/>
    <FormInputField id="logo-suffix" label="Accented suffix" :model="punctuation"/>
    <FormSelectField id="logo-layout" label="Layout" :model="logoLayout" :options="layouts"/>
    <FormSelectField id="logo-size" label="Size" :model="logoSize" :options="sizes"/>
    <FormSelectField id="logo-gap" label="Mark-to-title spacing" :model="spacing" :options="spacings" :disabled="logoLayout === 'mark' || logoLayout === 'wordmark'"/>
    <FormSelectField id="logo-appearance" label="Appearance" :model="treatment" :options="treatments"/>
    <FormSelectField id="logo-mark" label="Mark treatment" :model="markTreatment" :options="marks"/>
    <FormSelectField id="logo-wordmark" label="Wordmark treatment" :model="wordmarkTreatment" :options="wordmarks"/>
    <FormSelectField id="logo-shape" label="Mark shape" :model="markShape" :options="shapes"/>
    <FormSelectField id="logo-tone" label="Tone" :model="logoTone" :options="tones"/>
    <FormSelectField id="logo-source" label="Mark source" :model="markSource" :options="sources"/>
    <FormInputField id="logo-accent" label="Custom accent color" :model="customAccent" placeholder="Theme default, or #58a6ff"/>
  </Grid>
  <Flex justify="between" align="center" wrap="true">
    <FormCheck id="logo-linked" label="Link the preview to the home page" :checked="linked"/>
    <Btn variant="outline" @click="reset">Reset playground</Btn>
  </Flex>
  <Panel variant="outline" bodyClass="p-3 min-w-0">
    <p class="text-eyebrow mt-0">Live configuration</p>
    <pre class="overflow-auto max-h-inspector m-0"><code id="logo-config-output">{{ configSource }}</code></pre>
  </Panel>
</Flex>`

function createLogoPlayground(): LogoPlayground {
  const brandName = ref('PureStack')
  const tagline = ref('')
  const punctuation = ref('.')
  const logoLayout = ref<NonNullable<LogoConfig['layout']>>('horizontal')
  const logoSize = ref<NonNullable<LogoConfig['size']>>('md')
  const treatment = ref<NonNullable<LogoConfig['appearance']>>('plain')
  const markTreatment = ref<NonNullable<LogoConfig['markStyle']>>('plain')
  const wordmarkTreatment =
    ref<NonNullable<LogoConfig['wordmarkStyle']>>('plain')
  const markShape = ref<NonNullable<LogoConfig['shape']>>('rounded')
  const logoTone = ref<NonNullable<LogoConfig['tone']>>('accent')
  const markSource = ref('icon')
  const customAccent = ref('')
  const spacing = ref('')
  const linked = ref(false)
  const previewConfig = computed<LogoConfig>(() => ({
    brand: brandName(),
    subtitle: tagline(),
    suffix: punctuation(),
    href: linked() ? '/' : null,
    layout: logoLayout(),
    size: logoSize(),
    appearance: treatment(),
    markStyle: markTreatment(),
    wordmarkStyle: wordmarkTreatment(),
    shape: markShape(),
    tone: logoTone(),
    icon: markSource() === 'icon' ? 'tabler:stack-2' : undefined,
    imageSrc:
      markSource() === 'image' ? '/assets/pure-stack-logo.png' : undefined,
    accentColor: customAccent() || undefined,
    gap: spacing() || undefined,
  }))
  return {
    brandName,
    tagline,
    punctuation,
    logoLayout,
    logoSize,
    treatment,
    markTreatment,
    wordmarkTreatment,
    markShape,
    logoTone,
    markSource,
    customAccent,
    spacing,
    linked,
    previewConfig,
    configSource: computed(() => JSON.stringify(previewConfig(), null, 2)),
    layouts: ['horizontal', 'stacked', 'wordmark', 'mark'].map((value) => ({
      label: value,
      value,
    })),
    sizes: ['sm', 'md', 'lg', 'xl'].map((value) => ({ label: value, value })),
    spacings: [
      { label: 'Default (compact)', value: '' },
      { label: 'None · 0px', value: '0px' },
      { label: 'Tight · 4px', value: '0.25rem' },
      { label: 'Compact · 6px', value: '0.375rem' },
      { label: 'Comfortable · 8px', value: '0.5rem' },
      { label: 'Relaxed · 12px', value: '0.75rem' },
      { label: 'Wide · 16px', value: '1rem' },
    ],
    treatments: ['plain', 'badge', 'outline'].map((value) => ({
      label: value,
      value,
    })),
    marks: ['plain', 'soft', 'solid', 'outline'].map((value) => ({
      label: value,
      value,
    })),
    wordmarks: ['plain', 'accent', 'gradient'].map((value) => ({
      label: value,
      value,
    })),
    shapes: ['rounded', 'square', 'circle'].map((value) => ({
      label: value,
      value,
    })),
    tones: [
      'accent',
      'neutral',
      'secondary',
      'info',
      'success',
      'warning',
      'danger',
      'feature',
    ].map((value) => ({ label: value, value })),
    sources: [
      { label: 'Registered icon', value: 'icon' },
      { label: 'Image', value: 'image' },
      { label: 'Monogram', value: 'monogram' },
    ],
    reset: () => {
      brandName('PureStack')
      tagline('')
      punctuation('.')
      logoLayout('horizontal')
      logoSize('md')
      treatment('plain')
      markTreatment('plain')
      wordmarkTreatment('plain')
      markShape('rounded')
      logoTone('accent')
      markSource('icon')
      customAccent('')
      spacing('')
      linked(false)
    },
  }
}

const logoPlayground = defineComponent<LogoPlayground>(logoPlaygroundTemplate, {
  context: createLogoPlayground,
})
const icons: Record<string, string> = {
  'tabler:stack-2': tabler_stack_2,
  'lucide:chevron-down': lucide_chevron_down,
}
createApp(
  {
    components: {
      LogoPlayground: logoPlayground,
      ...defineLogoComponents(),
      ...defineFlexComponents(),
      ...defineGridComponents(),
      ...defineBadgeComponents(),
      ...defineButtonComponents(),
      ...definePanelComponents(),
      ...defineFormComponents(),
      ...defineFormInputField(),
      ...defineFormSelectField(),
      ...defineIconComponents((name) => icons[name] ?? ''),
    },
  },
  { selector: 'app#logo-playground', template: html`<LogoPlayground/>` },
)

Appearances and compositions

Keep the identity consistent while adapting its presentation. These examples share one configuration and override only the settings that differ. The final example supplies its own vector through the mark slot.

<RegorApp id="logo-appearances" src="./appearances.ts"/>
import {
  defineFlexComponents,
  defineGridComponents,
  defineIconComponents,
  defineLogoComponents,
  definePanelComponents,
  type LogoConfig,
} from '@purestack/ts-components'
import { tabler_stack_2 } from '@purestack/ts-svg-icons'
import { createApp, defineComponent, html } from 'regor'

export interface LogoAppearances {
  identity: LogoConfig
}

const logoAppearancesTemplate = html`<Grid columns="1" columnsMd="2">
  <Panel variant="outline" bodyClass="p-4 min-w-0">
    <p class="text-eyebrow mt-0">Navigation</p>
    <SiteLogo :config="identity"/>
    <p class="text-muted mb-0">A quiet horizontal signature.</p>
  </Panel>
  <Panel variant="outline" bodyClass="p-4 min-w-0">
    <p class="text-eyebrow mt-0">Brand badge</p>
    <SiteLogo :config="identity" appearance="badge" markStyle="soft" size="sm"/>
    <p class="text-muted mb-0">A contained identity for cards and embeds.</p>
  </Panel>
  <Panel variant="outline" bodyClass="p-4 min-w-0">
    <p class="text-eyebrow mt-0">Centered signature</p>
    <Flex justify="center"><SiteLogo :config="identity" layout="stacked" subtitle="Pure frontend infrastructure" size="lg" markStyle="solid"/></Flex>
  </Panel>
  <Panel variant="outline" bodyClass="p-4 min-w-0">
    <p class="text-eyebrow mt-0">Wordmark</p>
    <SiteLogo :config="identity" layout="wordmark" wordmarkStyle="gradient" size="lg"/>
    <p class="text-muted mb-0">Typography with a theme-aware accent.</p>
  </Panel>
  <Panel variant="outline" bodyClass="p-4 min-w-0">
    <p class="text-eyebrow mt-0">Compact marks</p>
    <Flex align="center" wrap="true">
      <SiteLogo :config="identity" layout="mark" markStyle="solid" shape="circle" size="lg"/>
      <SiteLogo :config="identity" layout="mark" markStyle="outline" shape="square" size="lg"/>
      <SiteLogo brand="North Star" monogram="NS" :href="null" layout="mark" markStyle="soft" size="lg" tone="info"/>
    </Flex>
    <p class="text-muted mb-0">Full accessible names, even without a wordmark.</p>
  </Panel>
  <Panel variant="outline" bodyClass="p-4 min-w-0">
    <p class="text-eyebrow mt-0">Custom artwork</p>
    <SiteLogo brand="Orbit" subtitle="A mark slot for your own vector" :href="null" size="lg" appearance="outline" tone="info">
      <template #mark><svg viewBox="0 0 32 32" fill="none" aria-hidden="true">
        <circle cx="16" cy="16" r="5" fill="currentColor"/>
        <ellipse cx="16" cy="16" rx="14" ry="7" stroke="currentColor" stroke-width="2" transform="rotate(-35 16 16)"/>
      </svg></template>
    </SiteLogo>
  </Panel>
</Grid>`

const logoAppearances = defineComponent<LogoAppearances>(
  logoAppearancesTemplate,
  {
    context: () => ({
      identity: {
        brand: 'PureStack',
        suffix: '.',
        icon: 'tabler:stack-2',
        href: null,
      },
    }),
  },
)
createApp(
  {
    components: {
      LogoAppearances: logoAppearances,
      ...defineLogoComponents(),
      ...defineFlexComponents(),
      ...defineGridComponents(),
      ...definePanelComponents(),
      ...defineIconComponents((name) =>
        name === 'tabler:stack-2' ? tabler_stack_2 : '',
      ),
    },
  },
  { selector: 'app#logo-appearances', template: html`<LogoAppearances/>` },
)

Choose a layout

Layout Displays Useful for
horizontal Mark beside the wordmark and subtitle Headers, footers and navigation
stacked Mark centered above the text Welcome screens and brand cards
wordmark Brand text and optional subtitle Editorial headings and compact signatures
mark Only the icon, image or monogram Compact navigation and application tiles

Choose a treatment

These choices are independent: you can combine an outlined container with a solid mark, or a plain container with a gradient wordmark.

Setting Choices What changes
appearance plain, badge, outline The whole logo's container, padding and border
markStyle plain, soft, solid, outline The mark's background, border and inset
wordmarkStyle plain, accent, gradient The brand text's color treatment
shape rounded, square, circle The mark's outer shape
size sm, md, lg, xl Coordinated wordmark, mark and subtitle sizes

The default wordmark keeps your original casing, spacing and text shaping. It does not split your brand into individual letters or force uppercase.

Share a brand across the site

Define your identity once in siteConfig.json. The default documentation header reads this object through TopBar. PureStack Studio's landing header and footer use the same object through its small StudioBrand component.

{
  "logo": {
    "brand": "PureStack",
    "suffix": ".",
    "icon": "tabler:stack-2",
    "href": "/",
    "ariaLabel": "PureStack home",
    "size": "md",
    "markStyle": "plain"
  }
}

In a Regor app, keep the same structure in a typed object:

import type { LogoConfig } from '@purestack/ts-components'

const identity: LogoConfig = {
  brand: 'PureStack',
  suffix: '.',
  icon: 'tabler:stack-2',
  href: '/',
}
<SiteLogo :config="identity"/>
<SiteLogo :config="identity" size="sm"/>
<SiteLogo :config="identity" layout="stacked" subtitle="Pure frontend infrastructure"/>

Direct props override configuration values. An undefined prop inherits from config; an empty subtitle or suffix clears that text, and a null href removes navigation. Props and configuration refs remain reactive, including when the whole configuration object is replaced. A standalone SiteLogo does not implicitly read the site configuration, pass config or explicit brand props.

Icons, images and custom marks

SiteLogo resolves its mark in this order: a mark slot, an image, a registered Icon, then a monogram. The default monogram uses the first character of the first two words in the brand; set monogram for a specific abbreviation such as “PS”.

Register icons through defineIconComponents when creating your app. The examples include their complete registrations. SiteLogo itself does not bundle an icon collection.

For existing artwork, provide the image path. A second image can adapt the artwork to the active dark theme:

<SiteLogo
  brand="Your brand"
  imageSrc="/assets/brand-light.svg"
  imageSrcDark="/assets/brand-dark.svg"
  markSize="3rem"
/>

Images keep their aspect ratio within the mark box. They have empty alt text because the component supplies the accessible name. The dark image follows the site's active theme, including a manual theme switch; it is only used when imageSrc is also provided. tone and markColor tint SVG icons and monograms, not the pixels inside an image.

For your own SVG or markup, use Regor's named template syntax:

<SiteLogo brand="Orbit" :href="null">
  <template #mark>
    <svg viewBox="0 0 32 32" fill="none" aria-hidden="true">
      <circle cx="16" cy="16" r="10" stroke="currentColor" stroke-width="3"/>
    </svg>
  </template>
</SiteLogo>

The mark is decorative content within the logo's accessible name. Do not place buttons, links or other interactive controls in this slot.

Spacing between the mark and title

gap controls the space between the logo mark and the entire text block. The default is 0.375rem (6px) for a horizontal logo and 0.5rem (8px) for a stacked logo. These compact defaults apply to the landing header, footer and documentation header automatically.

Override a single instance with a CSS length:

<SiteLogo brand="PureStack" icon="tabler:stack-2" suffix="." gap="0.25rem"/>

To share the same spacing across the site, set gap inside the existing logo configuration in siteConfig.json:

{
  "logo": {
    "brand": "PureStack",
    "suffix": ".",
    "icon": "tabler:stack-2",
    "href": "/",
    "gap": "0.25rem"
  }
}

A direct gap prop overrides config.gap. Omit it to use the defaults; gap="0px" removes the layout gap. In stacked layout, the same prop controls vertical spacing. It does not change the space between the brand name and subtitle, or remove transparent padding inside an image or SVG.

Size and color customization

The four sizes coordinate the mark, brand and subtitle. The default medium size scales gently on small screens. For your own fluid sizing, use CSS lengths directly instead of separate props for every breakpoint:

<SiteLogo
  brand="PureStack"
  icon="tabler:stack-2"
  suffix="."
  brandSize="clamp(1.25rem, 3vw, 2rem)"
  markSize="clamp(1.75rem, 4vw, 2.75rem)"
  subtitleSize="0.75rem"
  gap="0.375rem"
/>

tone gets colors from the active skin. Use brandColor and accentColor for specific brand colors, and markBackground and markColor to customize the mark independently. CSS theme variables work too. The gradient wordmark blends the brand and accent colors; the solid mark uses the tone's button colors unless overridden.

The subtitle wraps and the wordmark can break on narrow screens. For very constrained navigation, choose mark or a smaller size. SiteLogo does not automatically hide your brand or subtitle at a viewport breakpoint.

API reference

Identity · presentation · reuse

Import LogoConfig for shared data and SiteLogo for the component type. Individual props accept literals or refs; config also accepts a reactive object. Bind reactive values with colon-prefixed attributes.

Identity and reuse

config

RefOrValue<Partial<LogoConfig>>
Default
not set

Shared settings. Explicit props override individual fields, so one identity can serve a header, footer and centered signature.

brand

RefOrValue<string>
Default
empty string

Brand name as ordinary text. Provide it even for a mark-only logo so it can supply the accessible name.

subtitle

RefOrValue<string>
Default
not set

Optional secondary line. It wraps naturally and is hidden in mark-only layout. An empty string clears a configured subtitle.

suffix

RefOrValue<string>
Default
not set

Accent-colored text appended directly to the brand, such as a period. An empty string clears a configured suffix.

Layout and treatments

layout

RefOrValue<LogoLayout>
Default
horizontal

horizontal, stacked, wordmark or mark. Determines which parts are shown and their arrangement.

size

RefOrValue<LogoSize>
Default
md

sm, md, lg or xl. Coordinates mark, brand and subtitle dimensions without changing the identity.

appearance

RefOrValue<LogoAppearance>
Default
plain

plain has no container decoration. badge adds a tinted surface and border. outline adds padding and a border.

markStyle

RefOrValue<LogoMarkStyle>
Default
plain

plain, soft, solid or outline. Applies to the mark box independently of the container appearance.

wordmarkStyle

RefOrValue<LogoWordmarkStyle>
Default
plain

plain uses the brand color; accent uses the accent color; gradient blends both. The suffix keeps its accent color.

shape

RefOrValue<LogoShape>
Default
rounded

rounded, square or circle. Shapes and clips the mark box without changing the wordmark.

Mark sources

icon

RefOrValue<string>
Default
not set

Registered SVG icon name, such as tabler:stack-2. Used when there is no mark slot or image.

imageSrc

RefOrValue<string>
Default
not set

Image URL for the mark. Takes precedence over icon and monogram. The image is contained within the mark dimensions.

imageSrcDark

RefOrValue<string>
Default
not set

Optional dark-theme counterpart to imageSrc. Follows the active site theme.

monogram

RefOrValue<string>
Default
derived from brand

Fallback mark text when no image or icon is supplied. Defaults to the first characters of the first two words; provide a short abbreviation for a specific identity.

Colors and custom dimensions

tone

RefOrValue<SemanticTone>
Default
accent

Semantic palette for the mark and accent text. The plain wordmark uses the neutral text color so it remains readable on the surrounding surface.

brandColor · accentColor

RefOrValue<string>
Default
theme colors

CSS colors or theme variables for the wordmark and accent. Gradient wordmarks blend these two values.

markBackground · markColor

RefOrValue<string>
Default
mark treatment

Override the mark background and foreground independently. Backgrounds also accept CSS gradients. Foreground color affects currentColor SVGs and monograms.

brandSize · subtitleSize · markSize

RefOrValue<string>
Default
size preset

CSS lengths overriding the corresponding preset dimensions. Use rem, px or clamp() values; markSize sets both width and height.

gap

RefOrValue<string>
Default
0.375rem; stacked 0.5rem

Space between the mark and text block: horizontal in horizontal layout, vertical when stacked. Accepts CSS lengths such as 0.25rem, 6px or 0px. Has no visible effect in mark-only or wordmark-only layout.

Set it on an instance or in the shared logo configuration. Try the Mark-to-title spacing control in the playground above.

href

RefOrValue<string | null>
Default
/

Anchor destination. Null or an empty string removes the href and makes the logo noninteractive. Relative public paths honor the SSG base path.

ariaLabel

RefOrValue<string>
Default
brand name

Accessible name for the whole logo. Use a meaningful brand or destination label; the visual children do not create duplicate announcements.

Slots and registration

The named mark slot replaces the built-in image/icon/monogram content. It is omitted in wordmark-only layout. There is no default content slot.

Register defineLogoComponents() and defineIconComponents() for icon marks. The standard SSG component registration includes both; client examples register their own dependencies.

Accessibility and navigation

The root has a single accessible name from ariaLabel, then brand, then monogram, with “Home” as a last fallback. Visual mark and text children are hidden from assistive technology to avoid duplicate announcements. A meaningful name is especially important in mark-only layout.

A linked logo is an anchor with native keyboard navigation and a visible focus outline. Use href="/" for home, or another destination with an appropriate label. With :href="null" or an empty href, the logo has an image role, no link destination and no default tab stop. In an SSG page, logo links and image paths are resolved against the configured public base path.

  • TopBar: the documentation header that consumes the shared logo configuration.
  • SiteFooter: compose a brand signature with footer content.
  • Icon: named SVG artwork for icon marks.