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
activeEmphasis
borderActive
active
reserveBorderSpace
Headline
Emphasis
Feedback
Background
Translucent
Interactive

Background Styles

Panel supports five surface levels (surface.peak through surface.ground) 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.peak" 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.hill" padding="lg">
      <p>Slightly darker surface for subtle layering.</p>
    </Panel>
  </div>

  <!-- Surface.3 (mid) -->
  <div>
    <h3>Surface 3</h3>
    <Panel background="surface.base" padding="lg">
      <p>Mid-tone surface for visual prominence.</p>
    </Panel>
  </div>

  <!-- Surface.4 -->
  <div>
    <h3>Surface 4</h3>
    <Panel background="surface.valley" 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.ground" 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.peak

Default layer.

surface.base

Mid-tone palette surface.

surface.ground

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 backgroundprop still takes surface.* values — each value picks a different step within the accent ramp.

Default (surface.peak → shade 500)

The brand fill.

surface.hill

One step deeper than the brand.

surface.base

Two steps deeper.

surface.ground

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.peak</strong></p>
        <p class={css({ fontSize: "sm" })}>Default layer.</p>
      </Panel>
      <Panel
        emphasis="secondary"
        feedback="primary"
        background="surface.base"
        padding="lg"
        border="default"
      >
        <p><strong>surface.base</strong></p>
        <p class={css({ fontSize: "sm" })}>Mid-tone palette surface.</p>
      </Panel>
      <Panel
        emphasis="secondary"
        feedback="primary"
        background="surface.ground"
        padding="lg"
        border="default"
      >
        <p><strong>surface.ground</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.peak → shade 500)</strong></p>
        <p class={css({ fontSize: "sm" })}>The brand fill.</p>
      </Panel>
      <Panel
        emphasis="primary"
        feedback="primary"
        background="surface.hill"
        padding="lg"
      >
        <p><strong>surface.hill</strong></p>
        <p class={css({ fontSize: "sm" })}>One step deeper than the brand.</p>
      </Panel>
      <Panel
        emphasis="primary"
        feedback="primary"
        background="surface.base"
        padding="lg"
      >
        <p><strong>surface.base</strong></p>
        <p class={css({ fontSize: "sm" })}>Two steps deeper.</p>
      </Panel>
      <Panel
        emphasis="primary"
        feedback="primary"
        background="surface.ground"
        padding="lg"
      >
        <p><strong>surface.ground</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.peak" 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.peak" 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.peak" 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.peak" 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.hill; 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.peak → hover: step.1

surface.hill → hover: step.2

surface.base → hover: step.3

surface.valley → hover: step.2 (reversed)

surface.ground → 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, andprimary 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.peak" padding="lg" interactive>
      <p><strong>Hover me</strong></p>
      <p>
        Hover steps to <code>surface.hill</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.peak" padding="lg" interactive>
        <p><strong>surface.peak</strong> → hover: step.1</p>
      </Panel>
      <Panel background="surface.hill" padding="lg" interactive>
        <p><strong>surface.hill</strong> → hover: step.2</p>
      </Panel>
      <Panel background="surface.base" padding="lg" interactive>
        <p><strong>surface.base</strong> → hover: step.3</p>
      </Panel>
      <Panel background="surface.valley" padding="lg" interactive>
        <p><strong>surface.valley</strong> → hover: step.2 (reversed)</p>
      </Panel>
      <Panel background="surface.ground" padding="lg" interactive>
        <p><strong>surface.ground</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>

Active Panels

Where interactive says a panel can be clicked, active says it is the current choice in a set — the current navigation item, a checked radio in button appearance, a selected choice card.

Combined with interactive, hover and press restart their stepping from the active baseline instead of the resting one — a chosen row still answers the pointer rather than sliding back into the unchosen ramp. Without interactive, an active panel is inert under the pointer.

How loud: activeEmphasis

activeEmphasis controls the state’s intensity. It defaults to the panel’s own emphasis, so the chosen state lands on the same ramp as the resting look — a tertiary panel gets a neutral tint, a primary one travels the accent ramp. Set it explicitly only when the active item should be louder (or quieter) than the panel itself.

activeEmphasisActive surfaceText / borderSurface Δ
"secondary"tint on colorPalette.surface.*text.bold / border~10 units
"tertiary"tint on neutral.surface.* (colorless)neutral.text.bold / neutral.border~7 units
"primary"fill on colorPalette.accent.surface.*contrast scale / border.contrast.bold~150 units

