Skip to content

Panel ​

A surface container for grouping content, the building block for cards, sidebars, and sectioned layouts. Panel provides a rounded, bordered surface; its parts (PanelHeader, PanelContent, PanelFooter) give it structure.

The parts are independent layout pieces, compose the ones you need, in the order you need. The common arrangement is header, content, footer.

Anatomy ​

Panel
├── PanelHeader
├── PanelContent
└── PanelFooter

Usage ​

BasicA panel with a header, content, and footer.
<script setup lang="ts">
import {
    Panel,
    PanelHeader,
    PanelContent,
    PanelFooter,
    Avatar,
    Badge,
    Separator,
    Button,
} from "@dlbcodes/ui";
import { PhDotsThree, PhPlus } from "@phosphor-icons/vue";

const members = [
    {
        name: "Ana Silva",
        img: "https://i.pravatar.cc/120?img=10",
        email: "ana@acme.co",
        role: "Owner",
        status: "success" as const,
    },
    {
        name: "John Carter",
        img: "https://i.pravatar.cc/120?img=3",
        email: "john@acme.co",
        role: "Admin",
        status: "success" as const,
    },
    {
        name: "Maria Costa",
        img: "https://i.pravatar.cc/120?img=9",
        email: "maria@acme.co",
        role: "Member",
        status: "pending" as const,
    },
];
</script>

<template>
    <div class="w-full max-w-sm">
        <Panel>
            <PanelHeader>
                <span class="text-sm font-semibold text-text-primary"
                    >Team members</span
                >
                <Button
                    variant="icon"
                    size="icon-sm"
                    aria-label="Panel options"
                    class="ml-auto"
                >
                    <PhDotsThree class="size-5" weight="bold" />
                </Button>
            </PanelHeader>

            <PanelContent class="p-2">
                <div class="flex flex-col">
                    <template v-for="(m, i) in members" :key="m.email">
                        <div class="flex items-center gap-3 px-2 py-2">
                            <Avatar :name="m.name" :src="m.img" size="sm" />
                            <div class="min-w-0 flex-1">
                                <div
                                    class="truncate text-sm font-medium text-text-primary"
                                >
                                    {{ m.name }}
                                </div>
                                <div
                                    class="truncate text-xs text-text-tertiary"
                                >
                                    {{ m.email }}
                                </div>
                            </div>
                            <Badge :variant="m.status">
                                {{
                                    m.status === "success"
                                        ? "Active"
                                        : "Pending"
                                }}
                            </Badge>
                        </div>
                        <Separator v-if="i < members.length - 1" />
                    </template>
                </div>
            </PanelContent>

            <PanelFooter>
                <span class="text-xs text-text-tertiary">3 members</span>
                <Button variant="ghost" size="sm" class="ml-auto">
                    <PhPlus class="size-4" />
                    Invite
                </Button>
            </PanelFooter>
        </Panel>
    </div>
</template>

Content only ​

Use just PanelContent for a simple raised surface inside the panel, no header or footer required.

Content onlyA panel with only a content surface.
<script setup lang="ts">
import { Panel, PanelContent } from "@dlbcodes/ui";
</script>

<template>
    <div class="w-full max-w-sm">
        <Panel>
            <PanelContent class="p-4">
                <div class="text-sm text-text-secondary">
                    A raised content surface with concentric rounded corners,
                    nested cleanly inside the panel.
                </div>
            </PanelContent>
        </Panel>
    </div>
</template>

As a navbar ​

Panel isn't only for cards. Because it's a generic surface, it works just as well for app chrome, here it composes a brand mark, navigation links, and actions into a navigation bar.

NavbarA navigation bar built from a Panel surface.
<script setup lang="ts">
import { Panel, PanelContent, Button } from "@dlbcodes/ui";
import { PhCube } from "@phosphor-icons/vue";
</script>

<template>
    <div class="w-full max-w-2xl">
        <Panel>
            <PanelContent class="flex items-center justify-between p-3">
                <!-- brand -->
                <div class="flex items-center gap-2">
                    <div
                        class="flex size-8 items-center justify-center rounded-lg bg-brand-200 text-white"
                    >
                        <PhCube class="size-5" weight="fill" />
                    </div>
                    <span
                        class="text-lg font-semibold tracking-tight text-text-primary"
                    >
                        ACME
                    </span>
                </div>

                <!-- actions -->
                <div class="flex items-center gap-2">
                    <Button variant="secondary" size="sm">Sign in</Button>
                    <Button variant="primary" size="sm">Get started</Button>
                </div>
            </PanelContent>
        </Panel>
    </div>
</template>

Appearance ​

Panel is a --muted frame with a --border outline, and PanelContent is a --card surface inside it. In the light theme the content is the brighter of the two; in dark mode it is a step darker than the frame, so it reads as inset. Both follow your theme. See Theming.

Concentric corners ​

PanelContent uses a slightly smaller corner radius than Panel so the inner surface nests cleanly inside the outer border. This concentric radius (Panel's radius minus its padding) keeps the rounded corners visually parallel rather than mismatched.

Both radii come from the theme's --radius token, so they stay concentric when you change it, from square to very round. It assumes PanelContent sits directly inside Panel.

Props ​

Every part accepts a single class prop, merged onto its root element.

ComponentPurpose
PanelThe outer surface: border, rounding, padding, shadow.
PanelHeaderA top row for a title and controls.
PanelContentThe main raised surface (concentric inner radius).
PanelFooterA bottom row for actions or metadata.

Notes ​

The parts are not coupled by shared state, they're independent layout containers. You can omit any of them, reorder them, or use PanelContent on its own. The concentric corner radius on PanelContent is calibrated for being a direct child of Panel; if you nest it elsewhere, set the radius yourself via class (for example rounded-xl).