Skip to content
edmi-ui

Inset panel

Header and footer on a muted shell with a card body plate inset 2px from the shell. Optional bottom fade and elevation.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Live joiners
Body runs edge to edge with a faded bottom.
2,846 people joined an index this week
Keeper activity
No footer: the body runs to the bottom edge.
Live joiners
Body runs edge to edge with a faded bottom.
2,846 people joined an index this week
Keeper activity
No footer: the body runs to the bottom edge.
Live joiners
Body runs edge to edge with a faded bottom.
2,846 people joined an index this week
Keeper activity
No footer: the body runs to the bottom edge.
Terminal window
npx shadcn@latest add @edmi-ui/inset-panel
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/inset-panel
Terminal window
yarn dlx shadcn@latest add @edmi-ui/inset-panel
Terminal window
bunx shadcn@latest add @edmi-ui/inset-panel

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

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

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

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>
registry/ui/inset-panel.tsx
"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 };
registry/ui/inset-panel/InsetPanel.vue
<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>
registry/ui/inset-panel/InsetPanelBody.vue
<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>
registry/ui/inset-panel/InsetPanelFooter.vue
<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>
registry/ui/inset-panel/InsetPanelHeader.vue
<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>
registry/ui/inset-panel/context.ts
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");
registry/ui/inset-panel/index.ts
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";
src/lib/registry/ui/inset-panel/inset-panel.svelte
<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>
src/lib/registry/ui/inset-panel/inset-panel-header.svelte
<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>
src/lib/registry/ui/inset-panel/inset-panel-body.svelte
<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>
src/lib/registry/ui/inset-panel/inset-panel-footer.svelte
<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>
src/lib/registry/ui/inset-panel/context.ts
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";
},
};
}
src/lib/registry/ui/inset-panel/index.ts
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,
};

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.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Sunken (-1)

Live joiners
The body plate is inset 2px from the shell.
2,846 people joined this week

Flat (0)

Live joiners
The body plate is inset 2px from the shell.
2,846 people joined this week

Raised (+1)

Live joiners
The body plate is inset 2px from the shell.
2,846 people joined this week

Floating (+2)

Live joiners
The body plate is inset 2px from the shell.
2,846 people joined this week

Sunken (-1)

Live joiners
The body plate is inset 2px from the shell.
2,846 people joined this week

Flat (0)

Live joiners
The body plate is inset 2px from the shell.
2,846 people joined this week

Raised (+1)

Live joiners
The body plate is inset 2px from the shell.
2,846 people joined this week

Floating (+2)

Live joiners
The body plate is inset 2px from the shell.
2,846 people joined this week

Sunken (-1)

Live joiners
The body plate is inset 2px from the shell.
2,846 people joined this week

Flat (0)

Live joiners
The body plate is inset 2px from the shell.
2,846 people joined this week

Raised (+1)

Live joiners
The body plate is inset 2px from the shell.
2,846 people joined this week

Floating (+2)

Live joiners
The body plate is inset 2px from the shell.
2,846 people joined this week
Prop Type Default
InsetPanelBody fade boolean false
elevation "auto" | "sunken" | "flat" | "raised" | "floating" "auto"

Parts: InsetPanel, InsetPanelHeader, InsetPanelBody, InsetPanelFooter.

  • Whole component is an Edmi addition (DESIGN §4.8): 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.