Inset panel
Header and footer on a muted shell with a card body plate inset 2px from the shell. Optional bottom fade and elevation.
import { InsetPanel, InsetPanelBody, InsetPanelFooter, InsetPanelHeader,} from "@/components/ui/inset-panel";
export default function Demo() { return ( <div className="grid w-full max-w-2xl gap-5 sm:grid-cols-2"> <InsetPanel> <InsetPanelHeader>Live joiners</InsetPanelHeader> <InsetPanelBody fade className="h-40 p-4 text-sm"> Body runs edge to edge with a faded bottom. </InsetPanelBody> <InsetPanelFooter> 2,846 people joined an index this week </InsetPanelFooter> </InsetPanel> <InsetPanel> <InsetPanelHeader>Keeper activity</InsetPanelHeader> <InsetPanelBody className="h-40 p-4 text-sm"> No footer: the body runs to the bottom edge. </InsetPanelBody> </InsetPanel> </div> );}<script setup lang="ts">import { InsetPanel, InsetPanelBody, InsetPanelFooter, InsetPanelHeader,} from "@/components/ui/inset-panel";</script>
<template> <div class="grid w-full max-w-2xl gap-5 sm:grid-cols-2"> <InsetPanel> <InsetPanelHeader>Live joiners</InsetPanelHeader> <InsetPanelBody fade class="h-40 p-4 text-sm"> Body runs edge to edge with a faded bottom. </InsetPanelBody> <InsetPanelFooter>2,846 people joined an index this week</InsetPanelFooter> </InsetPanel> <InsetPanel> <InsetPanelHeader>Keeper activity</InsetPanelHeader> <InsetPanelBody class="h-40 p-4 text-sm"> No footer: the body runs to the bottom edge. </InsetPanelBody> </InsetPanel> </div></template><script lang="ts"> import * as InsetPanel from "$lib/components/ui/inset-panel";</script>
<div class="grid w-full max-w-2xl gap-5 sm:grid-cols-2"> <InsetPanel.Root> <InsetPanel.Header>Live joiners</InsetPanel.Header> <InsetPanel.Body fade class="h-40 p-4 text-sm"> Body runs edge to edge with a faded bottom. </InsetPanel.Body> <InsetPanel.Footer>2,846 people joined an index this week</InsetPanel.Footer> </InsetPanel.Root> <InsetPanel.Root> <InsetPanel.Header>Keeper activity</InsetPanel.Header> <InsetPanel.Body class="h-40 p-4 text-sm"> No footer: the body runs to the bottom edge. </InsetPanel.Body> </InsetPanel.Root></div>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/inset-panelpnpm dlx shadcn@latest add @edmi-ui/inset-panelyarn dlx shadcn@latest add @edmi-ui/inset-panelbunx shadcn@latest add @edmi-ui/inset-panelPick 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/inset-panelpnpm dlx shadcn-vue@latest add @edmi-ui/inset-panelyarn dlx shadcn-vue@latest add @edmi-ui/inset-panelbunx shadcn-vue@latest add @edmi-ui/inset-panelPick 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/inset-panel.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/inset-panel.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/inset-panel.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/inset-panel.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 { InsetPanel, InsetPanelBody, InsetPanelFooter, InsetPanelHeader,} from "@/components/ui/inset-panel";
<div className="grid w-full max-w-2xl gap-5 sm:grid-cols-2"> <InsetPanel> <InsetPanelHeader>Live joiners</InsetPanelHeader> <InsetPanelBody fade className="h-40 p-4 text-sm"> Body runs edge to edge with a faded bottom. </InsetPanelBody> <InsetPanelFooter> 2,846 people joined an index this week </InsetPanelFooter> </InsetPanel> <InsetPanel> <InsetPanelHeader>Keeper activity</InsetPanelHeader> <InsetPanelBody className="h-40 p-4 text-sm"> No footer: the body runs to the bottom edge. </InsetPanelBody> </InsetPanel></div><script setup lang="ts">import { InsetPanel, InsetPanelBody, InsetPanelFooter, InsetPanelHeader,} from "@/components/ui/inset-panel";</script>
<template> <div class="grid w-full max-w-2xl gap-5 sm:grid-cols-2"> <InsetPanel> <InsetPanelHeader>Live joiners</InsetPanelHeader> <InsetPanelBody fade class="h-40 p-4 text-sm"> Body runs edge to edge with a faded bottom. </InsetPanelBody> <InsetPanelFooter>2,846 people joined an index this week</InsetPanelFooter> </InsetPanel> <InsetPanel> <InsetPanelHeader>Keeper activity</InsetPanelHeader> <InsetPanelBody class="h-40 p-4 text-sm"> No footer: the body runs to the bottom edge. </InsetPanelBody> </InsetPanel> </div></template><script lang="ts"> import * as InsetPanel from "$lib/components/ui/inset-panel";</script>
<div class="grid w-full max-w-2xl gap-5 sm:grid-cols-2"> <InsetPanel.Root> <InsetPanel.Header>Live joiners</InsetPanel.Header> <InsetPanel.Body fade class="h-40 p-4 text-sm"> Body runs edge to edge with a faded bottom. </InsetPanel.Body> <InsetPanel.Footer>2,846 people joined an index this week</InsetPanel.Footer> </InsetPanel.Root> <InsetPanel.Root> <InsetPanel.Header>Keeper activity</InsetPanel.Header> <InsetPanel.Body class="h-40 p-4 text-sm"> No footer: the body runs to the bottom edge. </InsetPanel.Body> </InsetPanel.Root></div>Component source
Section titled “Component source”"use client";
import { cn } from "cn";import * as React from "react";
import { type Elevation, type ElevationLevel, SurfaceProvider, useElevation,} from "@/registry/edmi/ui/elevation";
// ✦ Edmi addition (DESIGN §4.8, v4): the header sits on the shell (--muted, level 0); the body is a --card// plate inset 2px from the shell (left/right/bottom) with its own full radius; with a footer the body keeps a// 0 bottom gap and the footer sits on the shell without a divider.// ✦ depth: raised = the body plate bevels; floating = the shell also gets the soft drop; sunken = the shell is a well.const insetPanelElevation = { sunken: { root: "border-sk-bd bg-sk-bg shadow-sunken", body: "" }, flat: { root: "", body: "" }, raised: { root: "", body: "border-transparent shadow-raised" }, floating: { root: "border-transparent shadow-[0_0_1.5px_var(--bv-out),var(--bv-float)]", body: "border-transparent shadow-raised", },};
const InsetPanelContext = React.createContext<ElevationLevel>("flat");
function InsetPanel({ className, elevation, children, ...props}: React.ComponentProps<"div"> & { /** ✦ depth: sunken -1, flat 0, raised +1 (body plate bevels), floating +2 (shell also drops). */ elevation?: Elevation;}) { const level = useElevation(elevation, "surface"); return ( <div data-slot="inset-panel" className={cn( "group/inset-panel flex flex-col overflow-hidden rounded-2xl border border-border bg-muted", insetPanelElevation[level].root, className, )} {...props} > <InsetPanelContext.Provider value={level}> <SurfaceProvider level={level}>{children}</SurfaceProvider> </InsetPanelContext.Provider> </div> );}
function InsetPanelHeader({ className, ...props}: React.ComponentProps<"div">) { return ( <div data-slot="inset-panel-header" className={cn( "flex items-center gap-2 px-4 py-3 text-sm font-medium", className, )} {...props} /> );}
function InsetPanelBody({ className, fade = false, ...props}: React.ComponentProps<"div"> & { fade?: boolean }) { const level = React.useContext(InsetPanelContext); return ( <div data-slot="inset-panel-body" data-fade={fade || undefined} className={cn( "relative mx-0.5 mb-0.5 flex-1 overflow-hidden rounded-xl border border-border bg-card", // footer: it sits right under the body, so no bottom gap "group-has-[[data-slot=inset-panel-footer]]/inset-panel:mb-0", insetPanelElevation[level].body, "data-[fade]:after:pointer-events-none data-[fade]:after:absolute data-[fade]:after:inset-x-0 data-[fade]:after:bottom-0 data-[fade]:after:h-14 data-[fade]:after:bg-linear-to-b data-[fade]:after:from-transparent data-[fade]:after:to-card", className, )} {...props} /> );}
function InsetPanelFooter({ className, ...props}: React.ComponentProps<"div">) { return ( <div data-slot="inset-panel-footer" className={cn( "bg-muted px-4 py-3 text-center text-[13px] text-foreground-2", className, )} {...props} /> );}
export { InsetPanel, InsetPanelBody, InsetPanelFooter, InsetPanelHeader };<script setup lang="ts">import type { HTMLAttributes } from 'vue'import { provide } from 'vue'import { cn } from '@/registry/edmi/lib/utils'import { type Elevation, provideSurface, useElevation } from '@/registry/edmi/ui/elevation'import { INSET_PANEL_KEY, insetPanelElevation } from './context'
const props = withDefaults(defineProps<{ class?: HTMLAttributes['class'] /** ✦ depth: sunken -1, flat 0, raised +1 (body plate bevels), floating +2 (shell also drops) */ elevation?: Elevation}>(), { elevation: undefined })
const level = useElevation(() => props.elevation, 'surface')provideSurface(() => level.value)provide(INSET_PANEL_KEY, { get value() { return level.value } })</script>
<template> <div data-slot="inset-panel" :class="cn('group/inset-panel flex flex-col overflow-hidden rounded-2xl border border-border bg-muted', insetPanelElevation[level].root, props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { inject } from 'vue'import { cn } from '@/registry/edmi/lib/utils'import { INSET_PANEL_KEY, insetPanelElevation } from './context'
const props = withDefaults(defineProps<{ class?: HTMLAttributes['class'] /** Bottom fade into the card colour. */ fade?: boolean}>(), { fade: false,})
const panel = inject(INSET_PANEL_KEY, undefined)</script>
<template> <div data-slot="inset-panel-body" :data-fade="fade || undefined" :class="cn( 'relative mx-0.5 mb-0.5 flex-1 overflow-hidden rounded-xl border border-border bg-card', // footer: it sits right under the body, so no bottom gap 'group-has-[[data-slot=inset-panel-footer]]/inset-panel:mb-0', insetPanelElevation[panel?.value ?? 'flat'].body, 'data-[fade]:after:pointer-events-none data-[fade]:after:absolute data-[fade]:after:inset-x-0 data-[fade]:after:bottom-0 data-[fade]:after:h-14 data-[fade]:after:bg-linear-to-b data-[fade]:after:from-transparent data-[fade]:after:to-card', props.class, )" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <div data-slot="inset-panel-footer" :class="cn('bg-muted px-4 py-3 text-center text-[13px] text-foreground-2', props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <div data-slot="inset-panel-header" :class="cn('flex items-center gap-2 px-4 py-3 text-sm font-medium', props.class)" > <slot /> </div></template>import type { InjectionKey } from "vue";import type { ElevationLevel } from "@/registry/edmi/ui/elevation";
// ✦ depth (v4): raised = the body plate bevels; floating = the shell also gets the soft drop; sunken = the shell is a well.export const insetPanelElevation = { sunken: { root: "border-sk-bd bg-sk-bg shadow-sunken", body: "" }, flat: { root: "", body: "" }, raised: { root: "", body: "border-transparent shadow-raised" }, floating: { root: "border-transparent shadow-[0_0_1.5px_var(--bv-out),var(--bv-float)]", body: "border-transparent shadow-raised", },};
// The panel hands its resolved level to the body (getter object so it stays reactive).export const INSET_PANEL_KEY: InjectionKey<{ readonly value: ElevationLevel }> = Symbol("EDMI_INSET_PANEL");export { INSET_PANEL_KEY, insetPanelElevation } from "./context";export { default as InsetPanel } from "./InsetPanel.vue";export { default as InsetPanelBody } from "./InsetPanelBody.vue";export { default as InsetPanelFooter } from "./InsetPanelFooter.vue";export { default as InsetPanelHeader } from "./InsetPanelHeader.vue";<script lang="ts"> // ✦ Edmi addition (DESIGN §4.8, v4): the header sits on the shell (--muted, level 0); the body is a // --card plate inset 2px from the shell (left/right/bottom) with its own full radius; with a footer the // body keeps a 0 bottom gap and the footer sits on the shell without a divider. // ✦ depth: raised = the body plate bevels; floating = the shell also drops; sunken = the shell is a well. import { cn, type WithElementRef } from "$lib/utils.js"; import { type Elevation, setSurface, useElevation } from "$lib/registry/ui/elevation/index.js"; import { insetPanelElevation, setInsetPanelLevel } from "./context.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, elevation = "auto", ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { /** ✦ depth: sunken -1, flat 0, raised +1 (body plate bevels), floating +2 (shell also drops). */ elevation?: Elevation; } = $props();
const level = useElevation(() => elevation, "surface"); setSurface(() => level.current); setInsetPanelLevel(() => level.current);</script>
<div bind:this={ref} data-slot="inset-panel" class={cn( "group/inset-panel flex flex-col overflow-hidden rounded-2xl border border-border bg-muted", insetPanelElevation[level.current].root, className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="inset-panel-header" class={cn( "flex items-center gap-2 px-4 py-3 text-sm font-medium", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import { getInsetPanelLevel, insetPanelElevation } from "./context.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, fade = false, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { fade?: boolean } = $props();
const panel = getInsetPanelLevel();</script>
<div bind:this={ref} data-slot="inset-panel-body" data-fade={fade || undefined} class={cn( "relative mx-0.5 mb-0.5 flex-1 overflow-hidden rounded-xl border border-border bg-card", // footer: it sits right under the body, so no bottom gap "group-has-[[data-slot=inset-panel-footer]]/inset-panel:mb-0", insetPanelElevation[panel.current].body, "data-[fade]:after:pointer-events-none data-[fade]:after:absolute data-[fade]:after:inset-x-0 data-[fade]:after:bottom-0 data-[fade]:after:h-14 data-[fade]:after:bg-linear-to-b data-[fade]:after:from-transparent data-[fade]:after:to-card", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="inset-panel-footer" class={cn( "bg-muted px-4 py-3 text-center text-[13px] text-foreground-2", className )} {...restProps}> {@render children?.()}</div>import { getContext, setContext } from "svelte";import type { ElevationLevel } from "$lib/registry/ui/elevation/index.js";
// ✦ depth (v4): raised = the body plate bevels; floating = the shell also gets the soft drop; sunken = the shell is a well.export const insetPanelElevation = { sunken: { root: "border-sk-bd bg-sk-bg shadow-sunken", body: "" }, flat: { root: "", body: "" }, raised: { root: "", body: "border-transparent shadow-raised" }, floating: { root: "border-transparent shadow-[0_0_1.5px_var(--bv-out),var(--bv-float)]", body: "border-transparent shadow-raised", },};
const INSET_PANEL_CONTEXT = Symbol("EDMI_INSET_PANEL");
// The panel hands its resolved level to the body (getter, reactive).export function setInsetPanelLevel(getLevel: () => ElevationLevel) { setContext(INSET_PANEL_CONTEXT, getLevel);}
export function getInsetPanelLevel(): { readonly current: ElevationLevel } { const get = getContext<(() => ElevationLevel) | undefined>( INSET_PANEL_CONTEXT, ); return { get current() { return get?.() ?? "flat"; }, };}import Root from "./inset-panel.svelte";import Body from "./inset-panel-body.svelte";import Footer from "./inset-panel-footer.svelte";import Header from "./inset-panel-header.svelte";
export { Body, Body as InsetPanelBody, Footer, Footer as InsetPanelFooter, Header, Header as InsetPanelHeader, Root, // Root as InsetPanel,};Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
Flat by default. raised (+1) bevels the body plate, floating (+2) also gives the shell the soft drop and sunken (-1) turns the shell into a well. The shell itself is level 0 and never rises on its own.
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
import { InsetPanel, InsetPanelBody, InsetPanelFooter, InsetPanelHeader,} from "@/components/ui/inset-panel";
const levels = [ { value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" },] as const;
export default function Demo() { return ( <div className="grid w-full max-w-3xl gap-5 sm:grid-cols-2"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <InsetPanel elevation={value}> <InsetPanelHeader>Live joiners</InsetPanelHeader> <InsetPanelBody fade className="h-32 p-4 text-sm"> The body plate is inset 2px from the shell. </InsetPanelBody> <InsetPanelFooter>2,846 people joined this week</InsetPanelFooter> </InsetPanel> </div> ))} </div> );}<script setup lang="ts">import { InsetPanel, InsetPanelBody, InsetPanelFooter, InsetPanelHeader } from "@/components/ui/inset-panel";
const levels = [{ value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }] as const;</script>
<template> <div class="grid w-full max-w-3xl gap-5 sm:grid-cols-2"> <div v-for="level in levels" :key="level.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ level.label }}</p> <InsetPanel :elevation="level.value"> <InsetPanelHeader>Live joiners</InsetPanelHeader> <InsetPanelBody fade class="h-32 p-4 text-sm">The body plate is inset 2px from the shell.</InsetPanelBody> <InsetPanelFooter>2,846 people joined this week</InsetPanelFooter> </InsetPanel> </div> </div></template><script lang="ts"> import * as InsetPanel from "$lib/components/ui/inset-panel";
const levels = [{ value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }] as const;</script>
<div class="grid w-full max-w-3xl gap-5 sm:grid-cols-2"> {#each levels as level (level.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{level.label}</p> <InsetPanel.Root elevation={level.value}> <InsetPanel.Header>Live joiners</InsetPanel.Header> <InsetPanel.Body fade class="h-32 p-4 text-sm">The body plate is inset 2px from the shell.</InsetPanel.Body> <InsetPanel.Footer>2,846 people joined this week</InsetPanel.Footer> </InsetPanel.Root> </div> {/each}</div>| Prop | Type | Default |
|---|---|---|
InsetPanelBody fade |
boolean |
false |
elevation |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
Parts: InsetPanel, InsetPanelHeader, InsetPanelBody, InsetPanelFooter.
✦ Edmi additions
Section titled “✦ Edmi additions”- Whole component is an Edmi addition (DESIGN §4.8): the body is a
--cardplate inset 2px from the shell (left, right, bottom) with its own full radius; with a footer the body keeps a 0 bottom gap and the footer sits on the shell without a divider.