Concepts

Conditional Styles

When writing styles, you might need to apply specific changes depending on a specific condition, whether it's based on breakpoint, css pseudo state, media query or custom data attributes.

Panda allows you to write conditional styles, and provides common condition shortcuts to make your life easier. Let's say you want to change the background color of a button when it's hovered. You can do it like this:

<button
  className={css({
    bg: 'red.500',
    _hover: { bg: 'red.700' }
  })}
>
  Hover me
</button>

Overview

Property based condition

This works great, but might be a bit verbose. You can apply the condition _hover directly to the bg property, leading to a more concise syntax:

<button
  className={css({
-   bg: 'red.500',
-   _hover: { bg: 'red.700' }
+   bg: { base: 'red.500', _hover: 'red.700' }
  })}
>
  Hover me
</button>
💡

Note: The base key is used to define the default value of the property, without any condition.

Nested condition

Conditions in Panda can be nested, which means you can apply multiple conditions to a single property or another condition.

Let's say you want to change the background color of a button when it's focused and hovered. You can do it like this:

<button
  className={css({
    bg: { base: 'red.500', _hover: { _focus: 'red.700' } }
  })}
>
  Hover me
</button>

Built-in conditions

Panda includes a set of common pseudo states that you can use to style your components:

  • Pseudo Class: _hover, _active, _focus, _focus-visible, _focus-within, _disabled
  • Pseudo Element: _before, _after
  • Media Query: sm, md, lg, xl, 2xl
  • Data Attribute Selector: _horizontal, _vertical, _portrait, _landscape

Arbitrary selectors

What if you need a one-off selector that is not defined in your config's conditions? You can use the css function to generate classes for arbitrary selectors:

import { css } from '../styled-system/css'
 
const App = () => {
  return (
    <div
      className={css({
        '&[data-state=closed]': { color: 'red.300' },
        '& > *': { margin: '2' }
      })}
    />
  )
}

This also works with the supported at-rules (@media, @layer, @container, @supports, and @page):

import { css } from '../styled-system/css'
 
const App = () => {
  return (
    <div className={css({ display: 'flex', containerType: 'size' })}>
      <div
        className={css({
          '@media (min-width: 768px)': {
            color: 'red.300'
          },
          '@container (min-width: 10px)': {
            color: 'green.300'
          },
          '@supports (display: flex)': {
            fontSize: '3xl',
            color: 'blue.300'
          }
        })}
      />
    </div>
  )
}

Pseudo Classes

Hover, Active, Focus, and Disabled

You can style the hover, active, focus, and disabled states of an element using their _ modifier:

<button
  className={css({
    bg: 'red.500',
    _hover: { bg: 'red.700' },
    _active: { bg: 'red.900' }
  })}
>
  Hover me
</button>

First, Last, Odd, Even

You can style the first, last, odd, and even elements of a group using their _ modifier:

<ul>
  {items.map(item => (
    <li key={item} className={css({ _first: { color: 'red.500' } })}>
      {item}
    </li>
  ))}
</ul>

You can also style even and odd elements using the _even and _odd modifier:

<table>
  <tbody>
    {items.map(item => (
      <tr
        key={item}
        className={css({
          _even: { bg: 'gray.100' },
          _odd: { bg: 'white' }
        })}
      >
        <td>{item}</td>
      </tr>
    ))}
  </tbody>
</table>

Pseudo Elements

Before and After

You can style the ::before and ::after pseudo elements of an element using their _before and _after modifier:

<div
  className={css({
    _before: { content: '"👋"' }
  })}
>
  Hello
</div>

Notes

  • Before and After: Ensure you wrap the content value in double quotes.
  • Mixing with Conditions: When using condition and pseudo elements, prefer to place the condition before the pseudo element.
css({
  // This works ✅
  _dark: { _backdrop: { color: 'red' } }
  // This doesn't work ❌
  _backdrop: { _dark: { color: 'red' } }
})

The reason _backdrop: { _dark: { color: 'red' } } doesn't work is because it generated an invalid CSS structure that looks like:

&::backdrop {
  &.dark,
  .dark & {
    color: red;
  }
}

Placeholder

Style the placeholder text of any input or textarea using the _placeholder modifier:

<input
  placeholder="Enter your name"
  className={css({
    _placeholder: { color: 'gray.500' }
  })}
/>

File Inputs

Style the file input button using the _file modifier:

<input
  type="file"
  className={css({
    _file: { bg: 'gray.500', px: '4', py: '2', marginEnd: '3' }
  })}
/>

Media Queries

Reduced Motion

Use the _motionReduce and _motionSafe modifiers to style an element based on the user's motion preference:

<div
  className={css({
    _motionReduce: { transition: 'none' },
    _motionSafe: { transition: 'all 0.3s' }
  })}
>
  Hello
</div>

Color Scheme