The two tints shift the surface two steps toward deep (reversing toward soft when +2 would overflow), one more than interactive uses for hover so “chosen” doesn’t read as “hovered”. Be aware how small those steps are: measured on the shipped dark theme a 2-step move is only ~7–18 RGB units. Since the default follows emphasis, a default (tertiary) panel therefore gets the subtlest state — on a dense list, override with activeEmphasis="primary" so the chosen row reads at a glance. transparent is the exception at every level: it gains a fill outright, which is itself the state change.

<!-- follows emphasis — neutral panel, neutral tint -->
<Panel interactive active></Panel>

<!-- override: quiet panel, loud chosen state -->
<Panel interactive active activeEmphasis="primary"></Panel>

activeEmphasis="primary" on a panel that is already an accent fill (emphasis="primary") can’t simply become one, so it travels to the far end of the accent ramp instead and the border.contrast.bold ring carries the state.

active takes three values, differing only in what drives the state:

ValueDriven byUse for
truedata-panel-active="true", baked by the connectComponents whose framework state knows the answer at render time
"checked":has(input:checked)A wrapped native radio/checkbox — self-driving, zero JS, nothing to re-render
"current"[aria-current] (any value but false)Links already carrying aria-current="page" for accessibility

:has(input:checked) is deliberately opt-in rather than always-on, so a panel that merely contains a form with a checked box doesn’t light up as chosen.

The attribute is data-panel-active, not a bare data-active. Panda compiles its _active (pressed) condition to &:is(:active, [data-active]), so a plain data-active would trigger every pressed rule in the recipe and an active panel would render permanently held down. Query [data-panel-active] in tests and custom CSS.

active — the chosen one in a set

interactive says a panel can be clicked; active says it is the current choice. The active surface sits two steps deeper in the panel's own ramp — two rather than one, so it stays distinguishable from a mere hover — and text promotes to bold with the border lifting to the default line.

With interactive, hover and press restart their stepping from the active baseline, so the chosen row still answers the pointer instead of sliding back into the unchosen ramp.

active="checked" — driven by the DOM, zero JS

Wrap a native radio or checkbox and the panel follows :has(input:checked) — no state to thread and, in Astro, no client-side JS at all. Click through the cards below: the selection is pure CSS. This is the arm a checkbox/radio button appearance or a selectable card wants. It's opt-in precisely so a panel that merely contains a checked box doesn't light up as chosen.

active="current" — free for links

Matches [aria-current] (any value but false), so a link that already carries aria-current="page" for accessibility gets the visual state with no extra prop. Rendered as buttons here — it's the attribute, not the tag, that drives the state.

activeEmphasis — how loud the state is

Independent of the panel's own emphasis, which still owns the resting look. secondary (the default) tints the feedback surface ramp, tertiary tints the neutral ramp, and primaryfills with the feedback accent ramp and flips text to the contrast scale. The tints move the surface only ~7–18 RGB units — on a dense list, activeEmphasis="primary" (~150) is the one that reads at a glance. Each row below is resting / active.

activeEmphasis="secondary"

resting
active

activeEmphasis="tertiary"

resting
active

activeEmphasis="primary"

resting
active

The derived default — active follows emphasis

activeEmphasis defaults to the panel's own emphasis, so with no override the chosen state lands on the same ramp as the resting look — tertiary on neutral.surface.*, secondary on colorPalette.surface.*, and primary travelling colorPalette.accent.surface.*. Each pair below is resting / active, with no activeEmphasis set.

tertiary — resting
tertiary — active
secondary — resting
secondary — active
primary — resting
primary — active

Active without interactive

A read-only "this is the current one" marker. It paints the active surface but stays inert under the pointer — hover and press collapse back onto the resting active background, so nothing moves.

Current selection

Hover me — nothing happens.

---
import Panel from "../Panel.astro";
import { stack, grid } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";

const pages = ["Overview", "Components", "Tokens", "Changelog"];
const currentPage = "Components";
const plans = ["Starter", "Pro", "Enterprise"];
const emphases = ["tertiary", "secondary", "primary"] as const;
const levels = ["secondary", "tertiary", "primary"] as const;
---

