Site Header
Marketing top bar for the navbar layout: brand, stepped lead-in list, links and one call to action.
import { SiteHeader } from "@/components/site-header/site-header";import { Button } from "@/components/ui/button";
export default function Demo() { return ( <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";
const steps = [ { label: "Start", href: "#start" }, { label: "Build", href: "#build" }, { label: "Sell", href: "#sell" }, { label: "Scale", href: "#scale" },];const links = [ { label: "Resources", href: "#resources" }, { label: "Pricing", href: "#pricing" },];</script>
<template> <SiteHeader name="Stockbreak" lead="How to" :steps="steps" :links="links"> <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";
const steps = [ { label: "Start", href: "#start" }, { label: "Build", href: "#build" }, { label: "Sell", href: "#sell" }, { label: "Scale", href: "#scale" }, ]; const links = [ { label: "Resources", href: "#resources" }, { label: "Pricing", href: "#pricing" }, ];</script>
<SiteHeader name="Stockbreak" lead="How to" {steps} {links}> {#snippet action()} <Button>Create an index</Button> {/snippet}</SiteHeader>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/site-headerpnpm dlx shadcn@latest add @edmi-ui/site-headeryarn dlx shadcn@latest add @edmi-ui/site-headerbunx shadcn@latest add @edmi-ui/site-headerPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/site-headerpnpm dlx shadcn-vue@latest add @edmi-ui/site-headeryarn dlx shadcn-vue@latest add @edmi-ui/site-headerbunx shadcn-vue@latest add @edmi-ui/site-headerPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/site-header.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/site-header.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/site-header.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/site-header.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
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>Component source
Section titled “Component source”"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 };<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><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><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>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";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";<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><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><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>Elevation ✦
Section titled “Elevation ✦”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).
import { SiteHeader } from "@/components/site-header/site-header";import { Button } from "@/components/ui/button";import type { Elevation } from "@/components/ui/elevation";
function Sample({ elevation }: { elevation: Elevation }) { return ( <SiteHeader name="Stockbreak" elevation={elevation} 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 elevation={elevation}>Create an index</Button>} /> );}
const levels = [ { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" },] as const;
export default function Demo() { return ( <div className="flex flex-col gap-6"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <Sample elevation={value} /> </div> ))} </div> );}<script setup lang="ts">import { SiteHeader } from "@/components/ui/site-header";import { Button } from "@/components/ui/button";
const steps = [ { label: "Start", href: "#start" }, { label: "Build", href: "#build" }, { label: "Sell", href: "#sell" }, { label: "Scale", href: "#scale" },];const links = [ { label: "Resources", href: "#resources" }, { label: "Pricing", href: "#pricing" },];
const levels = [ { value: "flat", label: "Flat (0)" } as const, { value: "raised", label: "Raised (+1)" } as const, { value: "floating", label: "Floating (+2)" } as const,];</script>
<template> <div class="flex flex-col gap-6"> <div v-for="l in levels" :key="l.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ l.label }}</p> <SiteHeader name="Stockbreak" :elevation="l.value" lead="How to" :steps="steps" :links="links"> <template #action> <Button :elevation="l.value">Create an index</Button> </template> </SiteHeader> </div> </div></template><script lang="ts"> import { Button } from "$lib/components/ui/button"; import { SiteHeader } from "$lib/components/ui/site-header";
const steps = [ { label: "Start", href: "#start" }, { label: "Build", href: "#build" }, { label: "Sell", href: "#sell" }, { label: "Scale", href: "#scale" }, ]; const links = [ { label: "Resources", href: "#resources" }, { label: "Pricing", href: "#pricing" }, ];
const levels = [ { value: "flat", label: "Flat (0)" } as const, { value: "raised", label: "Raised (+1)" } as const, { value: "floating", label: "Floating (+2)" } as const, ];</script>
<div class="flex flex-col gap-6"> {#each levels as l (l.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{l.label}</p><SiteHeader name="Stockbreak" elevation={l.value} lead="How to" {steps} {links}> {#snippet action({ elevation })} <Button {elevation}>Create an index</Button> {/snippet}</SiteHeader> </div> {/each}</div>| 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.
✦ Edmi additions
Section titled “✦ Edmi additions”- Whole block is new (no shadcn counterpart): composed from the card recipe plus a wordmark slot (Sora,
font-brand).