The prefers-color-scheme media feature is used to detect if the user has requested the system use a light or dark color theme.

Use the _osLight and _osDark modifiers to style an element based on the user's color scheme preference:

<div
  className={css({
    bg: 'white',
    _osDark: { bg: 'black' }
  })}
>
  Hello
</div>

Let's say your app is dark by default, but you want to allow users to switch to a light theme. You can do it like this:

<div
  className={css({
    bg: 'black',
    _osLight: { bg: 'white' }
  })}
>
  Hello
</div>

Color Contrast

The prefers-contrast media feature is used to detect if the user has requested the system use a high or low contrast theme.

Use the _highContrast and _lessContrast modifiers to style an element based on the user's color contrast preference:

<div
  className={css({
    bg: 'white',
    _highContrast: { bg: 'black' }
  })}
>
  Hello
</div>

Orientation

The orientation media feature is used to detect if the user has a device in portrait or landscape mode.

Use the _portrait and _landscape modifiers to style an element based on the user's device orientation:

<div
  className={css({
    pb: '4',
    _portrait: { pb: '8' }
  })}
>
  Hello
</div>

Group Selectors

When you need to style an element based on its parent element's state or attribute, you can add the group class to the parent element, and use any of the _group* modifiers on the child element.

<div className="group">
  <p className={css({ _groupHover: { bg: 'red.500' } })}>Hover me</p>
</div>

This modifer for every pseudo class modifiers like _groupHover, _groupActive, _groupFocus, and _groupDisabled, etc.

Sibling Selectors

When you need to style an element based on its sibling element's state or attribute, you can add the peer class to the sibling element, and use any of the _peer* modifiers on the target element.

<div>
  <p className="peer">Hover me</p>
  <p className={css({ _peerHover: { bg: 'red.500' } })}>I'll change by bg</p>
</div>
💡

Note: This only works for when the element marked with peer is a previous siblings, that is, it comes before the element you want to start.

Data Attribute

LTR and RTL

You can style an element based on the direction of the text using the _ltr and _rtl modifiers:

<div dir="ltr">
  <div
    className={css({
      _ltr: { ml: '3' },
      _rtl: { mr: '3' }
    })}
  >
    Hello
  </div>
</div>

For this to work, you need to set the dir attribute on the parent element. In most cases,you can set this on the html element.

💡

Note: Consider using logical css properties like marginInlineStart and marginInlineEnd instead their physical counterparts like marginLeft and marginRight. This will reduce the need to use the _ltr and _rtl modifiers.

State

You can style an element based on its data-{state} attribute using the corresponding _{state} modifier:

<div
  data-loading
  className={css({
    _loading: { bg: 'gray.500' }
  })}
>
  Hello
</div>

This also works for common states like data-active, data-disabled, data-focus, data-hover, data-invalid, data-required, and data-valid.

<div
  data-active
  className={css({
    _active: { bg: 'gray.500' }
  })}
>
  Hello
</div>
💡

Most of the data-{state} attributes typically mirror the corresponding browser pseudo class. For example, data-hover is equivalent to :hover, data-focus is equivalent to :focus, and data-active is equivalent to :active.

Orientation

You can style an element based on its data-orientation attribute using the _horizontal and _vertical modifiers:

<div
  data-orientation="horizontal"
  className={css({
    _horizontal: { bg: 'red.500' },
    _vertical: { bg: 'blue.500' }
  })}
>
  Hello
</div>

ARIA Attribute

You can style an element based on its aria-{state}=true attribute using the corresponding _{state} modifier:

<div
  aria-expanded="true"
  className={css({
    _expanded: { bg: 'gray.500' }
  })}
>
  Hello
</div>
💡

Most of the aria-{state} attributes typically mirror the support ARIA states in the browser pseudo class. For example, aria-checked=true is styled with _checked, aria-disabled=true is styled with _disabled.

Container queries

You can define container names and sizes in your theme configuration and use them in your styles.

export default defineConfig({
  // ...
  theme: {
    extend: {
      containerNames: ['sidebar', 'content'],
      containerSizes: {
        xs: '40em',
        sm: '60em',
        md: '80em'
      }
    }
  }
})

The default container sizes in the @pandacss/preset-panda preset are shown below:

export const containerSizes = {
  xs: '320px',
  sm: '384px',
  md: '448px',
  lg: '512px',
  xl: '576px',
  '2xl': '672px',
  '3xl': '768px',
  '4xl': '896px',
  '5xl': '1024px',
  '6xl': '1152px',
  '7xl': '1280px',
  '8xl': '1440px'
}

Then use them in your styles by referencing using @<container-name>/<container-size> syntax:

💡

The default container syntax is @/<container-size>.

import { css } from '/styled-system/css'
 
function Demo() {
  return (
    <nav className={css({ containerType: 'inline-size' })}>
      <div
        className={css({
          fontSize: { '@/sm': 'md' }
        })}
      />
    </nav>
  )
}

