← ui

panel

ui

the one container, plain or glass: a title or a head, a foot, and a body that scrolls

npx shadcn add @ag/panel
Already installed? Preview an update
npx shadcn add @ag/panel --dry-run

Your installed source stays unchanged until you update it. How updates work

Latest changes · 2026-10-15
  • A panel inside a frame already drawn (a panel's body, or InFrame) draws the inset surface, whatever its material: a step of light off what is under it, a hairline and the theme's corner, no glass of its own. A panel that is a link brightens a step under the hand, alone or inset. Every box that stands in another is now drawn the same, in one place.
Updated 2026-10-15 · release notes

also called card, container, glass pane, box

shelf controls

uses material, scroll-area

used by components, quote, tabs, group-box, stat, log, field, hover-card, accordion, timeline, deployment, trace, web-preview, email-address, github, tweet, social-profile

the demo above, verbatim

panel.demo.tsx
"use client"

import { Ellipsis } from "lucide-react"
import { Sample } from "@/app/samples"
import { Badge } from "@/registry/base-nova/ui/badge"
import { Button } from "@/registry/base-nova/ui/button"
import { MaterialProvider } from "@/registry/base-nova/ui/material"
import { Panel } from "@/registry/base-nova/ui/panel"

// #region release
function Release() {
  return (
    <Panel
      title="lore 1.4"
      description="ready to ship: notes reviewed, build notarized"
      action={
        <Button variant="ghost" size="icon-sm" aria-label="more">
          <Ellipsis />
        </Button>
      }
      footer={
        <>
          <Button>publish</Button>
          <Button variant="ghost">later</Button>
        </>
      }
      className="w-full max-w-80"
    >
      <div className="flex gap-2">
        <Badge variant="secondary">macOS 26</Badge>
        <Badge variant="outline">12 fixes</Badge>
      </div>
    </Panel>
  )
}
// #endregion

// #region inbox
const MESSAGES = Array.from({ length: 16 }, (_, i) => ({
  from: ["ana", "ben", "chen", "dara"][i % 4],
  line: [
    "the build is green again",
    "can you look at the cart?",
    "shipping notes are in",
    "a 504 from stripe, retried",
  ][i % 4],
}))

/** A head across its top, a foot along its bottom, and a body capped at a height:
 *  past it the body scrolls beneath both, which blur it in glass and hide it plain. */
function Inbox() {
  return (
    <Panel
      className="w-full max-w-sm"
      maxHeight="20rem"
      head={<span className="font-medium text-sm">inbox</span>}
      action={<Badge variant="secondary">16</Badge>}
      foot={
        <>
          <Button size="sm">reply</Button>
          <Button size="sm" variant="ghost">
            archive
          </Button>
        </>
      }
      bodyClassName="gap-0 py-3"
    >
      {MESSAGES.map((m, i) => (
        // biome-ignore lint/suspicious/noArrayIndexKey: a fixed run of messages
        <p key={i} className="m-0 py-2 text-[13px]">
          <span className="font-medium">{m.from}</span>{" "}
          <span className="text-current/60">{m.line}</span>
        </p>
      ))}
    </Panel>
  )
}
// #endregion

export default function PanelDemo() {
  return (
    <div className="space-y-8">
      <Sample
        name="head-foot-scroll"
        title="a head, a foot, a body that scrolls"
        description="maxHeight caps it: the body scrolls under the head and the foot, glass blurring what passes beneath them, plain hiding it; the glass's edge spills what nears it."
        with="inbox"
      >
        <div className="grid gap-4 lg:grid-cols-2">
          <div className="p-6">
            <Inbox />
          </div>
          <MaterialProvider material="glass">
            <div className="rounded-xl bg-[url(/lightbox/peaks.jpg)] bg-cover bg-center p-6">
              <Inbox />
            </div>
          </MaterialProvider>
        </div>
      </Sample>
      <Sample
        name="materials"
        title="a card, plain and glass"
        description="everything inside takes the panel's material from the provider: the badges and buttons are glass with it."
        with="release"
      >
        <div className="grid gap-4 lg:grid-cols-2">
          <Release />
          <MaterialProvider material="glass">
            <div className="rounded-xl bg-[url(/lightbox/lake.jpg)] bg-cover bg-center p-6">
              <Release />
            </div>
          </MaterialProvider>
        </div>
      </Sample>
      <Sample
        name="release"
        glance
        title="a release, ready to ship"
        with="release"
      >
        <Release />
      </Sample>
    </div>
  )
}

the source, every file an install writes

panel.tsx
"use client"

import { useRender } from "@base-ui/react/use-render"
import * as React from "react"
import { cn } from "@/lib/utils"
import {
  type Material,
  MaterialProvider,
  type Scheme,
  useMaterial,
  useSurfaceScheme,
} from "@/registry/base-nova/ui/material"
import {
  GlassEdge,
  type GlassLight,
  GlassSpill,
  type GlassStyle,
  glassFront,
  glassStyle,
  glassVariants,
  schemeOf,
} from "@/registry/base-nova/ui/material-glass"
import { ScrollArea } from "@/registry/base-nova/ui/scroll-area"

// THE CONTAINER: a surface that holds one thing. shadcn's Card, by another name (in
// this registry a name is a family, and `card-gallery`, `card-link` are their own
// items). Every box in the registry is this one, in either material: plain, the
// page's card with its hairline; glass, the material's glass, rounder, as glass is.
// Glass is a face of the panel, never a family of its own.
//
// What it is, above what it holds: `title`, a line under it (`description`),
// something to do with it beside them (`action`), what ends it (`footer`). Props, not
// five components to assemble, because a card's head is always the same three things
// in the same places.
//
// Its FRONT SURFACES: a `head` across its top and a `foot` along its bottom, strips of
// the panel itself, not pieces laid on it (a tab list, a board's column names, a
// card's buttons). In glass each is the material's front (`glassFront`): its own blur
// over what passes under it, one lit hairline as its inner edge, the panel's edge
// unbroken over both. Plain, a solid strip a step darker than the card. A head is
// `headHeight` tall (3rem), a foot `footHeight`, told to what it holds as
// `--panel-head` and `--panel-foot`; what pins under the head (a table's header row)
// pins at `--panel-under`.
//
//   <Panel material="glass" head={<TabsList>…</TabsList>} maxHeight="30rem">…</Panel>
//
// It SCROLLS when it is given a height: `maxHeight` (it grows to the cap, then
// scrolls) or `height` (it keeps it whatever it holds; what fills it, a log, scrolls
// itself). Its body is then the one capped scroller (`ScrollArea`), the head and the
// foot laid OVER it, so what scrolls passes beneath them and comes out from under them:
// in glass blurred by them, plain hidden by them. Never a front inside the scroller:
// a plain scroller's fade is a mask, which makes it a backdrop root, and a glass front
// inside it would blur nothing. Long ones wear thumbs (`thumb`, on).
// Growing with what it holds, its head can PIN to the page while the body scrolls
// under it (`pin`, a CSS length: below a site header). Plain, its frame is then drawn
// by the head and the body, so the head pinned mid-panel is still the panel's rounded
// top. `glide` eases the body to a new height (a tab of another length) instead of
// jumping.
//
// Glass is built of the material's parts, bottom to top: a back layer (the body: its
// blur, its fill, the shadow it casts), what it holds, the spill (`GlassSpill`: what
// nears the edge spills its light round it, on unless `spill={false}`), the front
// surfaces, the edge (`GlassEdge`). The root never blurs: a backdrop blur inside
// another sees only what is between the two, so a front inside a blurring root
// blurred nothing passing under it. What it holds stands on glass (`--glass-body`):
// a control inside takes its plain face unless chosen otherwise (no glass on glass).
//
// A frame is drawn once. What draws a box of its own when it stands alone (a log, a
// trace) asks `useInFrame()` and draws none inside one: a panel, or anything that
// says so with `<InFrame>`. A box inside a box was two edges, two corners and two
// grounds a few pixels apart; the default makes that impossible to write by accident,
// and the item's own `frame` prop still decides when given.

const Framed = React.createContext(false)

/** What it holds stands inside a frame already drawn: a box of its own is not. */
export function InFrame({ children }: { children: React.ReactNode }) {
  return <Framed.Provider value>{children}</Framed.Provider>
}

/** Whether a frame is already drawn round this point (a panel). */
export function useInFrame(): boolean {
  return React.useContext(Framed)
}

/** Inside a panel's head: what fits itself into the strip (a tab list) reads it. */
const InHead = React.createContext<{ material: Material } | null>(null)

/** The head this is inside, or null: a tab list in a head fits the strip (it stands
 *  as far from the panel's side as from the strip's top, a line list on the strip's
 *  rule). */
export function usePanelHead(): { material: Material } | null {
  return React.useContext(InHead)
}

/** The body of a panel that scrolls: its scroller, and whether the panel keeps a
 *  height its body fills (`height`). */
export type PanelBody = {
  scroller: React.RefObject<HTMLDivElement | null>
  filled: boolean
}
const InBody = React.createContext<PanelBody | null>(null)

/** The body of the panel this is inside when that panel scrolls, or null. A tab's
 *  panel keeps its own place in the scroll by it, and fills a panel of a set height. */
export function usePanelBody(): PanelBody | null {
  return React.useContext(InBody)
}

export type PanelProps = Omit<
  useRender.ComponentProps<"section">,
  "title" | "style"
> &
  GlassLight & {
    /** What it holds, in a few words. */
    title?: React.ReactNode
    /** A line under the title. */
    description?: React.ReactNode
    /** Something to do with it (a button, a menu): at the head's end, else beside
     *  the title. */
    action?: React.ReactNode
    /** What ends it, in its flow (its buttons, a note). */
    footer?: React.ReactNode
    /** A front surface across its top: a tab list, column names. */
    head?: React.ReactNode
    /** The head's height, a CSS length (3rem). */
    headHeight?: string
    headClassName?: string
    /** A front surface along its bottom: a card's buttons, a name. */
    foot?: React.ReactNode
    /** The foot's height, a CSS length (3rem). */
    footHeight?: string
    footClassName?: string
    /** A cap on its height, a CSS length: past it the body scrolls. */
    maxHeight?: string
    /** Its height, a CSS length, whatever it holds: the body scrolls what does not
     *  fit, and what scrolls itself (a log) fills it. */
    height?: string
    /** Where its head pins in a page that scrolls, a CSS length (`"0px"`, below a
     *  site header `"var(--header-height)"`). Only for a panel that grows. */
    pin?: string
    /** The head and the foot lie OVER what it holds, not above and below it, though
     *  the panel itself does not scroll: what it holds runs under them and pads
     *  itself by `--panel-head` and `--panel-foot` (a board's columns, each its
     *  own `ScrollArea cover="var(--panel-head)"`, scrolling under the head). */
    over?: boolean
    /** Overlay thumbs while its body scrolls. Default on. */
    thumb?: boolean
    /** Its body eases to a new height rather than jumping (a tab of another
     *  length). Only for a panel that grows. */
    glide?: boolean
    /** In glass, what nears its edge spills its light round it. Off for content
     *  that must stay crisp to the edge. Default on. */
    spill?: boolean
    /** Draws its surface (plain's card, glass's body and edge). Off, it is its
     *  layout and its context alone: what draws a box of its own when it stands
     *  alone (a log, a trace) passes `frame={!useInFrame()}` and is drawn once.
     *  Default on. */
    frame?: boolean
    /** Its corners, a CSS length (never a percentage): plain, the theme's xl;
     *  glass, 1.5rem. */
    radius?: string
    /** Classes on what lays out what it holds (its padding, its gap: `flex flex-col
     *  gap-4 p-5`). On the panel itself unless it has a head, a foot, a height or a
     *  glide; there `className` is the frame's alone. */
    bodyClassName?: string
    /** Glass: its body's colour (default the glass's own), and its front surfaces'
     *  (default the page's background at 55%), any CSS colour thin enough to stay
     *  glass. Set on that surface alone, so a panel inside keeps its own. */
    backColor?: string
    frontColor?: string
    material?: Material
    /** The colour scheme it stands in (material): default the nearest provider's.
     *  Dark, everything inside reads in the dark scheme too. */
    scheme?: Scheme
    style?: GlassStyle
  }

/** What lays out what a panel holds, unless `bodyClassName` says otherwise. */
const BODY = "flex flex-col gap-4 p-5"

/** A panel inside a frame already drawn (`useInFrame`: a panel's body, or what
 *  says so with `InFrame`), in either material: a step of light off the surface
 *  under it, a hairline and the theme's corner, nothing of glass's own (no blur,
 *  no rim, no spill: glass inside glass drew two rims, two blurs and two bodies
 *  where the eye wants one surface and what is on it). A panel that is a link
 *  brightens a step under the hand. Every box that stands in another (a profile,
 *  a post, a preview) is this one, drawn once here. */
const INSET =
  "rounded-(--panel-radius) border border-current/[0.07] bg-current/[0.035] transition-colors [a&]:hover:bg-current/[0.06]"

/** A plain front's face: recessed, a step darker than the card in both schemes
 *  (black over the card: 4% on a light one, 25% on a dark one, where steps must be
 *  wider apart to read), solid, so what scrolls under it stays hidden. A tab chosen
 *  in it wears the card's colour and rises out of it as a browser's tab does. A
 *  front of the card's own colour read as one flat box with a line in it, and one
 *  lighter than the card made the chosen tab a dark hole in it (Adrian, 2026-10-12). */
const PLAIN_FRONT = cn(
  "border-border bg-background bg-[linear-gradient(rgb(0_0_0/0.04),rgb(0_0_0/0.04)),linear-gradient(var(--color-card),var(--color-card))]",
  "scheme-dark:bg-[linear-gradient(rgb(0_0_0/0.25),rgb(0_0_0/0.25)),linear-gradient(var(--color-card),var(--color-card))]",
)

/** A plain panel whose head pins to the page draws its frame by its parts: the head
 *  is its TOP (its top and side edges, the top corners) and the body the rest (its
 *  sides, its foot, the bottom corners, the card's colour). Pinned mid-panel the head
 *  is still the panel's rounded top, the body's sides running on below it; drawn by
 *  the panel round them, the pinned head was a square strip between two straight
 *  edges (Adrian, 2026-10-12). */
const PLAIN_HEAD_EDGE = "border rounded-t-(--panel-radius)"
const PLAIN_BODY_EDGE =
  "border-border border-x border-b bg-background bg-[linear-gradient(var(--color-card),var(--color-card))] rounded-b-(--panel-radius)"

/** What a plain pinned head pins in: a square of the page's ground, the head
 *  (rounded) inside it, so rows passing under it never show behind its corners. */
const PLAIN_PIN = "sticky top-(--panel-pin) z-10 bg-background"

/** How a glass panel's light follows its state: a panel that moves (a card lifting
 *  under the pointer) and catches more light does both as one gesture, the light on
 *  its own 500 ms ease trailing the movement by a breath. Eased on the two elements
 *  that draw it (the edge, the fronts), never on the root: the rim is inherited,
 *  and moving there restyled everything the panel holds on every frame. */
const PANEL_LIGHT =
  "[transition:--glass-rim_500ms_cubic-bezier(0.22,1,0.36,1)_40ms] motion-reduce:[transition:none]"

/** The body's glide (`glide`): the height it is given follows what it holds, eased
 *  on the tabs' curve; on after the first reading, so the page opens at its height,
 *  never growing from nothing. */
const GLIDE =
  "transition-[height] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none"

function useGlide(on: boolean) {
  const [inner, setInner] = React.useState<HTMLDivElement | null>(null)
  const [height, setHeight] = React.useState<number | null>(null)
  const [moving, setMoving] = React.useState(false)
  React.useLayoutEffect(() => {
    if (!on || !inner) return
    const read = () => setHeight(inner.getBoundingClientRect().height)
    read()
    const frame = requestAnimationFrame(() => setMoving(true))
    const sizes = new ResizeObserver(read)
    sizes.observe(inner)
    return () => {
      cancelAnimationFrame(frame)
      sizes.disconnect()
    }
  }, [on, inner])
  return { setInner, height, moving }
}

export function Panel({
  title,
  description,
  action,
  footer,
  head,
  headHeight = "3rem",
  headClassName,
  foot,
  footHeight = "3rem",
  footClassName,
  maxHeight,
  height,
  pin,
  over = false,
  thumb = true,
  glide = false,
  spill = true,
  frame = true,
  radius,
  bodyClassName,
  backColor,
  frontColor,
  rim,
  rimWidth,
  rimColor,
  tint,
  tintAmount,
  material,
  scheme,
  render,
  className,
  style,
  children,
  ref,
  ...props
}: PanelProps) {
  // Inside a frame already drawn, its surface is the inset one (`INSET`) whatever
  // the material: its glass would be glass on glass.
  const inFrame = useInFrame()
  const own = useMaterial(material)
  const inset = frame && inFrame
  const m = inset ? "plain" : own
  const glass = m === "glass"
  const [s, here] = useSurfaceScheme<HTMLElement>(scheme, ref)
  const scroller = React.useRef<HTMLDivElement | null>(null)
  const scrolls = maxHeight !== undefined || height !== undefined
  // The fronts lie over what it holds: its own scroller's, or (`over`) content
  // that runs under them by itself.
  const laid = scrolls || over
  if (over && (scrolls || pin !== undefined))
    throw new Error(
      "Panel: `over` lays the fronts over content that scrolls by itself; a panel given a height lays them over its scroller already, and a pinned head is in the flow",
    )
  if (maxHeight !== undefined && height !== undefined)
    throw new Error(
      "Panel: `maxHeight` caps a panel that grows, `height` keeps one still; give one",
    )
  if (scrolls && (pin !== undefined || glide))
    throw new Error(
      "Panel: `pin` and `glide` are for a panel that grows with what it holds; one given a height scrolls itself, its head laid over its top",
    )
  if (pin !== undefined && head === undefined)
    throw new Error("Panel: `pin` pins the head to the page; give a `head`")
  if (head !== undefined && (title !== undefined || description !== undefined))
    throw new Error(
      "Panel: a panel with a head names itself in it (or in a heading above it); `title` and `description` are for one without",
    )
  // Asked of the material the caller chose, never the one drawn: a glass panel in
  // a frame draws the inset face, and its light is simply not drawn there. Asked of
  // the drawn one it threw on a nested glass panel's own tint (garden's /s/lore).
  if (
    own !== "glass" &&
    [backColor, frontColor, rim, rimWidth, rimColor, tint, tintAmount].some(
      (v) => v !== undefined,
    )
  )
    throw new Error(
      'Panel: `backColor`, `frontColor` and the light (`rim`, `tint`, …) are the glass face\'s; give `material="glass"`',
    )
  const glideBody = useGlide(glide)
  const titled = title || description || (action && head === undefined)
  // The panel is its own body (what lays out what it holds) unless something stands
  // apart from that body: a front surface, a scroller, a glide.
  const parts = head !== undefined || foot !== undefined || scrolls || glide
  // Plain and pinned to the page: the frame is drawn by the head and the body.
  const partsFrame = !glass && pin !== undefined && head !== undefined

  const header = titled && (
    <header
      data-slot="panel-header"
      className="flex items-start justify-between gap-4"
    >
      <div className="min-w-0 space-y-1">
        {title && (
          <h3
            data-slot="panel-title"
            className="font-medium text-sm leading-tight"
          >
            {title}
          </h3>
        )}
        {description && (
          <p
            data-slot="panel-description"
            className="text-pretty text-current/60 text-xs leading-5"
          >
            {description}
          </p>
        )}
      </div>
      {action && (
        <div data-slot="panel-action" className="-my-1 shrink-0">
          {action}
        </div>
      )}
    </header>
  )
  const footerPart = footer && (
    <footer data-slot="panel-footer" className="flex items-center gap-2 pt-1">
      {footer}
    </footer>
  )
  const held = (
    <>
      {header}
      {children}
      {footerPart}
    </>
  )

  // A front surface: laid over the scroller when the panel scrolls (or over what it
  // holds, `over`), else in the panel's flow (pinned to the page with `pin`).
  const front = (at: "top" | "bottom") => {
    const top = at === "top"
    const pinned = top && pin !== undefined
    return cn(
      "z-10 flex shrink-0 items-center gap-4 px-5",
      // A folder's tabs (a plain line list, tabs' `data-folder`) stand from a little
      // below the strip's top: the action stands as low, on their centre line.
      "[&:has(>[data-folder])>[data-slot=panel-action]]:self-stretch [&:has(>[data-folder])>[data-slot=panel-action]]:pt-2",
      top ? "h-(--panel-head)" : "h-(--panel-foot)",
      glass
        ? [
            glassFront[at],
            PANEL_LIGHT,
            // Pinned mid-panel it is square, the panel's clip rounding it at rest.
            pinned && "rounded-none",
          ]
        : [
            PLAIN_FRONT,
            top
              ? partsFrame
                ? PLAIN_HEAD_EDGE
                : "rounded-t-[inherit] border-b"
              : "rounded-b-[inherit] border-t",
          ],
      laid
        ? ["absolute inset-x-0", top ? "top-0" : "bottom-0"]
        : pinned
          ? glass
            ? "sticky top-(--panel-pin)"
            : "relative"
          : "relative",
      top ? headClassName : footClassName,
    )
  }
  const headPart = head !== undefined && (
    <div data-slot="panel-head" className={front("top")}>
      <InHead.Provider value={{ material: m }}>{head}</InHead.Provider>
      {action && (
        <div
          data-slot="panel-action"
          className="ml-auto flex shrink-0 items-center gap-2"
        >
          {action}
        </div>
      )}
    </div>
  )
  const footPart = foot !== undefined && (
    <div data-slot="panel-foot" className={front("bottom")}>
      {foot}
    </div>
  )

  // What it holds stands on glass, told so (a sibling of the back layer, not inside
  // it): a control inside takes its plain face.
  const onGlass = glass && frame && "[--glass-body:1]"
  const body = scrolls ? (
    <ScrollArea
      ref={scroller}
      data-slot="panel-scroller"
      thumb={thumb}
      // edge: in glass the spill ends it, a fade under it ended it twice.
      fade={!(glass && frame && spill)}
      cover={head === undefined ? undefined : "var(--panel-head)"}
      coverEnd={foot === undefined ? undefined : "var(--panel-foot)"}
      className={cn(
        onGlass,
        maxHeight !== undefined
          ? "max-h-(--panel-max)"
          : "flex flex-1 flex-col",
      )}
    >
      <InBody.Provider value={{ scroller, filled: height !== undefined }}>
        <div
          data-slot="panel-body"
          data-panel-pad=""
          className={cn(BODY, height !== undefined && "flex-1", bodyClassName)}
        >
          {held}
        </div>
      </InBody.Provider>
    </ScrollArea>
  ) : parts ? (
    <div
      data-slot="panel-body"
      data-panel-pad={glide ? undefined : ""}
      className={cn(
        onGlass,
        glide && "overflow-clip",
        glide && glideBody.moving && GLIDE,
        partsFrame && PLAIN_BODY_EDGE,
        !glide && [BODY, bodyClassName],
      )}
      style={
        glide && glideBody.height !== null
          ? { height: `${glideBody.height}px` }
          : undefined
      }
    >
      {glide ? (
        <div
          ref={glideBody.setInner}
          data-panel-pad=""
          className={cn(BODY, bodyClassName)}
        >
          {held}
        </div>
      ) : (
        held
      )}
    </div>
  ) : glass ? (
    // The panel is its own body: its layout stays the root's, and what it holds is
    // marked as standing on glass by a wrapper that lays out nothing.
    <div className={cn("contents", onGlass)}>{held}</div>
  ) : (
    held
  )

  const vars = {
    "--panel-radius": radius ?? (glass ? "1.5rem" : "var(--radius-xl)"),
    ...(head === undefined
      ? {}
      : {
          "--panel-head": headHeight,
          // Where the head ends, for what pins under it (a table's header row,
          // `sticky top-(--panel-under)`): below the pinned head on the page, or
          // the head's own height inside the panel's scroller.
          "--panel-under":
            pin === undefined
              ? "var(--panel-head)"
              : "calc(var(--panel-pin) + var(--panel-head))",
        }),
    ...(foot === undefined ? {} : { "--panel-foot": footHeight }),
    ...(maxHeight === undefined ? {} : { "--panel-max": maxHeight }),
    ...(height === undefined ? {} : { "--panel-height": height }),
    ...(pin === undefined ? {} : { "--panel-pin": pin }),
    ...(glass
      ? {
          "--glass-radius": "var(--panel-radius)",
          ...(frontColor === undefined
            ? {}
            : { "--glass-front-color": frontColor }),
          ...glassStyle({ rim, rimWidth, rimColor, tint, tintAmount }),
        }
      : {}),
  }
  // The edge and the spill draw the corners as an outline, which needs the length
  // itself, never a variable.
  const edgeRadius = radius ?? "1.5rem"

  // Pinned in glass: the head is square, so the panel's corners are a clip round
  // the head and the body; `clip`, which is no scroller, so the head pins on.
  const flow =
    glass && pin !== undefined && head !== undefined ? (
      <div className="flex flex-col overflow-clip rounded-[inherit]">
        {headPart}
        {body}
        {footPart}
      </div>
    ) : (
      <>
        {!laid &&
          headPart &&
          (partsFrame ? (
            <div className={cn(PLAIN_PIN, "shrink-0")}>{headPart}</div>
          ) : (
            headPart
          ))}
        {body}
        {!laid && footPart}
      </>
    )

  return useRender({
    defaultTagName: "section",
    render,
    ref: here,
    props: {
      "data-slot": "panel",
      "data-material": m,
      // The element whose padding is the body's (here, or the body part): what
      // runs to the panel's edge reads it there (tabs' track).
      "data-panel-pad": parts ? undefined : "",
      className: cn(
        "relative flex flex-col rounded-(--panel-radius)",
        !parts && [BODY, bodyClassName],
        height !== undefined && "h-(--panel-height)",
        schemeOf(s),
        !frame
          ? undefined
          : glass
            ? // No glass of its own (its body is the back layer): its corners,
              // the words a glass's body gives what it holds, and its own
              // movement (a card lifting under the pointer), which its light
              // follows on the same ease (PANEL_LIGHT).
              "isolate text-foreground scheme-light:text-zinc-950 [transition:translate_500ms_cubic-bezier(0.22,1,0.36,1),scale_500ms_cubic-bezier(0.22,1,0.36,1)] motion-reduce:[transition:none]"
            : inset
              ? INSET
              : partsFrame
                ? "shadow-sm"
                : // A panel that is a link brightens a step under the hand.
                  "border border-border bg-card text-card-foreground shadow-sm transition-colors [a&]:hover:bg-[color-mix(in_srgb,var(--card),var(--foreground)_3%)]",
        className,
      ),
      style: { ...vars, ...style } as React.CSSProperties,
      ...props,
      // What is inside stands on the panel, not on what is behind it: it wears the
      // panel's scheme, whatever decided it (a picture under the panel included).
      children: (
        <MaterialProvider scheme={s}>
          <InFrame>
            {glass && frame && (
              <div
                aria-hidden
                data-slot="panel-back"
                className={cn(
                  glassVariants({ shape: "surface", edge: false }),
                  "pointer-events-none absolute inset-0 -z-10 [--glass-radius:inherit]",
                )}
                style={
                  backColor === undefined
                    ? undefined
                    : ({ "--glass-tint": backColor } as GlassStyle)
                }
              />
            )}
            {flow}
            {glass && frame && spill && <GlassSpill radius={edgeRadius} />}
            {laid && headPart}
            {laid && footPart}
            {glass && frame && (
              <GlassEdge
                radius={edgeRadius}
                className={cn("z-20", PANEL_LIGHT)}
              />
            )}
          </InFrame>
        </MaterialProvider>
      ),
    },
  })
}