Skip to content
edmi-ui

Site Header

Marketing top bar for the navbar layout: brand, stepped lead-in list, links and one call to action.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Stockbreak
Terminal window
npx shadcn@latest add @edmi-ui/site-header
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/site-header
Terminal window
yarn dlx shadcn@latest add @edmi-ui/site-header
Terminal window
bunx shadcn@latest add @edmi-ui/site-header

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/site-header
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/site-header
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/site-header
Terminal window
bunx shadcn-vue@latest add @edmi-ui/site-header

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/site-header.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/site-header.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/site-header.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/site-header.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import { SiteHeader } from "@/components/site-header/site-header";
import { Button } from "@/components/ui/button";
<SiteHeader
name="Stockbreak"
lead="How to"
steps={[
{ label: "Start", href: "#start" },
{ label: "Build", href: "#build" },
{ label: "Sell", href: "#sell" },
{ label: "Scale", href: "#scale" },
]}
links={[
{ label: "Resources", href: "#resources" },
{ label: "Pricing", href: "#pricing" },
]}
action={<Button>Create an index</Button>}
/>
<script setup lang="ts">
import { SiteHeader } from "@/components/ui/site-header";
import { Button } from "@/components/ui/button";
</script>
<template>
<SiteHeader name="Stockbreak" lead="How to">
<template #action>
<Button>Create an index</Button>
</template>
</SiteHeader>
</template>
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { SiteHeader } from "$lib/components/ui/site-header";
</script>
<SiteHeader name="Stockbreak" lead="How to" >
{#snippet action()}
<Button>Create an index</Button>
{/snippet}
</SiteHeader>
registry/blocks/site-header/site-header.tsx
"use client";
import { cn } from "cn";
import type * as React from "react";
import { ChartLineUpIcon } from "@phosphor-icons/react";
import {
controlLevel,
type Elevation,
surfaceElevation,
useElevation,
} from "@/registry/edmi/ui/elevation";
type SiteHeaderLink = { label: string; href: string };
// Dark tile with a chart glyph (`raised` adds the raised face). Pass `logo` to replace it.
function SiteHeaderMark({
className,
elevation,
...props
}: React.ComponentProps<"span"> & { elevation?: Elevation }) {
const level = useElevation(elevation, "handle");
const raised = level === "raised" || level === "floating";
return (
<span
data-slot="site-header-mark"
className={cn(
"inline-flex size-7 items-center justify-center rounded-lg border border-transparent bg-primary text-primary-foreground",
raised &&
"[background-image:var(--r1-p-face)] shadow-btn-raised-primary [background-origin:border-box]",
className,
)}
{...props}
>
<ChartLineUpIcon
className="size-[15px]"
/>
</span>
);
}
function SiteHeaderBrand({
className,
logo,
name,
href = "/",
elevation,
...props
}: Omit<React.ComponentProps<"a">, "children"> & {
logo?: React.ReactNode;
name?: React.ReactNode;
/** ✦ depth of the mark. */
elevation?: Elevation;
}) {
return (
<a
data-slot="site-header-brand"
href={href}
className={cn("flex items-center gap-2.5 whitespace-nowrap", className)}
{...props}
>
{logo ?? <SiteHeaderMark elevation={elevation} />}
{name != null ? (
<span className="font-brand text-xl font-semibold tracking-[-0.4px]">
{name}
</span>
) : null}
</a>
);
}
// Marketing top bar (navbar layout): brand, a muted lead-in with a stepped list, plain links, one CTA.
function SiteHeader({
className,
logo,
name,
href,
lead,
steps = [],
links = [],
action,
elevation,
...props
}: Omit<React.ComponentProps<"header">, "children"> & {
logo?: React.ReactNode;
name?: React.ReactNode;
href?: string;
/** Muted lead-in before the stepped list, e.g. "How to". */
lead?: React.ReactNode;
steps?: SiteHeaderLink[];
links?: SiteHeaderLink[];
/** Trailing slot, usually a `<Button>`. */
action?: React.ReactNode;
/** ✦ depth of the header plate; raised +1 / floating +2 also raise the mark (pass `elevation` to the CTA Button yourself). */
elevation?: Elevation;
}) {
const level = useElevation(elevation, "surface");
return (
<header
data-slot="site-header"
className={cn(
"flex w-full items-center justify-between gap-6 rounded-xl border border-border bg-card px-5 py-3.5 text-sm text-card-foreground",
surfaceElevation[level],
className,
)}
{...props}
>
<SiteHeaderBrand
logo={logo}
name={name}
href={href}
elevation={controlLevel(elevation)}
/>
<nav
className="flex items-center gap-1 whitespace-nowrap max-md:hidden"
aria-label="Main"
>
{lead ? <span className="text-muted-foreground-2">{lead}</span> : null}
{steps.map((s) => (
<span key={s.href} className="flex items-center">
<span className="mx-2.5 h-3 w-px bg-border" aria-hidden />
<a href={s.href} className="hover:text-muted-foreground">
{s.label}
</a>
</span>
))}
{links.map((l, i) => (
<a
key={l.href}
href={l.href}
className={cn(
"hover:text-muted-foreground",
i === 0 ? "ml-7" : "ml-[18px]",
)}
>
{l.label}
</a>
))}
</nav>
{action ? (
<div className="ml-4 flex shrink-0 items-center">{action}</div>
) : null}
</header>
);
}
export type { SiteHeaderLink };
export { SiteHeader, SiteHeaderBrand, SiteHeaderMark };
registry/ui/site-header/SiteHeader.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { SiteHeaderLink } from '.'
import { computed } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
import { controlLevel, type Elevation, surfaceElevation, useElevation } from '@/registry/edmi/ui/elevation'
import SiteHeaderBrand from './SiteHeaderBrand.vue'
const props = withDefaults(defineProps<{
/** Brand text (or use the `name` slot). */
name?: string
href?: string
/** Muted lead-in before the stepped list, e.g. "How to" (or use the `lead` slot). */
lead?: string
steps?: SiteHeaderLink[]
links?: SiteHeaderLink[]
/** ✦ depth of the bar; raised +1 / floating +2 also raise the mark (pass `elevation` to the CTA Button yourself) */
elevation?: Elevation
class?: HTMLAttributes['class']
}>(), {
steps: () => [],
links: () => [],
elevation: undefined,
})
const level = useElevation(() => props.elevation, 'surface')
const mark = computed(() => controlLevel(props.elevation))
</script>
<template>
<!-- Marketing top bar (navbar layout): brand, a muted lead-in with a stepped list, plain links, one CTA (`action` slot). -->
<header
data-slot="site-header"
:class="cn(
'flex w-full items-center justify-between gap-6 rounded-xl border border-border bg-card px-5 py-3.5 text-sm text-card-foreground',
surfaceElevation[level],
props.class,
)"
>
<SiteHeaderBrand :name="name" :href="href" :elevation="mark">
<template v-if="$slots.logo" #logo>
<slot name="logo" />
</template>
<template v-if="$slots.name" #name>
<slot name="name" />
</template>
</SiteHeaderBrand>
<nav class="flex items-center gap-1 whitespace-nowrap max-md:hidden" aria-label="Main">
<span v-if="lead || $slots.lead" class="text-muted-foreground-2">
<slot name="lead">{{ lead }}</slot>
</span>
<span v-for="s in steps" :key="s.href" class="flex items-center">
<span class="mx-2.5 h-3 w-px bg-border" aria-hidden="true" />
<a :href="s.href" class="hover:text-muted-foreground">{{ s.label }}</a>
</span>
<a
v-for="(l, i) in links"
:key="l.href"
:href="l.href"
:class="cn('hover:text-muted-foreground', i === 0 ? 'ml-7' : 'ml-[18px]')"
>{{ l.label }}</a>
</nav>
<div v-if="$slots.action" class="ml-4 flex shrink-0 items-center">
<slot name="action" />
</div>
</header>
</template>
registry/ui/site-header/SiteHeaderBrand.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
import type { Elevation } from '@/registry/edmi/ui/elevation'
import SiteHeaderMark from './SiteHeaderMark.vue'
const props = withDefaults(defineProps<{
name?: string
href?: string
/** ✦ depth of the default mark */
elevation?: Elevation
class?: HTMLAttributes['class']
}>(), {
href: '/',
elevation: undefined,
})
</script>
<template>
<a
data-slot="site-header-brand"
:href="href"
:class="cn('flex items-center gap-2.5 whitespace-nowrap', props.class)"
>
<slot name="logo">
<SiteHeaderMark :elevation="elevation" />
</slot>
<span v-if="name || $slots.name" class="font-brand text-xl font-semibold tracking-[-0.4px]">
<slot name="name">{{ name }}</slot>
</span>
</a>
</template>
registry/ui/site-header/SiteHeaderMark.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { ChartLineIcon } from '@lucide/vue'
import { computed } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
import { type Elevation, useElevation } from '@/registry/edmi/ui/elevation'
const props = withDefaults(defineProps<{
/** ✦ raised +1 / floating +2 give the mark the raised face */
elevation?: Elevation
class?: HTMLAttributes['class']
}>(), { elevation: undefined })
const level = useElevation(() => props.elevation, 'handle')
const raised = computed(() => level.value === 'raised' || level.value === 'floating')
</script>
<template>
<!-- Dark tile with a chart glyph: Use the `logo` slot of the brand/header to replace it. -->
<span
data-slot="site-header-mark"
:class="cn(
'inline-flex size-7 items-center justify-center rounded-lg border border-transparent bg-primary text-primary-foreground',
raised && '[background-image:var(--r1-p-face)] shadow-btn-raised-primary [background-origin:border-box]',
props.class,
)"
>
<ChartLineIcon class="size-[15px]" />
</span>
</template>
registry/ui/site-header/index.ts
export type SiteHeaderLink = { label: string; href: string };
export { default as SiteHeader } from "./SiteHeader.vue";
export { default as SiteHeaderBrand } from "./SiteHeaderBrand.vue";
export { default as SiteHeaderMark } from "./SiteHeaderMark.vue";
src/lib/registry/ui/site-header/index.ts
export {
default as SiteHeader,
type SiteHeaderLink,
} from "./site-header.svelte";
export { default as SiteHeaderBrand } from "./site-header-brand.svelte";
export { default as SiteHeaderMark } from "./site-header-mark.svelte";
src/lib/registry/ui/site-header/site-header-brand.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAnchorAttributes } from "svelte/elements";
import type { Snippet } from "svelte";
import type { Elevation } from "$lib/registry/ui/elevation/index.js";
import SiteHeaderMark from "./site-header-mark.svelte";
let {
ref = $bindable(null),
class: className,
logo,
name,
href = "/",
elevation = "auto",
...restProps
}: WithElementRef<Omit<HTMLAnchorAttributes, "children">, HTMLAnchorElement> & {
/** Replaces the default mark. */
logo?: Snippet;
/** Brand text or a snippet. */
name?: string | Snippet;
/** ✦ depth of the default mark. */
elevation?: Elevation;
} = $props();
</script>
<a
bind:this={ref}
data-slot="site-header-brand"
{href}
class={cn("flex items-center gap-2.5 whitespace-nowrap", className)}
{...restProps}
>
{#if logo}{@render logo()}{:else}<SiteHeaderMark {elevation} />{/if}
{#if name}
<span class="font-brand text-xl font-semibold tracking-[-0.4px]">
{#if typeof name === "string"}{name}{:else}{@render name()}{/if}
</span>
{/if}
</a>
src/lib/registry/ui/site-header/site-header-mark.svelte
<script lang="ts">
import ChartLineUpIcon from "phosphor-svelte/lib/ChartLineUp";
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js";
let {
ref = $bindable(null),
class: className,
elevation = "auto",
...restProps
}: WithElementRef<Omit<HTMLAttributes<HTMLSpanElement>, "children">> & {
/** ✦ raised +1 / floating +2 give the mark the raised face. */
elevation?: Elevation;
} = $props();
const level = useElevation(() => elevation, "handle");
const raised = $derived(level.current === "raised" || level.current === "floating");
</script>
<!-- Dark tile with a chart glyph: Pass a `logo` snippet to the brand/header to replace it. -->
<span
bind:this={ref}
data-slot="site-header-mark"
class={cn(
"inline-flex size-7 items-center justify-center rounded-lg border border-transparent bg-primary text-primary-foreground",
raised && "[background-image:var(--r1-p-face)] shadow-btn-raised-primary [background-origin:border-box]",
className
)}
{...restProps}
>
<ChartLineUpIcon
class="size-[15px]"
/>
</span>
src/lib/registry/ui/site-header/site-header.svelte
<script lang="ts" module>
export type SiteHeaderLink = { label: string; href: string };
</script>
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
import type { Snippet } from "svelte";
import { controlLevel, type Elevation, surfaceElevation, useElevation } from "$lib/registry/ui/elevation/index.js";
import SiteHeaderBrand from "./site-header-brand.svelte";
let {
ref = $bindable(null),
class: className,
logo,
name,
href,
lead,
steps = [],
links = [],
action,
elevation = "auto",
...restProps
}: WithElementRef<Omit<HTMLAttributes<HTMLElement>, "children">> & {
logo?: Snippet;
/** Brand text or a snippet. */
name?: string | Snippet;
href?: string;
/** Muted lead-in before the stepped list, e.g. "How to". */
lead?: string | Snippet;
steps?: SiteHeaderLink[];
links?: SiteHeaderLink[];
/** Trailing slot, usually a `<Button>`; receives `{ elevation }` to forward to the CTA. */
action?: Snippet<[{ elevation: Elevation | undefined }]>;
/** ✦ depth of the bar; raised +1 / floating +2 also raise the mark (CTA via the `action` snippet argument). */
elevation?: Elevation;
} = $props();
const level = useElevation(() => elevation, "surface");
const control = $derived<Elevation | undefined>(controlLevel(elevation));
</script>
<!-- Marketing top bar (navbar layout): brand, a muted lead-in with a stepped list, plain links, one CTA. -->
<header
bind:this={ref}
data-slot="site-header"
class={cn(
"flex w-full items-center justify-between gap-6 rounded-xl border border-border bg-card px-5 py-3.5 text-sm text-card-foreground",
surfaceElevation[level.current],
className
)}
{...restProps}
>
<SiteHeaderBrand {logo} {name} {href} elevation={control ?? "auto"} />
<nav class="flex items-center gap-1 whitespace-nowrap max-md:hidden" aria-label="Main">
{#if lead}
<span class="text-muted-foreground-2">
{#if typeof lead === "string"}{lead}{:else}{@render lead()}{/if}
</span>
{/if}
{#each steps as s (s.href)}
<span class="flex items-center">
<span class="mx-2.5 h-3 w-px bg-border" aria-hidden="true"></span>
<a href={s.href} class="hover:text-muted-foreground">{s.label}</a>
</span>
{/each}
{#each links as l, i (l.href)}
<a href={l.href} class={cn("hover:text-muted-foreground", i === 0 ? "ml-7" : "ml-[18px]")}>{l.label}</a>
{/each}
</nav>
{#if action}
<div class="ml-4 flex shrink-0 items-center">{@render action({ elevation: control })}</div>
{/if}
</header>

See the Elevation guide for the levels, roles and layered mode.

Flat by default. elevation sets the depth of the bar; raised (+1) and floating (+2) also raise the brand mark. Pass elevation to the call-to-action Button yourself (the Svelte action snippet receives { elevation } to forward).

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Raised (+1)

Stockbreak

Floating (+2)

Stockbreak

Raised (+1)

Stockbreak

Floating (+2)

Stockbreak

Flat (0)

Stockbreak

Raised (+1)

Stockbreak

Floating (+2)

Stockbreak
Part Prop Type Default
SiteHeader logo ReactNode (Vue logo slot, Svelte snippet) chart mark
name ReactNode (Vue name slot, Svelte string | Snippet) none
href string "/"
lead ReactNode muted text before steps (Vue lead slot, Svelte string | Snippet) none
steps { label, href }[] (separated by 1px dividers) []
links { label, href }[] []
action ReactNode, usually a Button (Vue action slot, Svelte snippet receiving { elevation }) none
elevation "auto" | "sunken" | "flat" | "raised" | "floating" "auto"
SiteHeaderBrand logo, name, href, elevation as above; the name is not rendered when empty
SiteHeaderMark elevation raised +1 / floating +2 give the mark the raised face "auto"

Nav hides below md.

  • Whole block is new (no shadcn counterpart): composed from the card recipe plus a wordmark slot (Sora, font-brand).