<div class={stack({ gap: "2xl", direction: "column", width: "full" })}>
  <!-- Prop-driven arm -->
  <div class={stack({ gap: "md", direction: "column" })}>
    <h3>active — the chosen one in a set</h3>
    <p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
      <code>interactive</code> says a panel <em>can</em> be clicked; <code
        >active</code
      > says it <em>is</em> the current choice. The active surface sits two steps
      deeper in the panel's own ramp — two rather than one, so it stays distinguishable
      from a mere hover — and text promotes to <code>bold</code> with the border lifting
      to the default line.
    </p>
    <p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
      With <code>interactive</code>, hover and press <strong>restart</strong> their
      stepping from the active baseline, so the chosen row still answers the pointer
      instead of sliding back into the unchosen ramp.
    </p>
    <div class={stack({ gap: "2xs", direction: "column" })}>
      {
        pages.map((page) => (
          <Panel
            as="button"
            background="transparent"
            padding="sm"
            radius="md"
            interactive
            active={page === currentPage}
          >
            {page}
          </Panel>
        ))
      }
    </div>
  </div>

  <!-- DOM-driven: :has(input:checked) -->
  <div class={stack({ gap: "md", direction: "column" })}>
    <h3><code>active="checked"</code> — driven by the DOM, zero JS</h3>
    <p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
      Wrap a native radio or checkbox and the panel follows <code
        >:has(input:checked)</code
      > — no state to thread and, in Astro, no client-side JS at all. Click through
      the cards below: the selection is pure CSS. This is the arm a checkbox/radio
      button appearance or a selectable card wants. It's opt-in precisely so a panel
      that merely <em>contains</em> a checked box doesn't light up as chosen.
    </p>
    <div class={grid({ columns: 3, gap: "md" })}>
      {
        plans.map((plan) => (
          <Panel
            as="label"
            background="transparent"
            padding="lg"
            interactive
            active="checked"
            class={css({ display: "flex", alignItems: "center", gap: "sm" })}
          >
            <input
              type="radio"
              name="astro-panel-demo-plan"
              value={plan}
              checked={plan === "Pro"}
            />
            <strong>{plan}</strong>
          </Panel>
        ))
      }
    </div>
  </div>

  <!-- DOM-driven: aria-current -->
  <div class={stack({ gap: "md", direction: "column" })}>
    <h3><code>active="current"</code> — free for links</h3>
    <p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
      Matches <code>[aria-current]</code> (any value but <code>false</code>), so
      a link that already carries <code>aria-current="page"</code> for accessibility
      gets the visual state with no extra prop. Rendered as buttons here — it's the
      attribute, not the tag, that drives the state.
    </p>
    <div class={stack({ gap: "2xs", direction: "column" })}>
      <Panel
        as="button"
        padding="sm"
        radius="md"
        interactive
        active="current"
        aria-current="page"
      >
        Docs <span class={css({ color: "panel.text.muted" })}
          >— aria-current="page"</span
        >
      </Panel>
      <Panel as="button" padding="sm" radius="md" interactive active="current">
        Blog <span class={css({ color: "panel.text.muted" })}
          >— no aria-current</span
        >
      </Panel>
    </div>
  </div>

  <!-- activeEmphasis: how loud -->
  <div class={stack({ gap: "md", direction: "column" })}>
    <h3><code>activeEmphasis</code> — how loud the state is</h3>
    <p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
      Independent of the panel's own <code>emphasis</code>, which still owns the
      resting look. <strong>secondary</strong> (the default) tints the feedback surface
      ramp, <strong>tertiary</strong> tints the neutral ramp, and <strong
        >primary</strong
      >
      fills with the feedback <em>accent</em> ramp and flips text to the contrast
      scale. The tints move the surface only ~7–18 RGB units — on a dense list, <code
        >activeEmphasis="primary"</code
      > (~150) is the one that reads at a glance. Each row below is resting / active.
    </p>
    <div class={grid({ columns: 3, gap: "md" })}>
      {
        levels.map((level) => (
          <div class={stack({ gap: "2xs", direction: "column" })}>
            <p class={css({ fontSize: "xs", color: "panel.text.muted" })}>
              activeEmphasis="{level}"
            </p>
            <Panel feedback="primary" padding="md" interactive>
              resting
            </Panel>
            <Panel
              feedback="primary"
              padding="md"
              interactive
              active
              activeEmphasis={level}
            >
              active
            </Panel>
          </div>
        ))
      }
    </div>
  </div>

  <!-- Per-emphasis -->
  <div class={stack({ gap: "md", direction: "column" })}>
    <h3>The derived default — active follows <code>emphasis</code></h3>
    <p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
      <code>activeEmphasis</code> defaults to the panel's own <code
        >emphasis</code
      >, so with no override the chosen state lands on the same ramp as the
      resting look — <strong>tertiary</strong> on <code>neutral.surface.*</code
      >, <strong>secondary</strong> on <code>colorPalette.surface.*</code>, and <strong
        >primary</strong
      > travelling <code>colorPalette.accent.surface.*</code>. Each pair below
      is resting / active, with no <code>activeEmphasis</code> set.
    </p>
    <div class={grid({ columns: 3, gap: "md" })}>
      {
        emphases.map((emphasis) => (
          <div class={stack({ gap: "2xs", direction: "column" })}>
            <Panel
              emphasis={emphasis}
              feedback="primary"
              padding="md"
              interactive
            >
              <strong>{emphasis}</strong> — resting
            </Panel>
            <Panel
              emphasis={emphasis}
              feedback="primary"
              padding="md"
              interactive
              active
            >
              <strong>{emphasis}</strong> — active
            </Panel>
          </div>
        ))
      }
    </div>
  </div>

  <!-- Non-interactive active -->
  <div class={stack({ gap: "md", direction: "column" })}>
    <h3>Active without <code>interactive</code></h3>
    <p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
      A read-only "this is the current one" marker. It paints the active surface
      but stays inert under the pointer — hover and press collapse back onto the
      resting active background, so nothing moves.
    </p>
    <Panel padding="lg" active>
      <strong>Current selection</strong>
      <p class={css({ fontSize: "sm" })}>Hover me — nothing happens.</p>
    </Panel>
  </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.base" padding="none" border="default">
    <p>No padding</p>
  </Panel>
  <Panel background="surface.base" padding="sm" border="default">
    <p>Small padding</p>
  </Panel>
  <Panel background="surface.base" padding="md" border="default">
    <p>Medium padding (default)</p>
  </Panel>
  <Panel background="surface.base" 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.ground" padding="md" radius="none" border="bold">
    <p>No border radius</p>
  </Panel>
  <Panel background="surface.ground" padding="md" radius="sm" border="bold">
    <p>Small border radius</p>
  </Panel>
  <Panel background="surface.ground" padding="md" radius="md" border="bold">
    <p>Medium border radius</p>
  </Panel>
  <Panel background="surface.ground" padding="md" radius="lg" border="bold">
    <p>Large border radius</p>
  </Panel>
  <Panel background="surface.ground" padding="md" radius="xl" border="bold">
    <p>Extra large border radius (default)</p>
  </Panel>
  <Panel background="surface.ground" 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 renders max(xs, parent_radius - parent_padding) computed against its immediate parent panel’s exported --panel-out-radius / --panel-out-padding custom properties. No JavaScript, no observers — pure CSS.

