foundation

Patterns

The Panda CSS patterns and utilities the pindoba preset adds — glass, interactive states, control sizing, concentric radii, baseline rhythm and layout.

A Panda pattern is a function that turns a few typed props into a style object. You import it from @pindoba/styled-system/patterns and call it directly (translucent({ blur: "md" }) returns a class), or spread pattern.raw({...}) into css(), css.raw() or an sva() slot. The pindoba preset defines eight:

PatternFor
translucentFrosted-glass backdrop blur
interactiveStatesHover, active, focus and disabled colors in one call
sizingControl typography, padding and radius from the size scale
controlRowHeight, gap and per-side padding from the Control Var Contract
panelNestMake a plain element one level of the concentric radius cascade
rhythmMeasure a prose slot in baseline lines inside a [data-rhythm] scope
containerCentered max-width page box (pindoba override of the base preset)
gridCSS grid with a token-scale default gap (pindoba override)

Panda’s other base patterns (stack, flex, hstack, center, …) are still available unchanged.

Use the pattern, not a plain helper. Panda’s extractor ignores spreads of ordinary imported objects. A class name is generated, but the published bundle has no CSS for it. Patterns are a special case: their transform runs at codegen. That’s why controlRow and rhythm are patterns and not functions in a styles package.

Translucent

A frosted-glass surface: a backdrop blur, with saturation boosted so colors behind the glass stay vivid. You can get it two ways.

Utility: css({ translucent: "…" })

none
2xs
xs
sm
md
lg
xl

Pattern: translucent({ blur })

translucent({ blur: "md" })
translucent({ blur: "3xl" })
---
import { css } from "@pindoba/styled-system/css";
import { translucent } from "@pindoba/styled-system/patterns";

// Something busy behind the glass, so the blur has edges to soften.
const backdrop = css({
  position: "relative",
  display: "grid",
  placeItems: "center",
  minHeight: "120px",
  borderRadius: "md",
  overflow: "hidden",
  padding: "sm",
  backgroundColor: "neutral.surface.base",
  backgroundImage:
    // Soft color fields plus one crisp disc: an edge for the blur to soften,
    // without high-contrast stripes that strobe behind the glass.
    "radial-gradient(circle at 25% 35%, {colors.warning.accent.surface.base} 0 18%, transparent 19%), radial-gradient(circle at 70% 60%, {colors.primary.accent.surface.hill}, transparent 45%)",
});

const glass = css.raw({
  width: "full",
  paddingBlock: "sm",
  paddingInline: "md",
  borderRadius: "sm",
  borderWidth: "1px",
  borderStyle: "solid",
  borderColor: "neutral.border.muted",
  color: "neutral.text.bold",
  fontFamily: "mono",
  fontSize: "xs",
  textAlign: "center",
});

const grid = css({
  display: "grid",
  gridTemplateColumns: "repeat(auto-fit, minmax(140px, 1fr))",
  gap: "sm",
  width: "full",
});

// Utility: one string value, `none`–`xl`. Each class is a static call so
// Panda extracts every step.
const utilitySteps = [
  {
    value: "none",
    cls: css(glass, { translucent: "none", bg: "neutral.surface.peak/60" }),
  },
  {
    value: "2xs",
    cls: css(glass, { translucent: "2xs", bg: "neutral.surface.peak/60" }),
  },
  {
    value: "xs",
    cls: css(glass, { translucent: "xs", bg: "neutral.surface.peak/60" }),
  },
  {
    value: "sm",
    cls: css(glass, { translucent: "sm", bg: "neutral.surface.peak/60" }),
  },
  {
    value: "md",
    cls: css(glass, { translucent: "md", bg: "neutral.surface.peak/60" }),
  },
  {
    value: "lg",
    cls: css(glass, { translucent: "lg", bg: "neutral.surface.peak/60" }),
  },
  {
    value: "xl",
    cls: css(glass, { translucent: "xl", bg: "neutral.surface.peak/60" }),
  },
];

// Pattern: any `blurs` token, saturate 200%. Pass the fill explicitly.
const patternSteps = [
  {
    label: 'translucent({ blur: "md" })',
    cls: css(
      glass,
      translucent.raw({
        blur: "md",
        backgroundColor: "neutral.surface.peak/60",
      }),
    ),
  },
  {
    label: 'translucent({ blur: "3xl" })',
    cls: css(
      glass,
      translucent.raw({
        blur: "3xl",
        backgroundColor: "neutral.surface.peak/60",
      }),
    ),
  },
];

