Elevation
ElevationProvider and useElevation: the v4 depth plumbing (sunken, flat, raised, floating) with a layered mode.
field raised · button raised · surface flat · overlay raised
field raised · button raised · surface raised · overlay raised
field raised · button raised · surface raised · overlay raised
import { Button } from "@/components/ui/button";import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";import { ElevationProvider, SurfaceProvider, useElevation,} from "@/components/ui/elevation";import { Input } from "@/components/ui/input";import { Popover, PopoverContent, PopoverTrigger,} from "@/components/ui/popover";
/* What each role resolves to inside the scope (the same call every component makes). */function Resolved() { const field = useElevation(undefined, "field"); const button = useElevation(undefined, "button-filled"); const surface = useElevation(undefined, "surface"); const overlay = useElevation(undefined, "overlay"); return ( <p className="font-mono text-[11.5px] text-muted-foreground"> field {field} · button {button} · surface {surface} · overlay {overlay} </p> );}
function Scope({ title, children,}: { title: string; children: React.ReactNode;}) { return ( <div className="flex flex-col gap-3"> <div className="text-xs font-semibold text-muted-foreground">{title}</div> {children} </div> );}
function Sample() { return ( <Card className="w-72"> <CardHeader> <CardTitle>Rebalance limits</CardTitle> </CardHeader> <CardContent className="flex flex-col gap-3"> <Input defaultValue="5%" /> <div className="flex gap-2"> <Button>Save</Button> <Popover> <PopoverTrigger render={<Button variant="outline" />}> More </PopoverTrigger> <PopoverContent className="w-48 text-[13px]"> Applied to the next keeper run. </PopoverContent> </Popover> </div> <Resolved /> </CardContent> </Card> );}
/* Token check: the v4 shadow utilities and bevel / sunken variables, straight from the theme map. */function Swatches() { return ( <div className="flex flex-wrap items-center gap-4"> <div className="flex h-12 w-24 items-center justify-center rounded-lg border border-border bg-card text-xs"> flat </div> <div className="flex h-12 w-24 items-center justify-center rounded-lg border border-transparent bg-card text-xs shadow-raised"> shadow-raised </div> <div className="flex h-12 w-24 items-center justify-center rounded-lg border border-transparent bg-card text-xs shadow-floating"> shadow-floating </div> <div className="flex h-12 w-24 items-center justify-center rounded-lg border border-sk-bd bg-sk-bg text-xs shadow-sunken"> shadow-sunken </div> </div> );}
export default function Demo() { return ( <div className="flex flex-col gap-6"> <div className="flex flex-wrap gap-8"> <Scope title="Flat (default)"> <ElevationProvider> <Sample /> </ElevationProvider> </Scope> <Scope title='mode="layered"'> <ElevationProvider mode="layered"> <Sample /> </ElevationProvider> </Scope> <Scope title='level="raised" (forced)'> <ElevationProvider level="raised"> <SurfaceProvider level="raised"> <Resolved /> </SurfaceProvider> </ElevationProvider> </Scope> </div> <Swatches /> </div> );}<script setup lang="ts">import { Button } from "@/components/ui/button";import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";import { ElevationProvider, useElevation } from "@/components/ui/elevation";import { Input } from "@/components/ui/input";import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";import { defineComponent, h } from "vue";
// What each role resolves to inside the scope (the same call every component makes).const Resolved = defineComponent(() => { const field = useElevation(() => undefined, "field"); const button = useElevation(() => undefined, "button-filled"); const surface = useElevation(() => undefined, "surface"); const overlay = useElevation(() => undefined, "overlay"); return () => h( "p", { class: "font-mono text-[11.5px] text-muted-foreground" }, `field ${field.value} · button ${button.value} · surface ${surface.value} · overlay ${overlay.value}`, );});</script>
<template> <div class="flex flex-col gap-6"> <div class="flex flex-wrap gap-8"> <div v-for="scope in [{ title: 'Flat (default)', mode: undefined }, { title: 'mode="layered"', mode: 'layered' as const }]" :key="scope.title" class="flex flex-col gap-3"> <div class="text-xs font-semibold text-muted-foreground">{{ scope.title }}</div> <ElevationProvider :mode="scope.mode"> <Card class="w-72"> <CardHeader> <CardTitle>Rebalance limits</CardTitle> </CardHeader> <CardContent class="flex flex-col gap-3"> <Input default-value="5%" /> <div class="flex gap-2"> <Button>Save</Button> <Popover> <PopoverTrigger as-child> <Button variant="outline">More</Button> </PopoverTrigger> <PopoverContent class="w-48 text-[13px]">Applied to the next keeper run.</PopoverContent> </Popover> </div> <Resolved /> </CardContent> </Card> </ElevationProvider> </div> <div class="flex flex-col gap-3"> <div class="text-xs font-semibold text-muted-foreground">level="raised" (forced)</div> <ElevationProvider level="raised"> <Resolved /> </ElevationProvider> </div> </div> <div class="flex flex-wrap items-center gap-4"> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-border bg-card text-xs">flat</div> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-transparent bg-card text-xs shadow-raised">shadow-raised</div> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-transparent bg-card text-xs shadow-floating">shadow-floating</div> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-sk-bd bg-sk-bg text-xs shadow-sunken">shadow-sunken</div> </div> </div></template><script lang="ts"> import { Button } from "$lib/components/ui/button"; import * as Card from "$lib/components/ui/card"; import { ElevationProvider } from "$lib/components/ui/elevation"; import { Input } from "$lib/components/ui/input"; import * as Popover from "$lib/components/ui/popover"; import Resolved from "./_shared/elevation-resolved.svelte";</script>
{#snippet sample()} <Card.Root class="w-72"> <Card.Header> <Card.Title>Rebalance limits</Card.Title> </Card.Header> <Card.Content class="flex flex-col gap-3"> <Input value="5%" /> <div class="flex gap-2"> <Button>Save</Button> <Popover.Root> <Popover.Trigger> {#snippet child({ props })} <Button variant="outline" {...props}>More</Button> {/snippet} </Popover.Trigger> <Popover.Content class="w-48 text-[13px]">Applied to the next keeper run.</Popover.Content> </Popover.Root> </div> <Resolved /> </Card.Content> </Card.Root>{/snippet}
<div class="flex flex-col gap-6"> <div class="flex flex-wrap gap-8"> <div class="flex flex-col gap-3"> <div class="text-xs font-semibold text-muted-foreground">Flat (default)</div> <ElevationProvider>{@render sample()}</ElevationProvider> </div> <div class="flex flex-col gap-3"> <div class="text-xs font-semibold text-muted-foreground">mode="layered"</div> <ElevationProvider mode="layered">{@render sample()}</ElevationProvider> </div> <div class="flex flex-col gap-3"> <div class="text-xs font-semibold text-muted-foreground">level="raised" (forced)</div> <ElevationProvider level="raised"><Resolved /></ElevationProvider> </div> </div> <div class="flex flex-wrap items-center gap-4"> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-border bg-card text-xs">flat</div> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-transparent bg-card text-xs shadow-raised">shadow-raised</div> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-transparent bg-card text-xs shadow-floating">shadow-floating</div> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-sk-bd bg-sk-bg text-xs shadow-sunken">shadow-sunken</div> </div></div>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/elevationpnpm dlx shadcn@latest add @edmi-ui/elevationyarn dlx shadcn@latest add @edmi-ui/elevationbunx shadcn@latest add @edmi-ui/elevationPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @base-ui/reactTerminal window pnpm add @base-ui/reactTerminal window yarn add @base-ui/reactTerminal window bun add @base-ui/reactCopy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/elevationpnpm dlx shadcn-vue@latest add @edmi-ui/elevationyarn dlx shadcn-vue@latest add @edmi-ui/elevationbunx shadcn-vue@latest add @edmi-ui/elevationPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install reka-uiTerminal window pnpm add reka-uiTerminal window yarn add reka-uiTerminal window bun add reka-uiCopy 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/elevation.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/elevation.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/elevation.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/elevation.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Copy the code from Component source into your project and update the import paths to match your aliases.
import { ElevationProvider, SurfaceProvider, useElevation,} from "@/components/ui/elevation";
<p className="font-mono text-[11.5px] text-muted-foreground"> field · button · surface · overlay</p><script setup lang="ts">import { ElevationProvider } from "@/components/ui/elevation";</script>
<template> <div class="flex flex-col gap-6"> <div class="flex flex-wrap gap-8"> <div v-for="scope in [{ title: 'Flat (default)', mode: undefined }, { title: 'mode="layered"', mode: 'layered' as const }]" :key="scope.title" class="flex flex-col gap-3"> <div class="text-xs font-semibold text-muted-foreground">{{ scope.title }}</div> <!-- … --> </div> <div class="flex flex-col gap-3"> <div class="text-xs font-semibold text-muted-foreground">level="raised" (forced)</div> <ElevationProvider level="raised"> <Resolved /> </ElevationProvider> </div> </div> <div class="flex flex-wrap items-center gap-4"> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-border bg-card text-xs">flat</div> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-transparent bg-card text-xs shadow-raised">shadow-raised</div> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-transparent bg-card text-xs shadow-floating">shadow-floating</div> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-sk-bd bg-sk-bg text-xs shadow-sunken">shadow-sunken</div> </div> </div></template><script lang="ts"> import { ElevationProvider } from "$lib/components/ui/elevation";</script>
<div class="flex flex-col gap-6"> <div class="flex flex-wrap gap-8"> <div class="flex flex-col gap-3"> <div class="text-xs font-semibold text-muted-foreground">Flat (default)</div> <ElevationProvider></ElevationProvider> </div> <div class="flex flex-col gap-3"> <div class="text-xs font-semibold text-muted-foreground">mode="layered"</div> <ElevationProvider mode="layered"></ElevationProvider> </div> <div class="flex flex-col gap-3"> <div class="text-xs font-semibold text-muted-foreground">level="raised" (forced)</div> <ElevationProvider level="raised"><Resolved /></ElevationProvider> </div> </div> <div class="flex flex-wrap items-center gap-4"> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-border bg-card text-xs">flat</div> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-transparent bg-card text-xs shadow-raised">shadow-raised</div> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-transparent bg-card text-xs shadow-floating">shadow-floating</div> <div class="flex h-12 w-24 items-center justify-center rounded-lg border border-sk-bd bg-sk-bg text-xs shadow-sunken">shadow-sunken</div> </div></div>Component source
Section titled “Component source”import { mergeProps } from "@base-ui/react/merge-props";import { useRender } from "@base-ui/react/use-render";import * as React from "react";
/* Elevation plumbing (v4). Components take `elevation?: Elevation` (default "auto") and resolve it with `useElevation(prop, role)`; an <ElevationProvider> scope switches a whole subtree to the layered mode. */
export type Elevation = "auto" | "sunken" | "flat" | "raised" | "floating";export type ElevationLevel = Exclude<Elevation, "auto">;export type ElevationMode = "flat" | "layered";
/* Role defaults in layered mode (copied from the v4 recipes). In flat mode everything is "flat". */export const ROLE_LEVEL = { "button-filled": "raised", "button-quiet": "flat", field: "sunken", control: "flat", handle: "raised", surface: "raised", container: "flat", overlay: "floating",} as const;export type ElevationRole = keyof typeof ROLE_LEVEL;
/** prop -> scope (forced level, or mode) -> flat. An explicit prop always wins. */export function resolveElevation( prop: Elevation | undefined, scope: Elevation | ElevationMode | undefined, role: ElevationRole,): ElevationLevel { if (prop && prop !== "auto") return prop; if (scope && scope !== "auto" && scope !== "flat" && scope !== "layered") return scope; return scope === "layered" ? ROLE_LEVEL[role] : "flat";}
/** Surface class map shared by patterns and plates (sunken well / flat / bevel / bevel + drop). */export const surfaceElevation: Record<ElevationLevel, string> = { sunken: "border-sk-bd bg-sk-bg shadow-sunken", flat: "", raised: "border-transparent shadow-raised", floating: "border-transparent shadow-floating",};
/** Depth a container hands to the controls it renders: explicit raised/floating -> "raised", * explicit flat/sunken -> "flat", auto/undefined -> undefined (controls follow their own roles). */export function controlLevel( elevation: Elevation | undefined,): "raised" | "flat" | undefined { if (!elevation || elevation === "auto") return undefined; return elevation === "raised" || elevation === "floating" ? "raised" : "flat";}
const ScopeContext = React.createContext<ElevationLevel | ElevationMode>( "flat",);const SurfaceContext = React.createContext<ElevationLevel | undefined>( undefined,);
/** Resolved level for a component of the given role: prop -> nearest provider -> flat. * A `surface` with `auto` inside a raised/floating surface drops to flat (no bevel on bevel); * an explicit prop still wins, controls and fields ignore nesting. */function useElevation( prop: Elevation | undefined, role: ElevationRole,): ElevationLevel { const scope = React.useContext(ScopeContext); const parent = React.useContext(SurfaceContext); const level = resolveElevation(prop, scope, role); if ( role === "surface" && (!prop || prop === "auto") && (parent === "raised" || parent === "floating") ) return "flat"; return level;}
/** Surfaces (card, panel body, popover, dialog) wrap their children so nested surfaces can drop to flat. */function SurfaceProvider({ level, children,}: { level: ElevationLevel; children?: React.ReactNode;}) { return ( <SurfaceContext.Provider value={level}>{children}</SurfaceContext.Provider> );}
type ElevationProviderProps = useRender.ComponentProps<"div"> & { /** "layered": every role takes its default level; "flat" (default). Inherits the parent scope when omitted. */ mode?: ElevationMode; /** Force one level for the whole subtree (wins over `mode`). */ level?: ElevationLevel;};
/** Sets the elevation scope for a subtree and renders `data-elevation`. Layout-neutral (`display: contents`) * unless you pass a `className`. */function ElevationProvider({ mode, level, className, style, render, ...props}: ElevationProviderProps) { const parent = React.useContext(ScopeContext); const scope = level ?? mode ?? parent; const element = useRender({ defaultTagName: "div", props: mergeProps<"div">( { className, style: className ? style : { display: "contents", ...style }, }, props, ), render, state: { slot: "elevation-provider", elevation: scope }, }); return <ScopeContext.Provider value={scope}>{element}</ScopeContext.Provider>;}
export { ElevationProvider, SurfaceProvider, useElevation };<script setup lang="ts">import type { PrimitiveProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { computed, useAttrs } from 'vue'import { Primitive } from 'reka-ui'import { type ElevationLevel, type ElevationMode, injectElevationScope, provideElevationScope,} from './context'
const props = defineProps< PrimitiveProps & { class?: HTMLAttributes['class'] /** "layered": every role takes its default level; "flat" (default). Inherits the parent scope when omitted. */ mode?: ElevationMode /** Force one level for the whole subtree (wins over `mode`). */ level?: ElevationLevel }>()
const parent = injectElevationScope()const scope = computed(() => props.level ?? props.mode ?? parent)provideElevationScope(() => scope.value)
// Layout-neutral (display: contents) unless a class is passed.const attrs = useAttrs()const style = computed(() => (props.class || attrs.style ? undefined : { display: 'contents' }))</script>
<template> <Primitive data-slot="elevation-provider" :data-elevation="scope" :as="as" :as-child="asChild" :class="props.class" :style="style" > <slot /> </Primitive></template>import { type ComputedRef, computed, inject, provide } from "vue";
// Elevation plumbing (v4). Components take `elevation?: Elevation` (default "auto" = undefined) and resolve it// with `useElevation(() => props.elevation, role)`; an <ElevationProvider> scope switches a subtree to layered.
export type Elevation = "auto" | "sunken" | "flat" | "raised" | "floating";export type ElevationLevel = Exclude<Elevation, "auto">;export type ElevationMode = "flat" | "layered";
// Role defaults in layered mode (copied from the v4 recipes). In flat mode everything is "flat".export const ROLE_LEVEL = { "button-filled": "raised", "button-quiet": "flat", field: "sunken", control: "flat", handle: "raised", surface: "raised", container: "flat", overlay: "floating",} as const;export type ElevationRole = keyof typeof ROLE_LEVEL;
// prop -> scope (forced level, or mode) -> flat. An explicit prop always wins.export function resolveElevation( prop: Elevation | undefined, scope: Elevation | ElevationMode | undefined, role: ElevationRole,): ElevationLevel { if (prop && prop !== "auto") return prop; if (scope && scope !== "auto" && scope !== "flat" && scope !== "layered") return scope; return scope === "layered" ? ROLE_LEVEL[role] : "flat";}
/** Surface class map shared by patterns and plates (sunken well / flat / bevel / bevel + drop). */export const surfaceElevation: Record<ElevationLevel, string> = { sunken: "border-sk-bd bg-sk-bg shadow-sunken", flat: "", raised: "border-transparent shadow-raised", floating: "border-transparent shadow-floating",};
/** Depth a container hands to the controls it renders: explicit raised/floating -> "raised", * explicit flat/sunken -> "flat", auto/undefined -> undefined (controls follow their own roles). */export function controlLevel( elevation: Elevation | undefined,): "raised" | "flat" | undefined { if (!elevation || elevation === "auto") return undefined; return elevation === "raised" || elevation === "floating" ? "raised" : "flat";}
const SCOPE_KEY = Symbol("EDMI_ELEVATION_SCOPE");const SURFACE_KEY = Symbol("EDMI_ELEVATION_SURFACE");
type Getter<T> = { readonly value: T };
export function provideElevationScope( getScope: () => ElevationLevel | ElevationMode,) { provide<Getter<ElevationLevel | ElevationMode>>(SCOPE_KEY, { get value() { return getScope(); }, });}
export function injectElevationScope(): ElevationLevel | ElevationMode { return ( inject<Getter<ElevationLevel | ElevationMode> | undefined>( SCOPE_KEY, undefined, )?.value ?? "flat" );}
// Surfaces (card, panel body, popover, dialog) call this with their resolved level so nested surfaces drop to flat.export function provideSurface(getLevel: () => ElevationLevel) { provide<Getter<ElevationLevel>>(SURFACE_KEY, { get value() { return getLevel(); }, });}
// Resolved level for a component of the given role: prop -> nearest provider -> flat.// A `surface` with `auto` inside a raised/floating surface drops to flat (no bevel on bevel);// an explicit prop still wins, controls and fields ignore nesting.export function useElevation( getProp: () => Elevation | undefined, role: ElevationRole,): ComputedRef<ElevationLevel> { const scope = inject<Getter<ElevationLevel | ElevationMode> | undefined>( SCOPE_KEY, undefined, ); const parent = inject<Getter<ElevationLevel> | undefined>( SURFACE_KEY, undefined, ); return computed(() => { const prop = getProp(); const level = resolveElevation(prop, scope?.value, role); if ( role === "surface" && (!prop || prop === "auto") && (parent?.value === "raised" || parent?.value === "floating") ) return "flat"; return level; });}export { controlLevel, type Elevation, type ElevationLevel, type ElevationMode, type ElevationRole, injectElevationScope, provideElevationScope, provideSurface, ROLE_LEVEL, resolveElevation, surfaceElevation, useElevation,} from "./context";export { default as ElevationProvider } from "./ElevationProvider.vue";<script lang="ts"> import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements"; import type { WithElementRef } from "$lib/utils.js"; import { type ElevationLevel, type ElevationMode, getElevationScope, setElevationScope, } from "./context.js";
let { ref = $bindable(null), class: className, style, mode, level, child, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { /** "layered": every role takes its default level; "flat" (default). Inherits the parent scope when omitted. */ mode?: ElevationMode; /** Force one level for the whole subtree (wins over `mode`). */ level?: ElevationLevel; /** Render your own element: `{#snippet child({ props })}<section {...props}>…</section>{/snippet}` */ child?: Snippet<[{ props: Record<string, unknown> }]>; } = $props();
const parent = getElevationScope(); const scope = $derived(level ?? mode ?? parent.current); setElevationScope(() => scope);
// Layout-neutral (display: contents) unless a class or style is passed. const mergedProps = $derived({ ...restProps, "data-slot": "elevation-provider", "data-elevation": scope, class: className, style: className || style ? style : "display: contents", });</script>
{#if child} {@render child({ props: mergedProps })}{:else} <div bind:this={ref} {...mergedProps}> {@render children?.()} </div>{/if}import { getContext, setContext } from "svelte";
// Elevation plumbing (v4). Components take `elevation = "auto"` and resolve it with// `useElevation(() => elevation, role)`; an <ElevationProvider> scope switches a subtree to layered.
export type Elevation = "auto" | "sunken" | "flat" | "raised" | "floating";export type ElevationLevel = Exclude<Elevation, "auto">;export type ElevationMode = "flat" | "layered";
// Role defaults in layered mode (copied from the v4 recipes). In flat mode everything is "flat".export const ROLE_LEVEL = { "button-filled": "raised", "button-quiet": "flat", field: "sunken", control: "flat", handle: "raised", surface: "raised", container: "flat", overlay: "floating",} as const;export type ElevationRole = keyof typeof ROLE_LEVEL;
// prop -> scope (forced level, or mode) -> flat. An explicit prop always wins.export function resolveElevation( prop: Elevation | undefined, scope: Elevation | ElevationMode | undefined, role: ElevationRole,): ElevationLevel { if (prop && prop !== "auto") return prop; if (scope && scope !== "auto" && scope !== "flat" && scope !== "layered") return scope; return scope === "layered" ? ROLE_LEVEL[role] : "flat";}
/** Surface class map shared by patterns and plates (sunken well / flat / bevel / bevel + drop). */export const surfaceElevation: Record<ElevationLevel, string> = { sunken: "border-sk-bd bg-sk-bg shadow-sunken", flat: "", raised: "border-transparent shadow-raised", floating: "border-transparent shadow-floating",};
/** Depth a container hands to the controls it renders: explicit raised/floating -> "raised", * explicit flat/sunken -> "flat", auto/undefined -> undefined (controls follow their own roles). */export function controlLevel( elevation: Elevation | undefined,): "raised" | "flat" | undefined { if (!elevation || elevation === "auto") return undefined; return elevation === "raised" || elevation === "floating" ? "raised" : "flat";}
const SCOPE_CONTEXT = Symbol("EDMI_ELEVATION_SCOPE");const SURFACE_CONTEXT = Symbol("EDMI_ELEVATION_SURFACE");
type Scope = ElevationLevel | ElevationMode;
export function setElevationScope(getScope: () => Scope) { setContext(SCOPE_CONTEXT, getScope);}
// Current scope of the nearest <ElevationProvider> ("flat" without one). `current` is a getter: reactive.export function getElevationScope(): { readonly current: Scope } { const get = getContext<(() => Scope) | undefined>(SCOPE_CONTEXT); return { get current() { return get?.() ?? "flat"; }, };}
// Surfaces (card, panel body, popover, dialog) call this with their resolved level so nested surfaces drop to flat.export function setSurface(getLevel: () => ElevationLevel) { setContext(SURFACE_CONTEXT, getLevel);}
// Resolved level for a component of the given role: prop -> nearest provider -> flat.// A `surface` with `auto` inside a raised/floating surface drops to flat (no bevel on bevel);// an explicit prop still wins, controls and fields ignore nesting.// Call during component init; read `.current` inside `$derived` / the template to stay reactive.export function useElevation( getProp: () => Elevation | undefined, role: ElevationRole,): { readonly current: ElevationLevel } { const scope = getContext<(() => Scope) | undefined>(SCOPE_CONTEXT); const parent = getContext<(() => ElevationLevel) | undefined>( SURFACE_CONTEXT, ); return { get current() { const prop = getProp(); const level = resolveElevation(prop, scope?.(), role); const p = parent?.(); if ( role === "surface" && (!prop || prop === "auto") && (p === "raised" || p === "floating") ) return "flat"; return level; }, };}import ElevationProvider from "./elevation-provider.svelte";
export { controlLevel, type Elevation, type ElevationLevel, type ElevationMode, type ElevationRole, getElevationScope, ROLE_LEVEL, resolveElevation, setElevationScope, setSurface, surfaceElevation, useElevation,} from "./context.js";export { ElevationProvider };How it works
Section titled “How it works”Read the Elevation guide first for the philosophy, the role table and when to use layered mode.
Every Edmi component is flat by default (the plain shadcn look). Components that can take depth accept one prop,
elevation?: "auto" | "sunken" | "flat" | "raised" | "floating" (default "auto"), and resolve it in this order:
the explicit prop, then the nearest ElevationProvider, then flat.
| Level | Look |
|---|---|
sunken (-1) |
soft inset well |
flat (0) |
fill and 1px border |
raised (+1) |
bevel: rim, top highlight, hairline |
floating (+2) |
bevel and one soft drop |
In mode="layered" each role takes its own level: filled buttons raised, fields sunken, cards raised, popovers and dialogs
floating, everything else flat. A surface inside a raised or floating surface drops back to flat (no bevel on bevel);
buttons and fields ignore nesting. An explicit elevation prop always wins.
| Export | Notes |
|---|---|
ElevationProvider mode |
"flat" | "layered". Renders an element with data-elevation; layout-neutral (display: contents) unless you pass a class. Inherits the parent scope when omitted |
ElevationProvider level |
"sunken" | "flat" | "raised" | "floating". Forces one level for the whole subtree (wins over mode) |
useElevation(prop, role) |
Resolves the level for a component (React returns the level, Vue a ComputedRef, Svelte { current }) |
SurfaceProvider level |
React. Surfaces wrap their children with their resolved level so nested surfaces drop to flat (Vue provideSurface, Svelte setSurface) |
resolveElevation(prop, scope, role) |
Pure resolver |
surfaceElevation |
Surface class map keyed by level (sunken, flat, raised, floating); patterns use it for their plate instead of a local copy |
controlLevel(elevation) |
Depth a container hands to the controls it renders: explicit raised/floating gives "raised", explicit flat/sunken gives "flat", auto or unset gives undefined |
ROLE_LEVEL |
Role to layered level: button-filled, button-quiet, field, control, handle, surface, container, overlay |
| types | Elevation, ElevationLevel, ElevationMode, ElevationRole |
✦ Edmi additions
Section titled “✦ Edmi additions”The whole item is an Edmi addition; there is no stock counterpart. It is installed automatically as a dependency of every component that takes elevation.
Vue / Svelte differences
Section titled “Vue / Svelte differences”- Vue:
ElevationProvideris a RekaPrimitive(as,as-child);useElevation(() => props.elevation, role)returns a computed; surfaces callprovideSurface(() => level.value). - Svelte:
ElevationProvidertakes achildsnippet instead ofas-child;useElevation(() => elevation, role)returns{ current }; surfaces callsetSurface(() => level).