The subtracted inset is the parent’s padding prop, which is the only padding the cascade can see. Padding applied any other way — a class, a passThrough style, or a plain wrapper element between the two panels — contributes nothing, and the child subtracts zero:

<!-- ✗ the inset is invisible to the cascade: the child renders the PARENT's corner -->
<Panel radius="xl" padding="none" class={css({ padding: "md" })}>
  <Panel radius="inner"></Panel>
</Panel>

<!-- ✓ -->
<Panel radius="xl" padding="md">
  <Panel radius="inner"></Panel>
</Panel>

That failure is silent and reads as “inner is broken”: max(xs, R - 0) is R, so the child traces its parent’s curve instead of nesting inside it. It’s also the correct answer when a child really is flush to the edge — with padding="none" and no inset, matching corners is concentric. Use Padding an element that isn’t a Panel when the inset lives somewhere the cascade can’t reach.

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 usesradius="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'sradius 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 thexs 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.ground" 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.ground"
          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.ground"
          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.ground"
          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.ground"
          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.ground"
        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" recurses: each level steps down relative to its immediate parent panel, and a panel that resolves an inner corner republishes the value for its own children — concentric curves to a depth of 8 nested panels (deeper levels stay valid, just slightly rounder than the exact math). The xs floor keeps deep corners from collapsing to a sharp edge.

