App Header
App top bar for the navbar layout: brand, nav pills, search, network button and wallet connect.
import { AppHeader } from "@/components/app-header/app-header";import { useState } from "react";
const items = [ { label: "Explore", href: "#explore" }, { label: "Feed", href: "#feed" }, { label: "Leaderboard", href: "#leaderboard" }, { label: "AI", href: "#ai" }, { label: "Create", href: "#create" },];
export default function Demo() { const [connected, setConnected] = useState(false); return ( <AppHeader name="Stockbreak" network="Devnet" items={items} active="#leaderboard" connectLabel={connected ? "dG6r…4mSr" : "Connect"} onConnect={() => setConnected((c) => !c)} /> );}<script setup lang="ts">import { ref } from "vue";import { AppHeader } from "@/components/ui/app-header";
const items = [ { label: "Explore", href: "#explore" }, { label: "Feed", href: "#feed" }, { label: "Leaderboard", href: "#leaderboard" }, { label: "AI", href: "#ai" }, { label: "Create", href: "#create" },];const connected = ref(false);</script>
<template> <AppHeader name="Stockbreak" network="Devnet" :items="items" active="#leaderboard" :connect-label="connected ? 'dG6r…4mSr' : 'Connect'" @connect="connected = !connected" /></template><script lang="ts"> import { AppHeader } from "$lib/components/ui/app-header";
const items = [ { label: "Explore", href: "#explore" }, { label: "Feed", href: "#feed" }, { label: "Leaderboard", href: "#leaderboard" }, { label: "AI", href: "#ai" }, { label: "Create", href: "#create" }, ]; let connected = $state(false);</script>
<AppHeader name="Stockbreak" network="Devnet" {items} active="#leaderboard" connectLabel={connected ? "dG6r…4mSr" : "Connect"} onConnect={() => (connected = !connected)}/>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/app-headerpnpm dlx shadcn@latest add @edmi-ui/app-headeryarn dlx shadcn@latest add @edmi-ui/app-headerbunx shadcn@latest add @edmi-ui/app-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:
button,input-group,kbd,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/app-headerpnpm dlx shadcn-vue@latest add @edmi-ui/app-headeryarn dlx shadcn-vue@latest add @edmi-ui/app-headerbunx shadcn-vue@latest add @edmi-ui/app-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:
button,input-group,kbd,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/app-header.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/app-header.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/app-header.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/app-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:
button,input-group,kbd,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { AppHeader } from "@/components/app-header/app-header";
<AppHeader name="Stockbreak" network="Devnet" active="#leaderboard"/><script setup lang="ts">import { AppHeader } from "@/components/ui/app-header";</script>
<template> <AppHeader name="Stockbreak" network="Devnet" active="#leaderboard" /></template><script lang="ts"> import { AppHeader } from "$lib/components/ui/app-header";</script>
<AppHeader name="Stockbreak" network="Devnet" active="#leaderboard"/>Component source
Section titled “Component source”"use client";
import { cn } from "cn";import type * as React from "react";import { ChartLineUpIcon, MagnifyingGlassIcon, WalletIcon } from "@phosphor-icons/react";import { Button } from "@/registry/edmi/ui/button";import { controlLevel, type Elevation, type ElevationLevel, surfaceElevation, useElevation,} from "@/registry/edmi/ui/elevation";import { InputGroup, InputGroupAddon, InputGroupInput,} from "@/registry/edmi/ui/input-group";import { Kbd } from "@/registry/edmi/ui/kbd";
type AppHeaderItem = { label: string; href: string };
// A header that rises hands the same depth to the controls it renders; `flat` and `sunken` keep them flat.const rises = (level: ElevationLevel) => level === "raised" || level === "floating";
function AppHeaderMark({ elevation }: { elevation?: Elevation }) { const raised = rises(useElevation(elevation, "handle")); return ( <span 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]", )} > <ChartLineUpIcon className="size-[15px]" /> </span> );}
// Nav pills stand alone (no track): the `pills` Tabs look. Active = --tab-active + 1px border; `raised` makes the active pill a raised secondary button.function AppHeaderNavItem({ className, active, elevation, ...props}: React.ComponentProps<"a"> & { active?: boolean; /** ✦ raised +1 / floating +2 raise the active pill. */ elevation?: Elevation;}) { const raised = rises(useElevation(elevation, "control")); return ( <a data-slot="app-header-nav-item" data-active={active ? "" : undefined} aria-current={active ? "page" : undefined} className={cn( "inline-flex h-8 items-center rounded-[7px] border border-transparent px-3 text-[13.5px] font-medium whitespace-nowrap text-muted-foreground transition-colors outline-none hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", "data-[active]:border-border data-[active]:bg-tab-active data-[active]:text-foreground", raised && "data-[active]:border-transparent data-[active]:[background-image:var(--r1-s-face)] data-[active]:shadow-btn-raised-neutral data-[active]:[background-origin:border-box]", className, )} {...props} /> );}
// App top bar (navbar layout): brand + nav pills, then search, network and wallet.function AppHeader({ className, logo, name, href = "/", items = [], active, search = true, searchPlaceholder = "Search", shortcut = "⌘K", network, connectLabel = "Connect", onConnect, onNetworkClick, elevation, ...props}: Omit<React.ComponentProps<"header">, "children"> & { logo?: React.ReactNode; name?: React.ReactNode; href?: string; items?: AppHeaderItem[]; /** `href` of the active item. */ active?: string; /** `false` hides the search field. */ search?: boolean; searchPlaceholder?: string; shortcut?: string; /** Secondary button label (e.g. a network or workspace); omit or pass an empty string to hide it. */ network?: string; connectLabel?: React.ReactNode; onConnect?: () => void; onNetworkClick?: () => void; /** ✦ depth of the header plate; raised +1 / floating +2 also raise the mark, active pill and buttons. */ elevation?: Elevation;}) { const level = useElevation(elevation, "surface"); const control = controlLevel(elevation); return ( <header data-slot="app-header" className={cn( "@container/app-header flex w-full items-center justify-between gap-4 rounded-xl border border-border bg-card px-5 py-3 text-sm text-card-foreground", surfaceElevation[level], className, )} {...props} > <div className="flex items-center gap-[18px]"> <a href={href} className="flex items-center gap-2.5 whitespace-nowrap"> {logo ?? <AppHeaderMark elevation={control} />} {name != null ? ( <span className="font-brand text-xl font-semibold tracking-[-0.4px]"> {name} </span> ) : null} </a> <nav className="flex items-center gap-0.5 max-lg:hidden" aria-label="App" > {items.map((item) => ( <AppHeaderNavItem key={item.href} href={item.href} active={item.href === active} elevation={control} > {item.label} </AppHeaderNavItem> ))} </nav> </div> <div className="flex items-center gap-2"> {search ? ( <InputGroup className="w-[180px] @max-[960px]/app-header:hidden"> <InputGroupAddon> <MagnifyingGlassIcon /> </InputGroupAddon> <InputGroupInput placeholder={searchPlaceholder} aria-label={searchPlaceholder} className="text-[13px]" /> <InputGroupAddon align="inline-end"> <Kbd elevation={control}>{shortcut}</Kbd> </InputGroupAddon> </InputGroup> ) : null} {network ? ( <Button variant="secondary" elevation={control} onClick={onNetworkClick} > {network} </Button> ) : null} <Button elevation={control} onClick={onConnect}> <WalletIcon /> {connectLabel} </Button> </div> </header> );}
export type { AppHeaderItem };export { AppHeader, AppHeaderNavItem };<script setup lang="ts">import type { HTMLAttributes } from 'vue'import type { AppHeaderItem } from '.'import { ChartLineIcon, Search, WalletIcon } from '@lucide/vue'import { computed } from 'vue'import { cn } from '@/registry/edmi/lib/utils'import { Button } from '@/registry/edmi/ui/button'import { controlLevel, type Elevation, surfaceElevation, useElevation } from '@/registry/edmi/ui/elevation'import { InputGroup, InputGroupAddon, InputGroupInput } from '@/registry/edmi/ui/input-group'import { Kbd } from '@/registry/edmi/ui/kbd'import AppHeaderNavItem from './AppHeaderNavItem.vue'
const props = withDefaults(defineProps<{ /** Brand text (or use the `name` slot). */ name?: string href?: string items?: AppHeaderItem[] /** `href` of the active item. */ active?: string /** `false` hides the search field. */ search?: boolean searchPlaceholder?: string shortcut?: string /** Secondary button label (e.g. a network or workspace; or use the `network` slot); omit to hide it. */ network?: string /** Primary button label (or use the `connect-label` slot). */ connectLabel?: string /** ✦ depth of the bar; raised +1 / floating +2 also raise the mark, active pill and buttons */ elevation?: Elevation class?: HTMLAttributes['class']}>(), { href: '/', items: () => [], search: true, searchPlaceholder: 'Search', shortcut: '⌘K', connectLabel: 'Connect', elevation: undefined,})
const level = useElevation(() => props.elevation, 'surface')// Controls follow an explicit bar level: raised/floating raise them, flat/sunken keep them flat; auto leaves them to their own role.const control = computed(() => controlLevel(props.elevation))const markLevel = useElevation(() => control.value, 'handle')const markRaised = computed(() => markLevel.value === 'raised' || markLevel.value === 'floating')
const emit = defineEmits<{ (e: 'connect'): void (e: 'network-click'): void}>()</script>
<template> <!-- App top bar (navbar layout): brand + nav pills, then search, network and wallet. --> <header data-slot="app-header" :class="cn( '@container/app-header flex w-full items-center justify-between gap-4 rounded-xl border border-border bg-card px-5 py-3 text-sm text-card-foreground', surfaceElevation[level], props.class, )" > <div class="flex items-center gap-[18px]"> <a :href="href" class="flex items-center gap-2.5 whitespace-nowrap"> <slot name="logo"> <span :class="cn('inline-flex size-7 items-center justify-center rounded-lg border border-transparent bg-primary text-primary-foreground', markRaised && '[background-image:var(--r1-p-face)] shadow-btn-raised-primary [background-origin:border-box]')"> <ChartLineIcon class="size-[15px]" /> </span> </slot> <span v-if="name || $slots.name" class="font-brand text-xl font-semibold tracking-[-0.4px]"><slot name="name">{{ name }}</slot></span> </a> <nav class="flex items-center gap-0.5 max-lg:hidden" aria-label="App"> <AppHeaderNavItem v-for="item in items" :key="item.href" :href="item.href" :active="item.href === active" :elevation="control" > {{ item.label }} </AppHeaderNavItem> </nav> </div> <div class="flex items-center gap-2"> <InputGroup v-if="search" class="w-[180px] @max-[960px]/app-header:hidden"> <InputGroupAddon> <Search /> </InputGroupAddon> <InputGroupInput :placeholder="searchPlaceholder" :aria-label="searchPlaceholder" class="text-[13px]" /> <InputGroupAddon align="inline-end"> <Kbd :elevation="control">{{ shortcut }}</Kbd> </InputGroupAddon> </InputGroup> <Button v-if="network || $slots.network" variant="secondary" :elevation="control" @click="emit('network-click')"> <slot name="network">{{ network }}</slot> </Button> <Button :elevation="control" @click="emit('connect')"> <WalletIcon /> <slot name="connect-label">{{ connectLabel }}</slot> </Button> </div> </header></template><script setup lang="ts">import type { HTMLAttributes } from '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<{ active?: boolean /** ✦ raised +1 / floating +2 raise the active pill */ elevation?: Elevation class?: HTMLAttributes['class']}>(), { elevation: undefined })
const level = useElevation(() => props.elevation, 'control')const raised = computed(() => level.value === 'raised' || level.value === 'floating')</script>
<template> <!-- Nav pills stand alone (no track): the pills look of Tabs; `elevation` raises the active one. --> <a data-slot="app-header-nav-item" :data-active="active ? '' : undefined" :aria-current="active ? 'page' : undefined" :class="cn( 'inline-flex h-8 items-center rounded-[7px] border border-transparent px-3 text-[13.5px] font-medium whitespace-nowrap text-muted-foreground transition-colors outline-none hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring', 'data-[active]:border-border data-[active]:bg-tab-active data-[active]:text-foreground', raised && 'data-[active]:border-transparent data-[active]:[background-image:var(--r1-s-face)] data-[active]:shadow-btn-raised-neutral data-[active]:[background-origin:border-box]', props.class, )" > <slot /> </a></template>export type AppHeaderItem = { label: string; href: string };
export { default as AppHeader } from "./AppHeader.vue";export { default as AppHeaderNavItem } from "./AppHeaderNavItem.vue";<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAnchorAttributes } from "svelte/elements"; import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js";
let { ref = $bindable(null), class: className, active, elevation = "auto", children, ...restProps }: WithElementRef<HTMLAnchorAttributes, HTMLAnchorElement> & { active?: boolean; /** ✦ raised +1 / floating +2 raise the active pill. */ elevation?: Elevation; } = $props();
const level = useElevation(() => elevation, "control"); const raised = $derived(level.current === "raised" || level.current === "floating");</script>
<!-- Nav pills = `TabsList variant="pills"` look: flat active pill, `elevation` raises the active pill. --><a bind:this={ref} data-slot="app-header-nav-item" data-active={active ? "" : undefined} aria-current={active ? "page" : undefined} class={cn( "inline-flex h-8 items-center rounded-[7px] border border-transparent px-3 text-[13.5px] font-medium whitespace-nowrap text-muted-foreground transition-colors outline-none hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", "data-[active]:border-border data-[active]:bg-tab-active data-[active]:text-foreground", raised && "data-[active]:border-transparent data-[active]:[background-image:var(--r1-s-face)] data-[active]:shadow-btn-raised-neutral data-[active]:[background-origin:border-box]", className )} {...restProps}> {@render children?.()}</a><script lang="ts" module> export type AppHeaderItem = { label: string; href: string };</script>
<script lang="ts"> import ChartLineUpIcon from "phosphor-svelte/lib/ChartLineUp"; import MagnifyingGlassIcon from "phosphor-svelte/lib/MagnifyingGlass"; import WalletIcon from "phosphor-svelte/lib/Wallet"; import { Button } from "$lib/registry/ui/button/index.js"; import { InputGroup, InputGroupAddon, InputGroupInput, } from "$lib/registry/ui/input-group/index.js"; import { Kbd } from "$lib/registry/ui/kbd/index.js"; import { controlLevel, type Elevation, surfaceElevation, useElevation } from "$lib/registry/ui/elevation/index.js"; import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements"; import type { Snippet } from "svelte"; import AppHeaderNavItem from "./app-header-nav-item.svelte";
let { ref = $bindable(null), class: className, logo, name, href = "/", items = [], active, search = true, searchPlaceholder = "Search", shortcut = "⌘K", network, connectLabel = "Connect", onConnect, onNetworkClick, elevation = "auto", ...restProps }: WithElementRef<Omit<HTMLAttributes<HTMLElement>, "children">> & { logo?: Snippet; /** Brand text or a snippet. */ name?: string | Snippet; href?: string; items?: AppHeaderItem[]; /** `href` of the active item. */ active?: string; /** `false` hides the search field. */ search?: boolean; searchPlaceholder?: string; shortcut?: string; /** Secondary button label (e.g. a network or workspace); omit to hide it. */ network?: string | Snippet; /** Primary button label. */ connectLabel?: string | Snippet; onConnect?: () => void; onNetworkClick?: () => void; /** ✦ depth of the bar; raised +1 / floating +2 also raise the mark, active pill and buttons. */ elevation?: Elevation; } = $props();
const level = useElevation(() => elevation, "surface"); // Controls follow an explicit bar level (flat/sunken keep them flat); auto leaves them to their own role. const control = $derived<Elevation | undefined>(controlLevel(elevation)); const markLevel = useElevation(() => control ?? "auto", "handle"); const markRaised = $derived(markLevel.current === "raised" || markLevel.current === "floating");</script>
<!-- App top bar (navbar layout): brand + nav pills, then search, network and wallet. --><header bind:this={ref} data-slot="app-header" class={cn( "@container/app-header flex w-full items-center justify-between gap-4 rounded-xl border border-border bg-card px-5 py-3 text-sm text-card-foreground", surfaceElevation[level.current], className )} {...restProps}> <div class="flex items-center gap-[18px]"> <a {href} class="flex items-center gap-2.5 whitespace-nowrap"> {#if logo} {@render logo()} {:else} <span class={cn( "inline-flex size-7 items-center justify-center rounded-lg border border-transparent bg-primary text-primary-foreground", markRaised && "[background-image:var(--r1-p-face)] shadow-btn-raised-primary [background-origin:border-box]" )} > <ChartLineUpIcon class="size-[15px]" /> </span> {/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> <nav class="flex items-center gap-0.5 max-lg:hidden" aria-label="App"> {#each items as item (item.href)} <AppHeaderNavItem href={item.href} active={item.href === active} elevation={control}> {item.label} </AppHeaderNavItem> {/each} </nav> </div> <div class="flex items-center gap-2"> {#if search} <InputGroup class="w-[180px] @max-[960px]/app-header:hidden"> <InputGroupAddon> <MagnifyingGlassIcon /> </InputGroupAddon> <InputGroupInput placeholder={searchPlaceholder} aria-label={searchPlaceholder} class="text-[13px]" /> <InputGroupAddon align="inline-end"> <Kbd elevation={control}>{shortcut}</Kbd> </InputGroupAddon> </InputGroup> {/if} {#if network} <Button variant="secondary" elevation={control} onclick={onNetworkClick}> {#if typeof network === "string"}{network}{:else}{@render network()}{/if} </Button> {/if} <Button elevation={control} onclick={onConnect}> <WalletIcon /> {#if typeof connectLabel === "string"}{connectLabel}{:else}{@render connectLabel()}{/if} </Button> </div></header>export { type AppHeaderItem, default as AppHeader } from "./app-header.svelte";export { default as AppHeaderNavItem } from "./app-header-nav-item.svelte";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 mark, the active nav pill (only the active pill rises) and the buttons. In a layered ElevationProvider the bar is a raised surface and its controls follow their own roles.
import { AppHeader } from "@/components/app-header/app-header";import type { Elevation } from "@/components/ui/elevation";import { useState } from "react";
const items = [ { label: "Explore", href: "#explore" }, { label: "Feed", href: "#feed" }, { label: "Leaderboard", href: "#leaderboard" }, { label: "AI", href: "#ai" }, { label: "Create", href: "#create" },];
function Sample({ elevation }: { elevation: Elevation }) { const [connected, setConnected] = useState(false); return ( <AppHeader name="Stockbreak" network="Devnet" elevation={elevation} items={items} active="#leaderboard" connectLabel={connected ? "dG6r…4mSr" : "Connect"} onConnect={() => setConnected((c) => !c)} /> );}
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 { ref } from "vue";import { AppHeader } from "@/components/ui/app-header";
const items = [ { label: "Explore", href: "#explore" }, { label: "Feed", href: "#feed" }, { label: "Leaderboard", href: "#leaderboard" }, { label: "AI", href: "#ai" }, { label: "Create", href: "#create" },];const connected = ref(false);
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> <AppHeader name="Stockbreak" network="Devnet" :elevation="l.value" :items="items" active="#leaderboard" :connect-label="connected ? 'dG6r…4mSr' : 'Connect'" @connect="connected = !connected" /> </div> </div></template><script lang="ts"> import { AppHeader } from "$lib/components/ui/app-header";
const items = [ { label: "Explore", href: "#explore" }, { label: "Feed", href: "#feed" }, { label: "Leaderboard", href: "#leaderboard" }, { label: "AI", href: "#ai" }, { label: "Create", href: "#create" }, ]; let connected = $state(false);
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><AppHeader name="Stockbreak" network="Devnet" elevation={l.value} {items} active="#leaderboard" connectLabel={connected ? "dG6r…4mSr" : "Connect"} onConnect={() => (connected = !connected)}/> </div> {/each}</div>| Part | Prop | Type | Default |
|---|---|---|---|
AppHeader |
logo / name / href |
as SiteHeader (name is not rendered when empty) |
none / none / "/" |
items |
{ label, href }[] |
[] |
|
active |
string (href of the active pill) |
none | |
search |
boolean |
true |
|
searchPlaceholder / shortcut |
string |
"Search" / "⌘K" |
|
network |
secondary button label, e.g. a network or workspace (React string, Vue string or network slot, Svelte string | Snippet); empty hides it |
none | |
connectLabel |
ReactNode (Vue connect-label slot, Svelte string | Snippet) |
"Connect" |
|
onConnect / onNetworkClick |
() => void (Vue emits connect / network-click) |
none | |
elevation |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
|
AppHeaderNavItem |
active, elevation |
exported for custom navs |
✦ Edmi additions
Section titled “✦ Edmi additions”- Nav pills stand alone (no track), active pill is
--tab-active+ 1px border (thepillslook); withelevation="raised"it becomes a raised secondary button.