Skip to content
edmi-ui

Elevation

ElevationProvider and useElevation: the v4 depth plumbing (sunken, flat, raised, floating) with a layered mode.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Flat (default)
Rebalance limits

field flat · button flat · surface flat · overlay flat

mode="layered"
Rebalance limits

field sunken · button raised · surface flat · overlay floating

level="raised" (forced)

field raised · button raised · surface flat · overlay raised

flat
shadow-raised
shadow-floating
shadow-sunken
Flat (default)
Rebalance limits

field flat · button flat · surface flat · overlay flat

mode="layered"
Rebalance limits

field sunken · button raised · surface flat · overlay floating

level="raised" (forced)

field raised · button raised · surface raised · overlay raised

flat
shadow-raised
shadow-floating
shadow-sunken
Flat (default)
Rebalance limits

field flat · button flat · surface flat · overlay flat

mode="layered"
Rebalance limits

field sunken · button raised · surface flat · overlay floating

level="raised" (forced)

field raised · button raised · surface raised · overlay raised

flat
shadow-raised
shadow-floating
shadow-sunken
Terminal window
npx shadcn@latest add @edmi-ui/elevation
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/elevation
Terminal window
yarn dlx shadcn@latest add @edmi-ui/elevation
Terminal window
bunx shadcn@latest add @edmi-ui/elevation

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/elevation
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/elevation
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/elevation
Terminal window
bunx shadcn-vue@latest add @edmi-ui/elevation

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/elevation.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/elevation.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/elevation.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/elevation.json

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

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=&quot;layered&quot;', 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>
registry/ui/elevation.tsx
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 };
registry/ui/elevation/ElevationProvider.vue
<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>
registry/ui/elevation/context.ts
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;
});
}
registry/ui/elevation/index.ts
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";
src/lib/registry/ui/elevation/elevation-provider.svelte
<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}
src/lib/registry/ui/elevation/context.ts
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;
},
};
}
src/lib/registry/ui/elevation/index.ts
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 };

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

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: ElevationProvider is a Reka Primitive (as, as-child); useElevation(() => props.elevation, role) returns a computed; surfaces call provideSurface(() => level.value).
  • Svelte: ElevationProvider takes a child snippet instead of as-child; useElevation(() => elevation, role) returns { current }; surfaces call setSurface(() => level).