Four levels deep: 4xl → inner → inner → inner. Each corner steps down by its parent's padding.

The floor: by the third level 2xl − md − md bottoms out, so this corner clamps at xs instead of going sharp.

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" recurses: every level computes
    max(xs, parent_radius − parent_padding) against its IMMEDIATE parent and
    republishes the resolved value for its own children — concentric curves
    all the way down (depth 8), pure CSS.
  -->

  <Panel padding="lg" radius="4xl" border="bold">
    <Panel background="surface.peak" padding="md" radius="inner" border="bold">
      <Panel
        background="surface.ground"
        padding="sm"
        radius="inner"
        border="bold"
      >
        <Panel
          background="surface.hill"
          padding="sm"
          radius="inner"
          border="bold"
        >
          <p class={css({ fontSize: "sm", color: "fg.subtle" })}>
            Four levels deep: <code>4xl</code> → inner → inner → inner. Each corner
            steps down by its parent's padding.
          </p>
        </Panel>
      </Panel>
    </Panel>
  </Panel>

  <Panel padding="md" radius="2xl" border="bold">
    <Panel background="surface.peak" padding="md" radius="inner" border="bold">
      <Panel
        background="surface.ground"
        padding="md"
        radius="inner"
        border="bold"
      >
        <p class={css({ fontSize: "sm", color: "fg.subtle" })}>
          The floor: by the third level <code>2xl − md − md</code> bottoms out, so
          this corner clamps at <code>xs</code> instead of going sharp.
        </p>
      </Panel>
    </Panel>
  </Panel>

  <Panel padding="md" radius="full" border="bold">
    <Panel background="surface.ground" 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>

Padding an element that isn’t a Panel

Only panels are levels of the cascade. When the padding between a panel and its inner child belongs to something else — a layout <div>, a <ul>, a grid cell — make that element a level too, with the panelNest pattern plus the data-panel-nest attribute the cascade is keyed on:

<script>
  import { panelNest } from "@pindoba/styled-system/patterns";
</script>

<Panel radius="xl" padding="none">
  <div data-panel-nest class={panelNest({ padding: "md" })}>
    <Panel radius="inner"></Panel>
    <!-- corner = xl − md, exactly as if the div were a Panel -->
  </div>
</Panel>

Both parts are required: the vars without the attribute do nothing, since the subtraction runs on [data-panel-nest] elements only.

propdefaulteffect
paddingnoneThe inset children sit behind. Subtracted from this element’s corner, and emitted as a real padding declaration.
radiusinherits chainThis element’s own corner. Omit it for a transparent wrapper — it then resolves through the cascade like radius="inner" would.
setPaddingtrueSet false to publish the var without the padding declaration, when the real padding is asymmetric or already declared elsewhere.

An opted-in element consumes one of the cascade’s 8 levels, the same as a Panel. If the wrapper is yours to change, <Panel radius="inner" padding="md" background="transparent" border="none"> produces an identical corner and needs no attribute — prefer it, and reach for panelNest when the padded element isn’t yours to replace.

To pin a corner with no DOM relationship at all, the precomputed inner.<radius>.<padding> semantic tokens take the two values literally (e.g. borderRadius: "inner.2xl.md" via passThrough.root.style). Nothing cascades — you’re stating the parent’s geometry by hand, so it drifts the moment the parent’s radius or padding changes.

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.base">
      <p>Renders as an article element for self-contained content.</p>
    </Panel>
  </div>

  <div>
    <h3>main</h3>
    <Panel as="main" padding="lg" background="surface.peak" 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.ground">
      <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.base"
      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.base" 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.ground"
            >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.peak"
      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>

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.

props · 28 shown · 28 total
active
boolean"current""checked""current-within"
default false

Mark this as the chosen one in a set (a current nav item, a checked option, a selected card). Where `interactive` says it *can* be clicked, `active` says it *is* the current choice. Use `activeEmphasis` to control how loud the state is. Combined with `interactive`, hover and press restart their stepping from the active baseline rather than falling back to the unchosen ramp. The three values differ only in what drives the state: `true` by the prop (via `data-panel-active`), `"checked"` by a wrapped native input (`:has(input:checked)` — zero JS, nothing to re-render), and `"current"` by an existing `aria-current`.