This will generate the following CSS:

.cq-type_inline-size {
  container-type: inline-size;
}
 
@container (min-width: 60em) {
  .\@\/sm:fs_md {
    container-type: inline-size;
  }
}

You can also named container queries:

import { cq } from 'styled-system/patterns'
 
function Demo() {
  return (
    <nav className={cq({ name: 'sidebar' })}>
      <div
        className={css({
          fontSize: { base: 'lg', '@sidebar/sm': 'md' }
        })}
      />
    </nav>
  )
}

Reference

Here's a list of all the condition shortcuts you can use in Panda:

Condition nameSelector
_hover&:is(:hover, [data-hover])
_focus&:is(:focus, [data-focus])
_focusWithin&:focus-within
_focusVisible&:is(:focus-visible, [data-focus-visible])
_disabled&:is(:disabled, [disabled], [data-disabled])
_active&:is(:active, [data-active])
_visited&:visited
_target&:target
_readOnly&:is(:read-only, [data-read-only])
_readWrite&:read-write
_empty&:is(:empty, [data-empty])
_checked&:is(:checked, [data-checked], [aria-checked=true])
_enabled&:enabled
_expanded&:is([aria-expanded=true], [data-expanded])
_highlighted&[data-highlighted]
_before&::before
_after&::after
_firstLetter&::first-letter
_firstLine&::first-line
_marker&::marker
_selection&::selection
_file&::file-selector-button
_backdrop&::backdrop
_first&:first-child
_last&:last-child
_only&:only-child
_even&:even
_odd&:odd
_firstOfType&:first-of-type
_lastOfType&:last-of-type
_onlyOfType&:only-of-type
_peerFocus.peer:is(:focus, [data-focus]) ~ &
_peerHover.peer:is(:hover, [data-hover]) ~ &
_peerActive.peer:is(:active, [data-active]) ~ &
_peerFocusWithin.peer:focus-within ~ &
_peerFocusVisible.peer:is(:focus-visible, [data-focus-visible]) ~ &
_peerDisabled.peer:is(:disabled, [disabled], [data-disabled]) ~ &
_peerChecked.peer:is(:checked, [data-checked], [aria-checked=true]) ~ &
_peerInvalid.peer:is(:invalid, [data-invalid], [aria-invalid=true]) ~ &
_peerExpanded.peer:is([aria-expanded=true], [data-expanded]) ~ &
_peerPlaceholderShown.peer:placeholder-shown ~ &
_groupFocus.group:is(:focus, [data-focus]) &
_groupHover.group:is(:hover, [data-hover]) &
_groupActive.group:is(:active, [data-active]) &
_groupFocusWithin.group:focus-within &
_groupFocusVisible.group:is(:focus-visible, [data-focus-visible]) &
_groupDisabled.group:is(:disabled, [disabled], [data-disabled]) &
_groupChecked.group:is(:checked, [data-checked], [aria-checked=true]) &
_groupExpanded.group:is([aria-expanded=true], [data-expanded]) &
_groupInvalid.group:invalid &
_indeterminate&:is(:indeterminate, [data-indeterminate], [aria-checked=mixed])
_required&:is(:required, [data-required], [aria-required=true])
_valid&:is(:valid, [data-valid])
_invalid&:is(:invalid, [data-invalid])
_autofill&:autofill
_inRange&:in-range
_outOfRange&:out-of-range
_placeholder&:is(:placeholder, [data-placeholder])
_placeholderShown&:is(:placeholder-shown, [data-placeholder-shown])
_pressed&:is([aria-pressed=true], [data-pressed])
_selected&:is([aria-selected=true], [data-selected])
_default&:default
_optional&:optional
_open&[open]
_fullscreen&:fullscreen
_loading&:is([data-loading], [aria-busy=true])
_currentPage&[aria-current=page]
_currentStep&[aria-current=step]
_motionReduce@media (prefers-reduced-motion: reduce)
_motionSafe@media (prefers-reduced-motion: no-preference)
_print@media print
_landscape@media (orientation: landscape)
_portrait@media (orientation: portrait)
_dark&.dark, .dark &
_light&.light, .light &
_osDark@media (prefers-color-scheme: dark)
_osLight@media (prefers-color-scheme: light)
_highContrast@media (forced-colors: active)
_lessContrast@media (prefers-contrast: less)
_moreContrast@media (prefers-contrast: more)
_ltr[dir=ltr] &
_rtl[dir=rtl] &
_scrollbar&::-webkit-scrollbar
_scrollbarThumb&::-webkit-scrollbar-thumb
_scrollbarTrack&::-webkit-scrollbar-track
_horizontal&[data-orientation=horizontal]
_vertical&[data-orientation=vertical]

Custom conditions

Panda lets you create your own conditions, so you're not limited to the ones in the default preset. Learn more about customizing conditions here.