component

Panel

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.

Monthly revenue

$84,320

Up 12.4% versus last month — driven by stronger Pro plan retention and a healthy bump in annual upgrades.

Updated just now View report →
Monthly revenue

$84,320

Up 12.4% versus last month — driven by stronger Pro plan retention and a healthy bump in annual upgrades.

Updated just now View report →
padding
radius
border
borderInteract
shadow
Headline
Emphasis
Feedback
Background
Translucent
Interactive

Background Styles

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.

Surface 1

Lightest surface — the default and primary background for the application.

Surface 2

Slightly darker surface for subtle layering.

Surface 3

Mid-tone surface for visual prominence.

Surface 4

Darker surface for nested containers or secondary areas.

Surface 5

Darkest surface for recessed areas, wells, or secondary zones.

Transparent

No background — inherits from its parent container.

Translucent

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>

Feedback Colors

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.

Neutral

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.

Primary

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.

Success

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.

Warning

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.

Danger

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>

Emphasis

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.

Tertiary (default)

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.

Secondary

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.

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 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.

Primary across feedback colors

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>

Border Styles

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.

None

No Border

Use border="none" for no border.

Default

Default Border

Standard border using the neutral border token.

Bold

Bold Border

Stronger border weight for greater visual emphasis.

Muted

Muted Border

Lighter border for subtle visual separation.

Border Styles with Feedback Colors

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>

Shadow

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.

Extra Small

Subtle shadow for minimal depth.

Small

Small shadow for light elevation.

Medium

Medium shadow for moderate elevation.

Large

Large shadow for prominent elevation.

Extra Large

Extra large shadow for maximum elevation.

Shadow + Border

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>

Interactive Panels

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.

Basic

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.

Stepping adapts to the background

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

Per-emphasis

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

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>

Padding

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>

Border Radius

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>

Inner radius

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").

With vs without 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.

Adapts to any parent radius + padding

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"

Lower bound: clamps to 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>

Nesting

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>

Polymorphic Rendering

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.

div (default)

Default panel renders as a div element.

section

Renders as a semantic section element.

article

Renders as an article element for self-contained content.

main

Renders as a main element for primary page content.

aside

nav

---
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>

Custom Styling

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.

Custom Width

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.

Panel as Card

Delete workspace

This action cannot be undone. All data will be permanently removed.

Accessible Alert

---
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>

Props

props · 12 total
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…

emphasis

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 "primary" | "secondary" | "tertiary"
Default "tertiary"
Required No
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…

border

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 "none" | "default" | "bold" | "muted" | "accent"
Default "none"
Required No
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

Inheriting from Panel

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.

1. Extend PanelInheritedProps on your prop type

PanelInheritedProps 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.

2. Call connectPanel() from your connect function

Pass 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,
  });
}

3. Merge order (the contract)

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):

  1. Panel recipe variants (background, feedback, etc.)
  2. Your component’s inheritedStyle
  3. End user’s passThrough.root.style
  4. End user’s raw class="..." string (appended last)

Root attributes (low → high precedence):

  1. data-component (your dataComponent option) and data-slot="root"
  2. dataAttrs (your dynamic data-* attrs)
  3. inheritedProps (your static attrs)
  4. User-spread HTML attrs (...rest)
  5. End user’s 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.

4. See it in practice

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.