Renders as an article element for self-contained content.
Renders as a main element for primary page content.
A foundational background component for creating theme-aware panels with support for light and dark modes. The Panel component provides three emphasis levels (primary, secondary, tertiary), semantic feedback colors, interactive states, and extensive customization options for building cards, sections, and container elements.
Up 12.4% versus last month — driven by stronger Pro plan retention and a healthy bump in annual upgrades.
Panel supports five surface levels (surface.soft through surface.deep) plus transparent. Surface 1 is the lightest and surface 5 is the darkest — use them to create visual hierarchy within your layout. Use the translucent prop to add a frosted glass effect.
Lightest surface — the default and primary background for the application.
Slightly darker surface for subtle layering.
Mid-tone surface for visual prominence.
Darker surface for nested containers or secondary areas.
Darkest surface for recessed areas, wells, or secondary zones.
No background — inherits from its parent container.
Frosted Glass Effect
This translucent panel has a frosted glass effect with backdrop blur. Notice how the colorful shapes behind it are visible but blurred.
---
import Panel from "../Panel.astro";
import { css } from "@pindoba/styled-system/css";
import { stack } from "@pindoba/styled-system/patterns";
---
<div
class={stack({
gap: "xl",
direction: "column",
})}
>
<!-- Surface.1 (lightest) -->
<div>
<h3>Surface 1</h3>
<Panel background="surface.soft" padding="lg">
<p>
Lightest surface — the default and primary background for the
application.
</p>
</Panel>
</div>
<!-- Surface.2 -->
<div>
<h3>Surface 2</h3>
<Panel background="surface.step.1" padding="lg">
<p>Slightly darker surface for subtle layering.</p>
</Panel>
</div>
<!-- Surface.3 (mid) -->
<div>
<h3>Surface 3</h3>
<Panel background="surface.step.2" padding="lg">
<p>Mid-tone surface for visual prominence.</p>
</Panel>
</div>
<!-- Surface.4 -->
<div>
<h3>Surface 4</h3>
<Panel background="surface.step.3" padding="lg">
<p>Darker surface for nested containers or secondary areas.</p>
</Panel>
</div>
<!-- Surface.5 (darkest) -->
<div>
<h3>Surface 5</h3>
<Panel background="surface.deep" padding="lg">
<p>Darkest surface for recessed areas, wells, or secondary zones.</p>
</Panel>
</div>
<!-- Transparent -->
<div>
<h3>Transparent</h3>
<Panel background="transparent" padding="lg" border="default">
<p>No background — inherits from its parent container.</p>
</Panel>
</div>
<!-- Translucent -->
<div>
<h3>Translucent</h3>
<div
class={css({
position: "relative",
padding: "md",
borderRadius: "md",
overflow: "hidden",
background:
"linear-gradient(135deg, token(colors.primary) 0%, token(colors.primary.surface) 50%, token(colors.primary) 100%)",
})}
>
<!-- Decorative SVG shapes in background -->
<svg
class={css({
position: "absolute",
top: "0",
left: "0",
width: "full",
height: "full",
})}
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="20%" cy="30%" r="60" fill="var(--colors-primary-surface)"
></circle>
<circle
cx="80%"
cy="60%"
r="80"
fill="var(--colors-primary-border-muted)"></circle>
<rect
x="40%"
y="10%"
width="100"
height="100"
fill="var(--colors-primary-border)"
rx="15"></rect>
<polygon
points="70,20 90,60 50,60"
fill="var(--colors-primary)"
transform="translate(200, 80)"></polygon>
<circle cx="60%" cy="80%" r="50" fill="var(--colors-primary-hover)"
></circle>
<rect
x="10%"
y="70%"
width="80"
height="80"
fill="var(--colors-primary-active)"
rx="20"></rect>
</svg>
<Panel
translucent
padding="md"
radius="xs"
class={css({ position: "relative", zIndex: "1" })}
>
<p><strong>Frosted Glass Effect</strong></p>
<p>
This translucent panel has a frosted glass effect with backdrop blur.
Notice how the colorful shapes behind it are visible but blurred.
</p>
</Panel>
</div>
</div>
</div>Use semantic feedback colors to convey meaning and context. Panels default to a neutral (gray) color palette. All five feedback colors (neutral, primary, success, warning, danger) work with all three emphasis levels.
Emphasis Secondary
colorPalette.surface — neutral tinted background.
Emphasis Tertiary
neutral.surface — always neutral regardless of feedback.
Emphasis Primary
Full-color neutral (500) background with contrast text.
Emphasis Secondary
colorPalette.surface — palette-aware tinted background.
Emphasis Tertiary
neutral.surface — always neutral regardless of feedback.
Emphasis Primary
Full-color primary (500) background with contrast text.
Emphasis Secondary
colorPalette.surface — palette-aware tinted background.
Emphasis Tertiary
neutral.surface — always neutral regardless of feedback.
Emphasis Primary
Full-color success (500) background with contrast text.
Emphasis Secondary
colorPalette.surface — palette-aware tinted background.
Emphasis Tertiary
neutral.surface — always neutral regardless of feedback.
Emphasis Primary
Full-color warning (500) background with contrast text.
Emphasis Secondary
colorPalette.surface — palette-aware tinted background.
Emphasis Tertiary
neutral.surface — always neutral regardless of feedback.
Emphasis Primary
Full-color danger (500) background with contrast text.
---
import Panel from "../Panel.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div
class={stack({
gap: "xl",
direction: "column",
width: "100%",
})}
>
<div>
<h3>Neutral</h3>
<div class={stack({ gap: "md", direction: "column", width: "100%" })}>
<Panel
feedback="neutral"
emphasis="secondary"
padding="lg"
border="default"
>
<p><strong>Emphasis Secondary</strong></p>
<p>colorPalette.surface — neutral tinted background.</p>
</Panel>
<Panel
feedback="neutral"
emphasis="tertiary"
padding="lg"
border="default"
>
<p><strong>Emphasis Tertiary</strong></p>
<p>neutral.surface — always neutral regardless of feedback.</p>
</Panel>
<Panel feedback="neutral" emphasis="primary" padding="lg">
<p><strong>Emphasis Primary</strong></p>
<p>Full-color neutral (500) background with contrast text.</p>
</Panel>
</div>
</div>
<div>
<h3>Primary</h3>
<div class={stack({ gap: "md", direction: "column", width: "100%" })}>
<Panel
feedback="primary"
emphasis="secondary"
padding="lg"
border="default"
>
<p><strong>Emphasis Secondary</strong></p>
<p>colorPalette.surface — palette-aware tinted background.</p>
</Panel>
<Panel
feedback="primary"
emphasis="tertiary"
padding="lg"
border="default"
>
<p><strong>Emphasis Tertiary</strong></p>
<p>neutral.surface — always neutral regardless of feedback.</p>
</Panel>
<Panel feedback="primary" emphasis="primary" padding="lg">
<p><strong>Emphasis Primary</strong></p>
<p>Full-color primary (500) background with contrast text.</p>
</Panel>
</div>
</div>
<div>
<h3>Success</h3>
<div class={stack({ gap: "md", direction: "column", width: "100%" })}>
<Panel
feedback="success"
emphasis="secondary"
padding="lg"
border="default"
>
<p><strong>Emphasis Secondary</strong></p>
<p>colorPalette.surface — palette-aware tinted background.</p>
</Panel>
<Panel
feedback="success"
emphasis="tertiary"
padding="lg"
border="default"
>
<p><strong>Emphasis Tertiary</strong></p>
<p>neutral.surface — always neutral regardless of feedback.</p>
</Panel>
<Panel feedback="success" emphasis="primary" padding="lg">
<p><strong>Emphasis Primary</strong></p>
<p>Full-color success (500) background with contrast text.</p>
</Panel>
</div>
</div>
<div>
<h3>Warning</h3>
<div class={stack({ gap: "md", direction: "column", width: "100%" })}>
<Panel
feedback="warning"
emphasis="secondary"
padding="lg"
border="default"
>
<p><strong>Emphasis Secondary</strong></p>
<p>colorPalette.surface — palette-aware tinted background.</p>
</Panel>
<Panel
feedback="warning"
emphasis="tertiary"
padding="lg"
border="default"
>
<p><strong>Emphasis Tertiary</strong></p>
<p>neutral.surface — always neutral regardless of feedback.</p>
</Panel>
<Panel feedback="warning" emphasis="primary" padding="lg">
<p><strong>Emphasis Primary</strong></p>
<p>Full-color warning (500) background with contrast text.</p>
</Panel>
</div>
</div>
<div>
<h3>Danger</h3>
<div class={stack({ gap: "md", direction: "column", width: "100%" })}>
<Panel
feedback="danger"
emphasis="secondary"
padding="lg"
border="default"
>
<p><strong>Emphasis Secondary</strong></p>
<p>colorPalette.surface — palette-aware tinted background.</p>
</Panel>
<Panel
feedback="danger"
emphasis="tertiary"
padding="lg"
border="default"
>
<p><strong>Emphasis Tertiary</strong></p>
<p>neutral.surface — always neutral regardless of feedback.</p>
</Panel>
<Panel feedback="danger" emphasis="primary" padding="lg">
<p><strong>Emphasis Primary</strong></p>
<p>Full-color danger (500) background with contrast text.</p>
</Panel>
</div>
</div>
</div>Three emphasis levels control how the feedback color is applied. tertiary (the default) always sits on the neutral surface ramp with feedback-colored border and accent text — the most subtle option. secondary uses palette-aware surface tints. primary fills with the feedback color’s accent.surface.* ramp (anchored on shade 500) and uses contrast text; the background prop still takes surface.* values, each picking a different step within the accent ramp. The border prop still controls outline visibility on tertiary; set border="none" to opt out.
Always sits on the neutral surface ramp, with the feedback color applied to the border and accent text. The most subtle emphasis and the new default.
Primary feedback
Neutral surface, primary border + accent text.
Success feedback
Neutral surface, success border + accent text.
Warning (transparent)
No fill, warning border + text.
Danger (border=none)
Border opt-out — only the accent text remains.
Uses colorPalette.surface.* — a palette-aware tinted background
that responds to the active feedback color.
surface.soft
Default layer.
surface.step.2
Mid-tone palette surface.
surface.deep
Deepest palette surface.
transparent
No background, palette text.
Fills with the feedback color's accent.surface.* ramp (anchored
on shade 500) and uses contrast text. The background
prop still takes surface.* values — each value picks a different
step within the accent ramp.
Default (surface.soft → shade 500)
The brand fill.
surface.step.1
One step deeper than the brand.
surface.step.2
Two steps deeper.
surface.deep
The deepest accent step.
Each feedback color generates its own accent.surface ramp around its shade 500.
Primary
Success
Warning
Danger
---
import Panel from "../Panel.astro";
import { stack, grid } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";
---
<div class={stack({ gap: "2xl", direction: "column", width: "100%" })}>
<!-- Tertiary emphasis (default) -->
<div class={stack({ gap: "md", direction: "column" })}>
<h3>Tertiary (default)</h3>
<p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
Always sits on the neutral surface ramp, with the feedback color applied
to the border and accent text. The most subtle emphasis and the new
default.
</p>
<div class={grid({ columns: 2, gap: "md" })}>
<Panel emphasis="tertiary" feedback="primary" padding="lg">
<p><strong>Primary feedback</strong></p>
<p class={css({ fontSize: "sm" })}>
Neutral surface, primary border + accent text.
</p>
</Panel>
<Panel emphasis="tertiary" feedback="success" padding="lg">
<p><strong>Success feedback</strong></p>
<p class={css({ fontSize: "sm" })}>
Neutral surface, success border + accent text.
</p>
</Panel>
<Panel
emphasis="tertiary"
feedback="warning"
background="transparent"
padding="lg"
>
<p><strong>Warning (transparent)</strong></p>
<p class={css({ fontSize: "sm" })}>No fill, warning border + text.</p>
</Panel>
<Panel emphasis="tertiary" feedback="danger" border="none" padding="lg">
<p><strong>Danger (border=none)</strong></p>
<p class={css({ fontSize: "sm" })}>
Border opt-out — only the accent text remains.
</p>
</Panel>
</div>
</div>
<!-- Secondary emphasis -->
<div class={stack({ gap: "md", direction: "column" })}>
<h3>Secondary</h3>
<p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
Uses <code>colorPalette.surface.*</code> — a palette-aware tinted background
that responds to the active feedback color.
</p>
<div class={grid({ columns: 2, gap: "md" })}>
<Panel
emphasis="secondary"
feedback="primary"
padding="lg"
border="default"
>
<p><strong>surface.soft</strong></p>
<p class={css({ fontSize: "sm" })}>Default layer.</p>
</Panel>
<Panel
emphasis="secondary"
feedback="primary"
background="surface.step.2"
padding="lg"
border="default"
>
<p><strong>surface.step.2</strong></p>
<p class={css({ fontSize: "sm" })}>Mid-tone palette surface.</p>
</Panel>
<Panel
emphasis="secondary"
feedback="primary"
background="surface.deep"
padding="lg"
border="default"
>
<p><strong>surface.deep</strong></p>
<p class={css({ fontSize: "sm" })}>Deepest palette surface.</p>
</Panel>
<Panel
emphasis="secondary"
feedback="primary"
background="transparent"
padding="lg"
border="default"
>
<p><strong>transparent</strong></p>
<p class={css({ fontSize: "sm" })}>No background, palette text.</p>
</Panel>
</div>
</div>
<!-- Primary emphasis -->
<div class={stack({ gap: "md", direction: "column" })}>
<h3>Primary</h3>
<p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
Fills with the feedback color's <code>accent.surface.*</code> ramp (anchored
on shade 500) and uses contrast text. The <code>background</code>
prop still takes <code>surface.*</code> values — each value picks a different
step within the accent ramp.
</p>
<div class={grid({ columns: 2, gap: "md" })}>
<Panel emphasis="primary" feedback="primary" padding="lg">
<p><strong>Default (surface.soft → shade 500)</strong></p>
<p class={css({ fontSize: "sm" })}>The brand fill.</p>
</Panel>
<Panel
emphasis="primary"
feedback="primary"
background="surface.step.1"
padding="lg"
>
<p><strong>surface.step.1</strong></p>
<p class={css({ fontSize: "sm" })}>One step deeper than the brand.</p>
</Panel>
<Panel
emphasis="primary"
feedback="primary"
background="surface.step.2"
padding="lg"
>
<p><strong>surface.step.2</strong></p>
<p class={css({ fontSize: "sm" })}>Two steps deeper.</p>
</Panel>
<Panel
emphasis="primary"
feedback="primary"
background="surface.deep"
padding="lg"
>
<p><strong>surface.deep</strong></p>
<p class={css({ fontSize: "sm" })}>The deepest accent step.</p>
</Panel>
</div>
</div>
<!-- Primary across feedback colors -->
<div class={stack({ gap: "md", direction: "column" })}>
<h3>Primary across feedback colors</h3>
<p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
Each feedback color generates its own accent.surface ramp around its shade
500.
</p>
<div class={grid({ columns: 2, gap: "md" })}>
<Panel emphasis="primary" feedback="primary" padding="lg">
<p><strong>Primary</strong></p>
</Panel>
<Panel emphasis="primary" feedback="success" padding="lg">
<p><strong>Success</strong></p>
</Panel>
<Panel emphasis="primary" feedback="warning" padding="lg">
<p><strong>Warning</strong></p>
</Panel>
<Panel emphasis="primary" feedback="danger" padding="lg">
<p><strong>Danger</strong></p>
</Panel>
</div>
</div>
</div>Choose between different border styles to control the visual weight of panel borders. Use border="none" for no border, border="default" for a standard border, border="bold" for stronger emphasis using a bolder color, or border="muted" for subtle visual separation. Borders use box-shadow so they are layout-neutral — they do not affect element dimensions and compose automatically with the shadow prop.
No Border
Use border="none" for no border.
Default Border
Standard border using the neutral border token.
Bold Border
Stronger border weight for greater visual emphasis.
Muted Border
Lighter border for subtle visual separation.
Primary with Default Border
Success with Default Border
Warning with Bold Border
Danger with Muted Border
---
import Panel from "../Panel.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div
class={stack({
gap: "xl",
direction: "column",
width: "full",
})}
>
<!-- No Border -->
<div>
<h3>None</h3>
<Panel background="surface.soft" padding="lg" border="none">
<p><strong>No Border</strong></p>
<p>Use <code>border="none"</code> for no border.</p>
</Panel>
</div>
<!-- Default Border -->
<div>
<h3>Default</h3>
<Panel background="surface.soft" padding="lg" border="default">
<p><strong>Default Border</strong></p>
<p>Standard border using the neutral border token.</p>
</Panel>
</div>
<!-- Bold Border -->
<div>
<h3>Bold</h3>
<Panel background="surface.soft" padding="lg" border="bold">
<p><strong>Bold Border</strong></p>
<p>Stronger border weight for greater visual emphasis.</p>
</Panel>
</div>
<!-- Muted Border -->
<div>
<h3>Muted</h3>
<Panel background="surface.soft" padding="lg" border="muted">
<p><strong>Muted Border</strong></p>
<p>Lighter border for subtle visual separation.</p>
</Panel>
</div>
<!-- Bordered with Feedback Colors -->
<div>
<h3>Border Styles with Feedback Colors</h3>
<div class={stack({ gap: "md", direction: "column" })}>
<Panel feedback="primary" padding="md" border="default">
<p><strong>Primary with Default Border</strong></p>
</Panel>
<Panel feedback="success" padding="md" border="default">
<p><strong>Success with Default Border</strong></p>
</Panel>
<Panel feedback="warning" padding="md" border="bold">
<p><strong>Warning with Bold Border</strong></p>
</Panel>
<Panel feedback="danger" padding="md" border="muted">
<p><strong>Danger with Muted Border</strong></p>
</Panel>
</div>
</div>
</div>Apply drop shadows to any panel regardless of background. Shadows compose with borders via box-shadow, so both can be used together without layout side effects.
Subtle shadow for minimal depth.
Small shadow for light elevation.
Medium shadow for moderate elevation.
Large shadow for prominent elevation.
Extra large shadow for maximum elevation.
Shadow and border compose together via box-shadow.
---
import Panel from "../Panel.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div
class={stack({
gap: "xl",
direction: "column",
})}
>
<div>
<h3>Extra Small</h3>
<Panel padding="lg" shadow="xs">
<p>Subtle shadow for minimal depth.</p>
</Panel>
</div>
<div>
<h3>Small</h3>
<Panel padding="lg" shadow="sm">
<p>Small shadow for light elevation.</p>
</Panel>
</div>
<div>
<h3>Medium</h3>
<Panel padding="lg" shadow="md">
<p>Medium shadow for moderate elevation.</p>
</Panel>
</div>
<div>
<h3>Large</h3>
<Panel padding="lg" shadow="lg">
<p>Large shadow for prominent elevation.</p>
</Panel>
</div>
<div>
<h3>Extra Large</h3>
<Panel padding="lg" shadow="xl">
<p>Extra large shadow for maximum elevation.</p>
</Panel>
</div>
<div>
<h3>Shadow + Border</h3>
<Panel padding="lg" shadow="md" border="default">
<p>Shadow and border compose together via box-shadow.</p>
</Panel>
</div>
</div>Enable interactive hover states for clickable panels using the interactive prop. The hover state always steps one position deeper within the current emphasis’s surface ramp (hover = +1, active = +2). Tertiary steps within neutral.surface.*, secondary within colorPalette.surface.*, and primary within colorPalette.accent.surface.*. When the resting background is already at the deep end of the ramp, the stepping reverses toward soft so the visual delta stays consistent.
Set interactive to enable hover + active states. The panel gets
a pointer cursor and steps within its surface ramp on hover (+1
step toward deep) and active (+2 steps). Reverses at the high end of the ramp.
Hover me
Hover steps to surface.step.1; active to step.2.
Hover each panel — the hover target is always one step deeper in the ramp, regardless of where the resting background sits.
surface.soft → hover: step.1
surface.step.1 → hover: step.2
surface.step.2 → hover: step.3
surface.step.3 → hover: step.2 (reversed)
surface.deep → hover: step.3 (reversed)
transparent → hover: step.1
Each emphasis steps within its own concrete ramp. Tertiary uses the neutral surface ramp, secondary uses the colorPalette surface ramp, and primary uses the colorPalette accent surface ramp (anchored on shade 500).
Tertiary
Steps within neutral.surface.*.
Secondary
Steps within colorPalette.surface.*.
Primary
Steps within colorPalette.accent.surface.*.
Translucent + interactive — backdrop blur with hover feedback.
Frosted glass with hover
Hover to see the interactive state on the blurred panel.
---
import Panel from "../Panel.astro";
import { stack, grid } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";
---
<div class={stack({ gap: "2xl", direction: "column", width: "full" })}>
<!-- Basic interactive -->
<div class={stack({ gap: "md", direction: "column" })}>
<h3>Basic</h3>
<p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
Set <code>interactive</code> to enable hover + active states. The panel gets
a <code>pointer</code> cursor and steps within its surface ramp on hover (+1
step toward deep) and active (+2 steps). Reverses at the high end of the ramp.
</p>
<Panel background="surface.soft" padding="lg" interactive>
<p><strong>Hover me</strong></p>
<p>
Hover steps to <code>surface.step.1</code>; active to <code>step.2</code
>.
</p>
</Panel>
</div>
<!-- Stepping across backgrounds -->
<div class={stack({ gap: "md", direction: "column" })}>
<h3>Stepping adapts to the background</h3>
<p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
Hover each panel — the hover target is always one step deeper in the ramp,
regardless of where the resting background sits.
</p>
<div class={grid({ columns: 2, gap: "md" })}>
<Panel background="surface.soft" padding="lg" interactive>
<p><strong>surface.soft</strong> → hover: step.1</p>
</Panel>
<Panel background="surface.step.1" padding="lg" interactive>
<p><strong>surface.step.1</strong> → hover: step.2</p>
</Panel>
<Panel background="surface.step.2" padding="lg" interactive>
<p><strong>surface.step.2</strong> → hover: step.3</p>
</Panel>
<Panel background="surface.step.3" padding="lg" interactive>
<p><strong>surface.step.3</strong> → hover: step.2 (reversed)</p>
</Panel>
<Panel background="surface.deep" padding="lg" interactive>
<p><strong>surface.deep</strong> → hover: step.3 (reversed)</p>
</Panel>
<Panel background="transparent" padding="lg" interactive border="default">
<p><strong>transparent</strong> → hover: step.1</p>
</Panel>
</div>
</div>
<!-- Per-emphasis behavior -->
<div class={stack({ gap: "md", direction: "column" })}>
<h3>Per-emphasis</h3>
<p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
Each emphasis steps within its own concrete ramp.
<strong>Tertiary</strong> uses the neutral surface ramp,
<strong>secondary</strong> uses the colorPalette surface ramp, and
<strong>primary</strong> uses the colorPalette <em>accent</em> surface ramp
(anchored on shade 500).
</p>
<div class={grid({ columns: 3, gap: "md" })}>
<Panel emphasis="tertiary" feedback="primary" padding="lg" interactive>
<p><strong>Tertiary</strong></p>
<p class={css({ fontSize: "sm" })}>Steps within neutral.surface.*.</p>
</Panel>
<Panel
emphasis="secondary"
feedback="primary"
padding="lg"
interactive
border="default"
>
<p><strong>Secondary</strong></p>
<p class={css({ fontSize: "sm" })}>
Steps within colorPalette.surface.*.
</p>
</Panel>
<Panel emphasis="primary" feedback="primary" padding="lg" interactive>
<p><strong>Primary</strong></p>
<p class={css({ fontSize: "sm" })}>
Steps within colorPalette.accent.surface.*.
</p>
</Panel>
</div>
</div>
<!-- Translucent over a busy backdrop -->
<div class={stack({ gap: "md", direction: "column" })}>
<h3>Translucent</h3>
<p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
Translucent + interactive — backdrop blur with hover feedback.
</p>
<div
class={css({
position: "relative",
padding: "md",
borderRadius: "md",
overflow: "hidden",
background:
"linear-gradient(135deg, token(colors.primary) 0%, token(colors.primary.surface) 50%, token(colors.primary) 100%)",
})}
>
<svg
class={css({
position: "absolute",
top: "0",
left: "0",
width: "full",
height: "full",
})}
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="20%" cy="30%" r="60" fill="var(--colors-primary-surface)"
></circle>
<circle
cx="80%"
cy="60%"
r="80"
fill="var(--colors-primary-border-muted)"></circle>
<rect
x="40%"
y="10%"
width="100"
height="100"
fill="var(--colors-primary-border)"
rx="15"></rect>
<circle cx="60%" cy="80%" r="50" fill="var(--colors-primary-hover)"
></circle>
</svg>
<Panel
translucent
padding="md"
radius="xs"
interactive
class={css({ position: "relative", zIndex: "1" })}
>
<p><strong>Frosted glass with hover</strong></p>
<p>Hover to see the interactive state on the blurred panel.</p>
</Panel>
</div>
</div>
</div>Control the internal spacing of a panel using the padding prop. Panels default to md padding.
No padding
Small padding
Medium padding (default)
Large padding
---
import Panel from "../Panel.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "md", direction: "column", width: "full" })}>
<Panel background="surface.step.2" padding="none" border="default">
<p>No padding</p>
</Panel>
<Panel background="surface.step.2" padding="sm" border="default">
<p>Small padding</p>
</Panel>
<Panel background="surface.step.2" padding="md" border="default">
<p>Medium padding (default)</p>
</Panel>
<Panel background="surface.step.2" padding="lg" border="default">
<p>Large padding</p>
</Panel>
</div>Control corner rounding with the radius prop. Ranges from none to full.
No border radius
Small border radius
Medium border radius
Large border radius
Extra large border radius (default)
Full border radius
---
import Panel from "../Panel.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "md", direction: "column", width: "full" })}>
<Panel background="surface.deep" padding="md" radius="none" border="bold">
<p>No border radius</p>
</Panel>
<Panel background="surface.deep" padding="md" radius="sm" border="bold">
<p>Small border radius</p>
</Panel>
<Panel background="surface.deep" padding="md" radius="md" border="bold">
<p>Medium border radius</p>
</Panel>
<Panel background="surface.deep" padding="md" radius="lg" border="bold">
<p>Large border radius</p>
</Panel>
<Panel background="surface.deep" padding="md" radius="xl" border="bold">
<p>Extra large border radius (default)</p>
</Panel>
<Panel background="surface.deep" padding="md" radius="full" border="bold">
<p>Full border radius</p>
</Panel>
</div>Use radius="inner" on a nested Panel to keep its corners visually concentric with the parent’s curve. The child reads the parent’s exported --panel-out-radius and --panel-out-padding custom properties and renders max(xs, parent_radius - parent_padding). No JavaScript, no observers — pure CSS.
Works correctly one level deep. For deeper nesting, set borderRadius directly via passThrough.root.style.borderRadius using the precomputed inner.<radius>.<padding> semantic token (e.g. "inner.2xl.md").
radius="inner"
The child on the left uses the default radius="xl"; its
corners protrude past the parent's. The child on the right uses
radius="inner" and stays visually concentric.
Default radius="xl"
Corners don't match the parent's curve.
radius="inner"
Auto-fits the parent's curve.
The child has identical props in every panel below — only the parent's
radius and padding change.
Parent: radius="lg", padding="xs"
Parent: radius="2xl", padding="md"
Parent: radius="4xl", padding="lg"
xs
When parent_radius − parent_padding would go below the
xs radius, the formula clamps. This keeps inner corners from collapsing
to a sharp edge.
Parent: radius="sm", padding="lg" → child clamps
to xs.
---
import Panel from "../Panel.astro";
import { stack, grid } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";
---
<!--
`radius="inner"` computes max(xs, parent_radius − parent_padding) by reading
the parent panel's exported `--panel-out-radius` / `--panel-out-padding`
custom properties. Use it on the child to keep visually-concentric corners
no matter what radius / padding the parent uses.
-->
<div class={stack({ gap: "2xl", direction: "column", width: "full" })}>
<!-- Side-by-side: with vs without inner radius -->
<div class={stack({ gap: "md", direction: "column" })}>
<h3>With vs without <code>radius="inner"</code></h3>
<p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
The child on the left uses the default <code>radius="xl"</code>; its
corners protrude past the parent's. The child on the right uses
<code>radius="inner"</code> and stays visually concentric.
</p>
<div class={grid({ columns: 2, gap: "lg" })}>
<Panel padding="sm" radius="2xl" border="bold">
<Panel background="surface.deep" padding="md" border="bold">
<p class={css({ fontSize: "sm" })}>
<strong>Default radius="xl"</strong>
</p>
<p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
Corners don't match the parent's curve.
</p>
</Panel>
</Panel>
<Panel padding="sm" radius="2xl" border="bold">
<Panel
background="surface.deep"
padding="md"
radius="inner"
border="bold"
>
<p class={css({ fontSize: "sm" })}>
<strong>radius="inner"</strong>
</p>
<p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
Auto-fits the parent's curve.
</p>
</Panel>
</Panel>
</div>
</div>
<!-- Adapts to every parent radius/padding combo -->
<div class={stack({ gap: "md", direction: "column" })}>
<h3>Adapts to any parent radius + padding</h3>
<p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
The child has identical props in every panel below — only the parent's
<code>radius</code> and <code>padding</code> change.
</p>
<div class={stack({ gap: "md", direction: "column" })}>
<Panel padding="xs" radius="lg" border="bold">
<Panel
background="surface.deep"
padding="md"
radius="inner"
border="bold"
>
<p class={css({ fontSize: "sm" })}>
Parent: <code>radius="lg"</code>, <code>padding="xs"</code>
</p>
</Panel>
</Panel>
<Panel padding="md" radius="2xl" border="bold">
<Panel
background="surface.deep"
padding="md"
radius="inner"
border="bold"
>
<p class={css({ fontSize: "sm" })}>
Parent: <code>radius="2xl"</code>, <code>padding="md"</code>
</p>
</Panel>
</Panel>
<Panel padding="lg" radius="4xl" border="bold">
<Panel
background="surface.deep"
padding="md"
radius="inner"
border="bold"
>
<p class={css({ fontSize: "sm" })}>
Parent: <code>radius="4xl"</code>, <code>padding="lg"</code>
</p>
</Panel>
</Panel>
</div>
</div>
<!-- xs floor: when the formula would drop below xs, it clamps to xs -->
<div class={stack({ gap: "md", direction: "column" })}>
<h3>Lower bound: clamps to <code>xs</code></h3>
<p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
When <code>parent_radius − parent_padding</code> would go below the
<code>xs</code> radius, the formula clamps. This keeps inner corners from collapsing
to a sharp edge.
</p>
<Panel padding="lg" radius="sm" border="bold">
<Panel
background="surface.deep"
padding="md"
radius="inner"
border="bold"
>
<p class={css({ fontSize: "sm" })}>
Parent: <code>radius="sm"</code>, <code>padding="lg"</code> → child clamps
to <code>xs</code>.
</p>
</Panel>
</Panel>
</div>
</div>radius="inner" works one level deep. For deeper nesting, pick from the precomputed inner.<radius>.<padding> tokens via passThrough.root.style.borderRadius.
Parent radius="lg", padding="xs" → child radius="inner".
Parent radius="2xl", padding="md" → child radius="inner".
Parent radius="3xl", padding="md" → child radius="inner".
Parent radius="5xl", padding="md" → child radius="inner".
Parent radius="full" — use explicit "full" since inner would clamp to xs.
---
import Panel from "../Panel.astro";
import { stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";
---
<div class={stack({ gap: "xl", direction: "column", width: "full" })}>
<!--
radius="inner" auto-computes max(xs, parent_radius - parent_padding)
from the parent panel's exported --panel-out-radius / --panel-out-padding
custom properties. Works one level deep.
-->
<Panel padding="xs" radius="lg" border="bold">
<Panel background="surface.deep" padding="md" radius="inner" border="bold">
<p class={css({ fontSize: "sm", color: "fg.subtle" })}>
Parent radius="lg", padding="xs" → child radius="inner".
</p>
</Panel>
</Panel>
<Panel padding="md" radius="2xl" border="bold">
<Panel background="surface.deep" padding="md" radius="inner" border="bold">
<p class={css({ fontSize: "sm", color: "fg.subtle" })}>
Parent radius="2xl", padding="md" → child radius="inner".
</p>
</Panel>
</Panel>
<Panel padding="md" radius="3xl" border="bold">
<Panel background="surface.deep" padding="md" radius="inner" border="bold">
<p class={css({ fontSize: "sm", color: "fg.subtle" })}>
Parent radius="3xl", padding="md" → child radius="inner".
</p>
</Panel>
</Panel>
<Panel padding="md" radius="5xl" border="bold">
<Panel background="surface.deep" padding="md" radius="inner" border="bold">
<p class={css({ fontSize: "sm", color: "fg.subtle" })}>
Parent radius="5xl", padding="md" → child radius="inner".
</p>
</Panel>
</Panel>
<Panel padding="md" radius="full" border="bold">
<Panel background="surface.deep" padding="md" radius="full" border="bold">
<p class={css({ fontSize: "sm", color: "fg.subtle" })}>
Parent radius="full" — use explicit "full" since inner would clamp to
xs.
</p>
</Panel>
</Panel>
</div>Use the as prop to change the HTML element rendered by the panel. This enables semantic markup — render as <section>, <article>, <nav>, or any other HTML element while preserving all Panel styling and behavior. The as prop accepts any valid HTML tag name.
Default panel renders as a div element.
Renders as a semantic section element.
Renders as an article element for self-contained content.
Renders as a main element for primary page content.
---
import Panel from "../Panel.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div
class={stack({
gap: "xl",
direction: "column",
})}
>
<div>
<h3>div (default)</h3>
<Panel padding="lg">
<p>Default panel renders as a div element.</p>
</Panel>
</div>
<div>
<h3>section</h3>
<Panel as="section" padding="lg" border="default">
<p>Renders as a semantic section element.</p>
</Panel>
</div>
<div>
<h3>article</h3>
<Panel as="article" padding="lg" background="surface.step.2">
<p>Renders as an article element for self-contained content.</p>
</Panel>
</div>
<div>
<h3>main</h3>
<Panel as="main" padding="lg" background="surface.soft" border="muted">
<p>Renders as a main element for primary page content.</p>
</Panel>
</div>
<div>
<h3>aside</h3>
<Panel as="aside" padding="lg" background="surface.deep">
<p>Renders as an aside element for sidebar content.</p>
</Panel>
</div>
<div>
<h3>nav</h3>
<Panel as="nav" padding="lg" background="transparent" border="default">
<p>Renders as a nav element for navigation.</p>
</Panel>
</div>
</div>The passThrough prop provides two escape hatches for advanced customization: style accepts any Panda CSS SystemStyleObject applied directly to the panel root, and props forwards arbitrary HTML attributes. Use style for layout overrides like maxWidth or responsive constraints, and props for accessibility attributes like role, aria-label, or aria-live.
Centered with Max Width
Use passThrough.root.style to apply Panda CSS styles directly
to the panel root — here constraining width and centering the panel.
This action cannot be undone. All data will be permanently removed.
Your session expires in 5 minutes
Use passThrough.root.props to set HTML attributes — here
role="alert" and aria-live="polite" for screen reader
announcements.
---
import Panel from "../Panel.astro";
import Button from "@pindoba/astro-button";
import { css } from "@pindoba/styled-system/css";
import { stack, flex } from "@pindoba/styled-system/patterns";
---
<div
class={stack({
gap: "xl",
direction: "column",
})}
>
<!-- Custom Width via PassThrough -->
<div>
<h3>Custom Width</h3>
<Panel
background="surface.step.2"
padding="lg"
passThrough={{
root: {
style: css.raw({
maxWidth: "480px",
margin: "0 auto",
}),
},
}}
>
<p><strong>Centered with Max Width</strong></p>
<p>
Use <code>passThrough.root.style</code> to apply Panda CSS styles directly
to the panel root — here constraining width and centering the panel.
</p>
</Panel>
</div>
<!-- Panel as Card with Action -->
<div>
<h3>Panel as Card</h3>
<Panel background="surface.step.2" padding="lg" border="default">
<div class={stack({ gap: "md" })}>
<div>
<h4 class={css({ margin: "0", marginBottom: "2xs" })}>
Delete workspace
</h4>
<p class={css({ margin: "0", color: "fg.subtle", fontSize: "md" })}>
This action cannot be undone. All data will be permanently removed.
</p>
</div>
<div class={flex({ gap: "sm", justify: "flex-end" })}>
<Button emphasis="secondary" background="surface.deep">Cancel</Button>
<Button emphasis="primary" feedback="danger">Delete</Button>
</div>
</div>
</Panel>
</div>
<!-- Panel with ARIA Attributes -->
<div>
<h3>Accessible Alert</h3>
<Panel
background="surface.soft"
feedback="warning"
padding="lg"
border="default"
passThrough={{
root: {
props: {
role: "alert",
"aria-live": "polite",
"aria-label": "Session expiry warning",
},
},
}}
>
<p><strong>Your session expires in 5 minutes</strong></p>
<p>
Use <code>passThrough.root.props</code> to set HTML attributes — here
<code>role="alert"</code> and <code>aria-live="polite"</code> for screen reader
announcements.
</p>
</Panel>
</div>
</div>| prop | type | default | req | description |
|---|---|---|---|---|
| as | "div""section""article""aside""main""header""footer""nav""dialog""form""fieldset""ul""ol""li""a""button""label""span" | "div" | HTML element to render. Curated to container-like tags so semantic intent stays clear (no html/script/style/etc). | |
| background | "surface.soft""surface.step.1""surface.step.2""surface.step.3""surface.deep""transparent" | "surface.soft" | Surface level for the panel background. Surface 1 is the lightest and surface 5 is the darkest. Use transparent for no background. | |
| emphasis | "primary""secondary""tertiary" | "tertiary" | Controls the visual weight of the panel. "tertiary" (default) uses a neutral surface (regardless of feedback) with feedback-colored border and accent text — the most subtle option. "secondary" uses pa…
Controls the visual weight of the panel. "tertiary" (default) uses a neutral surface (regardless of feedback) with feedback-colored border and accent text — the most subtle option. "secondary" uses palette-aware colorPalette.surface tints. "primary" fills with the feedback color's accent.surface ramp (anchored on shade 500) and uses contrast text. Type Default Required | |
| translucent | boolean | false | Apply a frosted glass effect with backdrop blur. Works with surface backgrounds. | |
| feedback | "neutral""primary""success""warning""danger" | "neutral" | Semantic feedback color scheme. Defaults to neutral (gray). Each feedback color applies its corresponding color tokens for text, background, and border. | |
| padding | "none""5xs""4xs""3xs""2xs""xs""sm""md""lg""xl""2xl""3xl""4xl""5xl""6xl""7xl""8xl""9xl""10xl""11xl" | "md" | Internal padding size. Accepts the full spacing scale from none to 11xl. | |
| radius | "none""2xs""xs""sm""md""lg""xl""2xl""3xl""4xl""5xl""6xl""full""inner" | "xl" | Border radius size. Accepts the full radius scale from none to 6xl, plus full for fully rounded corners, plus inner for the auto-computed concentric-corner radius derived from the parent panel. | |
| border | "none""default""bold""muted""accent" | "none" | Box-shadow ring border. none: no border. default: standard border. bold: stronger emphasis using a bolder color. muted: subtle separation. accent: feedback-colored. Uses box-shadow so it does not affe…
Box-shadow ring border. none: no border. default: standard border. bold: stronger emphasis using a bolder color. muted: subtle separation. accent: feedback-colored. Uses box-shadow so it does not affect layout dimensions and composes with the shadow prop. Type Default Required | |
| shadow | "none""xs""sm""md""lg""xl" | "none" | Drop shadow applied to the panel. Composes with border via box-shadow so both can be used together. | |
| interactive | boolean | false | Enable hover and active effects with cursor pointer. | |
| passThrough | { root?: { style?: SystemStyleObject; props?: HTMLAttributes<HTMLElement> & Record<string, unknown> } } | undefined | Custom styling and props for panel elements | |
| ...rest | HTMLAttributes<HTMLElement> | - | Standard HTML div attributes |
Panel is the foundational surface used by Card, Dialog, Alert, Badge, Banner, Input, and many other components. When you build a new component that should look like a panel — and most surface-like components should — wire it up through connectPanel() instead of redeclaring the variants.
PanelInheritedProps on your prop typePanelInheritedProps carries every Panel visual prop (background, feedback, emphasis, translucent, padding, radius + per-side variants, border, shadow, interactive) plus as and passThrough. Extending it gives your component the full Panel API surface for free.
import type { PanelInheritedProps } from "@pindoba/core-panel";
export interface SurfaceBaseProps extends PanelInheritedProps {
// your component's own props go here
size?: "sm" | "md" | "lg";
}
If your component has its own HTML attribute type that overlaps with Panel’s props, use OmitPanelProps<T> to strip them cleanly.
connectPanel() from your connect functionPass your component’s slot-root styles via inheritedStyle, and any static attrs (e.g. role) via inheritedProps. The end user’s passThrough flows through untouched — connectPanel handles the merge.
import { connectPanel } from "@pindoba/core-panel";
import { surfaceStyles } from "@pindoba/styles-surface";
export function connectSurface(options: ConnectSurfaceOptions) {
const { size = "md", passThrough, class: className, ...rest } = options;
const slot = surfaceStyles.raw({ size });
return connectPanel({
dataComponent: "surface",
...rest,
inheritedStyle: slot.root,
passThrough,
class: className,
});
}
Once your component is wired up, here’s the precedence Panel guarantees — useful to remember when debugging or designing overrides:
Class composition (low → high specificity):
background, feedback, etc.)inheritedStylepassThrough.root.styleclass="..." string (appended last)Root attributes (low → high precedence):
data-component (your dataComponent option) and data-slot="root"dataAttrs (your dynamic data-* attrs)inheritedProps (your static attrs)...rest)passThrough.root.props (always wins)This means: end users can override anything your component sets without you having to plumb each attr individually. Set sane defaults via inheritedProps; trust users to override when they need to.
connectCard is the canonical example: it adds a size prop, derives a default radius, layers its own slot styles, and delegates everything else to connectPanel.