const heading = css({
  fontSize: "sm",
  fontWeight: "medium",
  color: "neutral.text.bold",
});
const stack = css({ display: "flex", flexDirection: "column", gap: "sm" });
---

<div class={css({ display: "flex", flexDirection: "column", gap: "lg" })}>
  <section class={stack}>
    <h4 class={heading}>
      Utility: <code>css(&#123; translucent: "…" &#125;)</code>
    </h4>
    <div class={grid}>
      {utilitySteps.map((step) => (
        <div class={backdrop}>
          <div class={step.cls}>{step.value}</div>
        </div>
      ))}
    </div>
  </section>

  <section class={stack}>
    <h4 class={heading}>
      Pattern: <code>translucent(&#123; blur &#125;)</code>
    </h4>
    <div class={grid}>
      {patternSteps.map((step) => (
        <div class={backdrop}>
          <div class={step.cls}>{step.label}</div>
        </div>
      ))}
    </div>
  </section>
</div>

The translucent utility is a css() property that takes one string, from none and 2xs to xl. It emits backdrop-filter: blur(<token>) saturate(150%) brightness(100%) and nothing else, so set the fill yourself:

import { css } from "@pindoba/styled-system/css";

<div className={css({ translucent: "md", bg: "neutral.surface.peak/40" })}>
  Glass card
</div>;

The translucent pattern takes blur (any blurs token, none to 5xl) and emits backdropBlur with backdropSaturate: 200% and backdropBrightness: 100%:

import { translucent } from "@pindoba/styled-system/patterns";

<div
  className={translucent({
    blur: "lg",
    backgroundColor: "neutral.surface.peak/40",
  })}
>
  Glass card
</div>;

// in a recipe slot
root: css.raw({ ...translucent.raw({ blur: "md" }) });

Always pass backgroundColor to the pattern. Its built-in default, neutral.surface/80, refers to a token that doesn’t exist, so without an explicit fill the background is dropped. Don’t use the pattern with blur: "none": it then sets saturate and brightness to 0, which turns the backdrop black. Use the none utility value, or leave the pattern off.

For a full glass surface with a border, radius and theme-aware fill, use <Panel translucent> (or any panel-based component). It mixes the background step at 35% opacity and adds an lg blur.

Interactive States

interactiveStates combines a resting color pair with _hover, _active, optional _focusVisibleWithin and _disabled blocks. Start from a preset, override individual states, or pass everything yourself:

import { interactiveStates } from "@pindoba/styled-system/patterns";

// preset: colorPalette fill with contrast text
<button className={interactiveStates({ preset: "primary" })}>Save</button>

// preset + one override
<a className={interactiveStates({ preset: "secondary", bgHover: "neutral.surface.hill" })}>Open</a>

// fully custom; any color prop takes a { base, _light, _dark } object
<div
  className={interactiveStates({
    bgBase: "neutral.surface.peak",
    bgHover: "neutral.surface.base",
    bgActive: "neutral.surface.ground",
    colorBase: "neutral.text.bold",
    withFocusOutline: true,
  })}
/>
PropEffect
presetprimary (colorPalette fill), secondary (neutral surfaces) or ghost (secondary on a transparent base)
bg*: Base, Hover, Active, DisabledBackground per state; overrides the preset
color*: Base, Hover, Active, DisabledText color per state; overrides the preset
withFocusOutlineFocus ring on _focusVisibleWithin (default false)
withDisabledFilterGrayscale + disabled opacity when disabled (default true)

Sizing

sizing maps a size (xs to xl) to the control typography (the control.<size> text style), inline and block padding (control.padX/padY.<size> spacing tokens) and the corner radius (control.<size> radii token). All of these tokens are generated from SIZE_PRESETS, so the pattern holds no table of its own. Any of fontSize, lineHeight, px, py, minHeight and borderRadius can be overridden.

import { sizing } from "@pindoba/styled-system/patterns";

css({ ...sizing.raw({ size: "md" }) });
css({ ...sizing.raw({ size: "md", px: "sm" }) }); // tighter inline padding
sizing · from SIZE_PRESETS
sizetext stylepaddingradius · font
xscontrol.xspx xs NaNpx · py 3xs NaNpxradius sm NaNpx · font xs
smcontrol.smpx sm NaNpx · py 2xs NaNpxradius sm NaNpx · font xs
mdcontrol.mdpx md NaNpx · py xs NaNpxradius md NaNpx · font sm
lgcontrol.lgpx xl NaNpx · py sm NaNpxradius lg NaNpx · font md
xlcontrol.xlpx 3xl NaNpx · py md NaNpxradius xl NaNpx · font lg

A fixed-height control row passes inlinePad: false and spreads controlRow next to it, so the contract owns inline padding and height. See Sizing & Controls.

Control Row

controlRow takes no props. It reads the Control Var Contract variables that the preset publishes on [data-control-size]: height: var(--control-h), gap: var(--control-gap), and padding-inline-start/end from --control-pad-start/end, which change per side when an <Affix> is present.

import { sizing, controlRow } from "@pindoba/styled-system/patterns";

size: {
  md: {
    root: css.raw({
      ...sizing.raw({ size: "md", inlinePad: false }),
      ...controlRow.raw({}),
    }),
  },
},

The connect function must put data-control-size="md" on the same root. Retune the input variables (--control-pad-x-base, --control-gap, …), never --control-pad-start/end. The full model, with a live explorer, is on Sizing & Controls.

Panel Nest

radius="inner" subtracts a panel’s padding to keep nested corners concentric. Padding from a plain <div>, <ul> or grid cell isn’t counted. panelNest makes such an element a real nesting level. It needs the data-panel-nest attribute as well, because the cascade rules are keyed on it.

<div data-panel-nest class={panelNest({ padding: "md" })}>
  <Panel radius="inner">…</Panel>
  <!-- corner = parent − md -->
</div>

Props: padding (spacing token, also emitted as padding unless setPadding: false) and radius (this element’s own corner; if omitted, it inherits the chain). Prefer a real <Panel radius="inner" background="transparent" border="none"> when you control the markup.

Rhythm

rhythm measures a recipe slot in baseline lines, but only inside a [data-rhythm] scope. Outside one it emits nothing. Counts are lines, not tokens, and 0.5 resolves to a half line.

import { rhythm } from "@pindoba/styled-system/patterns";

root:   css.raw({ ...rhythm.raw({ padBlock: 1, padInline: 1, borderW: "1px" }) }),
title:  css.raw({ ...rhythm.raw({ text: true }) }),     // quantise leading
header: css.raw({ ...rhythm.raw({ lines: 1 }) }),       // fixed one-line row

Props: text, textLines, lines, minLines, spaceBefore, padBlock, padInline, gap (block axis only), borderW (subtracted so the border box lands on the raster) and panelPad (republishes the padding to the radius cascade; on by default). Use it on prose surfaces such as panels, cards, banners and alerts, never on a control row: the row already gets its height from --control-h. See Raster.

Container

The base preset’s container pattern with values that work against pindoba’s tokens: maxWidth: breakpoint-2xl and paddingInline: { base: "md", md: "xl" }. Upstream’s values (px: 4/6/8, 8xl) don’t resolve with pindoba’s t-shirt spacing or its 10px root. For page layout, prefer <Container> from the grid packages, which also publishes the raster margins.

import { container } from "@pindoba/styled-system/patterns";

<main className={container()}>…</main>;

Grid

The base preset’s grid pattern (columns, minChildWidth, gap, columnGap, rowGap), except that the default gap is the md token instead of a raw 8px. For layouts on the modular grid, use <Grid> from the grid packages.

import { grid } from "@pindoba/styled-system/patterns";

<ul className={grid({ minChildWidth: "xs" })}>…</ul>;
<div className={grid({ columns: 3, gap: "lg" })}>…</div>;

Best practices

  • Use translucent sparingly. backdrop-filter is GPU-intensive, so check it on low-end devices.
  • Prefer component props (<Panel translucent>, size) over spreading a pattern by hand in app code. Patterns are mainly building blocks for recipes.
  • Spread patterns with .raw() inside css.raw()/sva(). Never spread a plain helper object, because the extractor drops it.
  • A pattern’s transform runs in isolation at codegen, so it can’t import or close over variables. That’s why sizing emits token names instead of reading SIZE_PRESETS.