1
2
Installation
npx @park-ui/cli@next add group1
Add Component
Copy the code snippet below into you components folder.
import { ark } from '@ark-ui/react'
import {
  Children,
  type ComponentProps,
  type CSSProperties,
  cloneElement,
  forwardRef,
  isValidElement,
  memo,
  type ReactElement,
  useMemo,
} from 'react'
import { styled } from 'styled-system/jsx'
import { group } from 'styled-system/recipes'
type StyledGroupProps = ComponentProps<typeof StyledGroup>
const StyledGroup = styled(ark.div, group)
export interface GroupProps extends StyledGroupProps {
  /**
   * The `alignItems` style property
   */
  align?: StyledGroupProps['alignItems'] | undefined
  /**
   * The `justifyContent` style property
   */
  justify?: StyledGroupProps['justifyContent'] | undefined
  /**
   * The `flexWrap` style property
   */
  wrap?: StyledGroupProps['flexWrap'] | undefined
  /**
   * A function that determines if a child should be skipped
   */
  skip?: (child: ReactElement) => boolean | undefined
}
export const Group = memo(
  forwardRef<HTMLDivElement, GroupProps>(function Group(props, ref) {
    const { align = 'center', justify = 'flex-start', children, wrap, skip, ...rest } = props
    const enhancedChildren = useMemo(() => {
      // Convert children to array and filter out invalid elements
      const childArray = Children.toArray(children).filter(isValidElement)
      // If only one child, no need for group enhancements
      if (childArray.length === 1) {
        return childArray
      }
      // Filter out skipped children to get valid children for indexing
      const validChildren = childArray.filter((child) => !skip?.(child))
      // If only one valid child after filtering, return original array
      if (validChildren.length === 1) {
        return childArray
      }
      // Enhance each child with group data attributes
      return childArray.map((child) => {
        // Skip enhancement for children that should be skipped
        if (skip?.(child)) {
          return child
        }
        const childProps = child.props as Record<string, unknown> & {
          style?: CSSProperties
        }
        const indexInValidChildren = validChildren.indexOf(child)
        const enhancedProps: Record<string, unknown> = {
          ...childProps,
          'data-group-item': '',
          'data-first': dataAttr(indexInValidChildren === 0),
          'data-last': dataAttr(indexInValidChildren === validChildren.length - 1),
          'data-between': dataAttr(
            indexInValidChildren > 0 && indexInValidChildren < validChildren.length - 1,
          ),
          style: {
            '--group-count': validChildren.length,
            '--group-index': indexInValidChildren,
            ...(childProps?.style ?? {}),
          } as CSSProperties & {
            '--group-count': number
            '--group-index': number
          },
        }
        return cloneElement(child, enhancedProps)
      })
    }, [children, skip])
    return (
      <StyledGroup ref={ref} alignItems={align} justifyContent={justify} flexWrap={wrap} {...rest}>
        {enhancedChildren}
      </StyledGroup>
    )
  }),
)
type Booleanish = boolean | 'true' | 'false'
const dataAttr = (condition: boolean | undefined): Booleanish =>
  (condition ? '' : undefined) as Booleanish
2
Integrate Recipe
Integrate this recipe in to your Panda config.
import { defineRecipe } from '@pandacss/dev'
export const group = defineRecipe({
  className: 'group',
  base: {
    display: 'inline-flex',
    gap: 'var(--group-gap, 0.5rem)',
    isolation: 'isolate',
    position: 'relative',
    '& [data-group-item]': {
      _focusVisible: {
        zIndex: 1,
      },
    },
  },
  variants: {
    orientation: {
      horizontal: {
        flexDirection: 'row',
      },
      vertical: {
        flexDirection: 'column',
      },
    },
    attached: {
      true: {
        gap: '0!',
      },
    },
    grow: {
      true: {
        display: 'flex',
        '& > *': {
          flex: 1,
        },
      },
    },
    stacking: {
      'first-on-top': {
        '& > [data-group-item]': {
          zIndex: 'calc(var(--group-count) - var(--group-index))',
        },
      },
      'last-on-top': {
        '& > [data-group-item]': {
          zIndex: 'var(--group-index)',
        },
      },
    },
  },
  compoundVariants: [
    {
      orientation: 'horizontal',
      attached: true,
      css: {
        '& > *[data-first]': {
          borderEndRadius: '0!',
          marginEnd: '-1px',
        },
        '& > *[data-between]': {
          borderRadius: '0!',
          marginEnd: '-1px',
        },
        '& > *[data-last]': {
          borderStartRadius: '0!',
        },
      },
    },
    {
      orientation: 'vertical',
      attached: true,
      css: {
        '& > *[data-first]': {
          borderBottomRadius: '0!',
          marginBottom: '-1px',
        },
        '& > *[data-between]': {
          borderRadius: '0!',
          marginBottom: '-1px',
        },
        '& > *[data-last]': {
          borderTopRadius: '0!',
        },
      },
    },
  ],
  defaultVariants: {
    orientation: 'horizontal',
  },
})
Usage
import { Group } from '@/components/ui'
<Group>
  <div />
  <div />
</Group>
Examples
Button
Here's an example of using the Group component to group buttons together.
Attached
Use the attached prop to attach the children together.
Commit status
90+
Grow
Use the grow prop to make the children grow to fill the available space.
Props
| Prop | Default | Type | 
|---|---|---|
| orientation | 'horizontal' | 'horizontal' | 'vertical' | 
| attached | boolean | |
| grow | boolean | |
| stacking | 'first-on-top' | 'last-on-top' |