activeEmphasis
"primary""secondary""tertiary"
default the panel's `emphasis`

How loud the `active` state is. Defaults to the panel's own `emphasis`, so the chosen state lands on the same ramp as the resting look — this prop is the override for when the active item should be louder (or quieter) than the panel itself. `"primary"` fills with the feedback accent ramp and flips text to the contrast scale (~150 RGB units of separation — the option that reads at a glance in a dense list). `"secondary"` tints the feedback surface ramp and `"tertiary"` the neutral ramp; both promote text to `bold` and lift the border to the default line, but move the surface only ~7–18 units.

as
"div""section""article""aside""main""header""footer""nav""dialog""form""fieldset""ul""ol""li""a""button""label""span""kbd"
default "div"

HTML element to render. Curated to container-like tags so semantic intent stays clear (no html/script/style/etc).

background
"surface.peak""surface.hill""surface.base""surface.valley""surface.ground""transparent"

Surface background from the Panel scale (`surface.peak` → `surface.ground`, or `transparent`).

border
"none""bold""default""muted""accent"

Border style: `none`, `default`, `bold`, or `muted`.

borderActive
"none""bold""inherit""default""muted""accent"

No description yet.

borderInteract
"none""bold""default""muted""accent"

Border style applied on hover / focus interaction.

children slot svelte
Snippet

No description yet.

class
unknown

User-supplied `class` string. Appended last via `cx()`.

dataAttrs
Record<string, string | number | boolean | undefined | null>

Extra data attributes to emit on the root (e.g. `{ shape: "circle" }` → `data-shape="circle"`). Values of `undefined`, `null`, or `false` are skipped.

dataComponent
string

Override the default `data-component="panel"` attribute.

element binding svelte
Elementnull

Bindable reference to the rendered element. Typed loosely as `Element` to stay compatible with `svelte:element`'s union return type.

emphasis
"primary""secondary""tertiary"

Visual emphasis / prominence level.

feedback
"primary""neutral""success""warning""danger""inherit"

Semantic color tone: `neutral`, `primary`, `success`, `warning`, `danger`, or `inherit`.

inheritedProps
ElementAttributes

Static HTML/data attrs produced by an inheriting component. Spread before the consumer's `restProps` and `passThrough.root.props`, so both can override them.

inheritedStyle
SystemStyleObject

Styles produced by an inheriting component (e.g. `cardStyles.raw().root`). Merged into the panel root's class between the recipe and the consumer's `passThrough.root.style`, so consumer overrides still win. Inheritors use this instead of funneling their slot styles through `passThrough.root.style` manually.

interactive
boolean

Enable interactive (hover / focus / press) affordances and states.

padding
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""7xl""8xl""4xs""3xs""2xs""9xl""10xl""11xl"

Inner padding from the spacing scale.

passThrough
PanelPassThrough<RootElementAttributes>

Per-slot style and attribute override bag for customizing the panel's elements.

radius
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius override from the spacing scale (or `full` for fully rounded).

radiusBottom
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius for the bottom-left and bottom-right corners.

radiusLeft
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius for the top-left and bottom-left corners.

radiusRight
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius for the top-right and bottom-right corners.

radiusTop
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius for the top-left and top-right corners.

reactive
boolean
default false

Serialize the panel's resolved surface inputs into a `data-panel-config` attribute on the root, enabling runtime surface changes via `updatePanelElement()` (and, for inheritors like Card, `updateCard()`) without a framework runtime — the primary consumer is Astro / vanilla JS, where props don't re-render. Reactive frameworks (Svelte/React/Vue) don't need this: their prop changes re-run the connect already.

reserveBorderSpace
boolean

Keep (or drop) the always-1px transparent border the panel reserves so a border appearing or changing never shifts layout. Computed automatically — it's reserved when a resting `border` is visible, or when `interactive` / `borderInteract` / `active` can change the border at runtime. Set `false` only on a panel that must not occupy that 1px (e.g. a borderless housing frame that would otherwise add 2px around a set).

shadow
"sm""md""lg""xl""none""xs"

Elevation shadow applied to the surface.

translucent
boolean

Apply a frosted-glass effect with backdrop blur over a surface background.

Plus all standard <anchor> HTML attributes.

Type

  • Components
  • Blocks