beta

Group

A group represents a set of related UI controls, and supports interactive states for styling.

installyarn add react-aria-components
version1.0.0-beta.2
usageimport {Group} from 'react-aria-components'

Example#


import {TextField, Label, Group, Input, Button} from 'react-aria-components';

<TextField>
  <Label>Email</Label>
  <Group>
    <Input />
    <Button aria-label="Add email">+</Button>
  </Group>
</TextField>
import {
  Button,
  Group,
  Input,
  Label,
  TextField
} from 'react-aria-components';

<TextField>
  <Label>Email</Label>
  <Group>
    <Input />
    <Button aria-label="Add email">+</Button>
  </Group>
</TextField>
import {
  Button,
  Group,
  Input,
  Label,
  TextField
} from 'react-aria-components';

<TextField>
  <Label>Email</Label>
  <Group>
    <Input />
    <Button aria-label="Add email">
      +
    </Button>
  </Group>
</TextField>
Show CSS
.react-aria-Group {
  --field-border: var(--spectrum-alias-border-color);
  --field-border-hovered: var(--spectrum-alias-border-color-hover);
  --field-background: var(--spectrum-global-color-gray-50);
  --text-color: var(--spectrum-alias-text-color);
  --text-color-disabled: var(--spectrum-alias-text-color-disabled);
  --focus-ring-color: slateblue;
  --invalid-color: var(--spectrum-global-color-red-600);

  display: flex;
  align-items: center;
  width: fit-content;
  border-radius: 6px;
  border: 1px solid var(--field-border);
  background: var(--field-background);
  overflow: hidden;

  &[data-hovered] {
    border-color: var(--field-border-hovered);
  }

  &[data-focus-within] {
    border-color: var(--focus-ring-color);
    box-shadow: 0 0 0 1px var(--focus-ring-color);
  }

  .react-aria-Input {
    padding: 0.286rem;
    margin: 0;
    font-size: 1rem;
    color: var(--text-color);
    outline: none;
    border: none;
    background: transparent;

    &::placeholder {
      color: var(--spectrum-gray-600);
    }
  }

  .react-aria-Button {
    padding: 0 6px;
    border-width: 0 0 0 1px;
    border-radius: 0 6px 6px 0;
    align-self: stretch;
    font-size: 1.5rem;
  }
}

.react-aria-Button {
  --border-color: var(--spectrum-alias-border-color);
  --border-color-pressed: var(--spectrum-alias-border-color-down);
  --border-color-disabled: var(--spectrum-alias-border-color-disabled);
  --background-color: var(--spectrum-global-color-gray-50);
  --background-color-pressed: var(--spectrum-global-color-gray-100);
  --text-color: var(--spectrum-alias-text-color);
  --text-color-disabled: var(--spectrum-alias-text-color-disabled);
  --focus-ring-color: slateblue;

  color: var(--text-color);
  background: var(--background-color);
  border: 1px solid var(--border-color);
  border-radius: 4px;
  appearance: none;
  vertical-align: middle;
  font-size: 1rem;
  text-align: center;
  margin: 0;
  outline: none;
  padding: 6px 10px;
  text-decoration: none;

  &[data-pressed] {
    box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
    background: var(--background-color-pressed);
    border-color: var(--border-color-pressed);
  }

  &[data-focus-visible] {
    border-color: var(--focus-ring-color);
    box-shadow: 0 0 0 1px var(--focus-ring-color);
  }
}

@media (forced-colors: active) {
  .react-aria-TextField {
    --field-border: ButtonBorder;
    --field-border-disabled: GrayText;
    --field-background: Field;
    --text-color: FieldText;
    --text-color-disabled: GrayText;
    --focus-ring-color: Highlight;
    --invalid-color: LinkText;
  }
}
.react-aria-Group {
  --field-border: var(--spectrum-alias-border-color);
  --field-border-hovered: var(--spectrum-alias-border-color-hover);
  --field-background: var(--spectrum-global-color-gray-50);
  --text-color: var(--spectrum-alias-text-color);
  --text-color-disabled: var(--spectrum-alias-text-color-disabled);
  --focus-ring-color: slateblue;
  --invalid-color: var(--spectrum-global-color-red-600);

  display: flex;
  align-items: center;
  width: fit-content;
  border-radius: 6px;
  border: 1px solid var(--field-border);
  background: var(--field-background);
  overflow: hidden;

  &[data-hovered] {
    border-color: var(--field-border-hovered);
  }

  &[data-focus-within] {
    border-color: var(--focus-ring-color);
    box-shadow: 0 0 0 1px var(--focus-ring-color);
  }

  .react-aria-Input {
    padding: 0.286rem;
    margin: 0;
    font-size: 1rem;
    color: var(--text-color);
    outline: none;
    border: none;
    background: transparent;

    &::placeholder {
      color: var(--spectrum-gray-600);
    }
  }

  .react-aria-Button {
    padding: 0 6px;
    border-width: 0 0 0 1px;
    border-radius: 0 6px 6px 0;
    align-self: stretch;
    font-size: 1.5rem;
  }
}

.react-aria-Button {
  --border-color: var(--spectrum-alias-border-color);
  --border-color-pressed: var(--spectrum-alias-border-color-down);
  --border-color-disabled: var(--spectrum-alias-border-color-disabled);
  --background-color: var(--spectrum-global-color-gray-50);
  --background-color-pressed: var(--spectrum-global-color-gray-100);
  --text-color: var(--spectrum-alias-text-color);
  --text-color-disabled: var(--spectrum-alias-text-color-disabled);
  --focus-ring-color: slateblue;

  color: var(--text-color);
  background: var(--background-color);
  border: 1px solid var(--border-color);
  border-radius: 4px;
  appearance: none;
  vertical-align: middle;
  font-size: 1rem;
  text-align: center;
  margin: 0;
  outline: none;
  padding: 6px 10px;
  text-decoration: none;

  &[data-pressed] {
    box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
    background: var(--background-color-pressed);
    border-color: var(--border-color-pressed);
  }

  &[data-focus-visible] {
    border-color: var(--focus-ring-color);
    box-shadow: 0 0 0 1px var(--focus-ring-color);
  }
}

@media (forced-colors: active) {
  .react-aria-TextField {
    --field-border: ButtonBorder;
    --field-border-disabled: GrayText;
    --field-background: Field;
    --text-color: FieldText;
    --text-color-disabled: GrayText;
    --focus-ring-color: Highlight;
    --invalid-color: LinkText;
  }
}
.react-aria-Group {
  --field-border: var(--spectrum-alias-border-color);
  --field-border-hovered: var(--spectrum-alias-border-color-hover);
  --field-background: var(--spectrum-global-color-gray-50);
  --text-color: var(--spectrum-alias-text-color);
  --text-color-disabled: var(--spectrum-alias-text-color-disabled);
  --focus-ring-color: slateblue;
  --invalid-color: var(--spectrum-global-color-red-600);

  display: flex;
  align-items: center;
  width: fit-content;
  border-radius: 6px;
  border: 1px solid var(--field-border);
  background: var(--field-background);
  overflow: hidden;

  &[data-hovered] {
    border-color: var(--field-border-hovered);
  }

  &[data-focus-within] {
    border-color: var(--focus-ring-color);
    box-shadow: 0 0 0 1px var(--focus-ring-color);
  }

  .react-aria-Input {
    padding: 0.286rem;
    margin: 0;
    font-size: 1rem;
    color: var(--text-color);
    outline: none;
    border: none;
    background: transparent;

    &::placeholder {
      color: var(--spectrum-gray-600);
    }
  }

  .react-aria-Button {
    padding: 0 6px;
    border-width: 0 0 0 1px;
    border-radius: 0 6px 6px 0;
    align-self: stretch;
    font-size: 1.5rem;
  }
}

.react-aria-Button {
  --border-color: var(--spectrum-alias-border-color);
  --border-color-pressed: var(--spectrum-alias-border-color-down);
  --border-color-disabled: var(--spectrum-alias-border-color-disabled);
  --background-color: var(--spectrum-global-color-gray-50);
  --background-color-pressed: var(--spectrum-global-color-gray-100);
  --text-color: var(--spectrum-alias-text-color);
  --text-color-disabled: var(--spectrum-alias-text-color-disabled);
  --focus-ring-color: slateblue;

  color: var(--text-color);
  background: var(--background-color);
  border: 1px solid var(--border-color);
  border-radius: 4px;
  appearance: none;
  vertical-align: middle;
  font-size: 1rem;
  text-align: center;
  margin: 0;
  outline: none;
  padding: 6px 10px;
  text-decoration: none;

  &[data-pressed] {
    box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
    background: var(--background-color-pressed);
    border-color: var(--border-color-pressed);
  }

  &[data-focus-visible] {
    border-color: var(--focus-ring-color);
    box-shadow: 0 0 0 1px var(--focus-ring-color);
  }
}

@media (forced-colors: active) {
  .react-aria-TextField {
    --field-border: ButtonBorder;
    --field-border-disabled: GrayText;
    --field-background: Field;
    --text-color: FieldText;
    --text-color-disabled: GrayText;
    --focus-ring-color: Highlight;
    --invalid-color: LinkText;
  }
}

Features#


A group can be created with a <div role="group"> or via the HTML <fieldset> element. The Group component supports additional UI states, and can be used standalone or as part of a larger pattern such as NumberField or DatePicker.

  • Styleable – Hover, keyboard focus, disabled, and invalid states are provided for easy styling. These states only apply when interacting with an appropriate input device, unlike CSS pseudo classes.
  • Accessible – Implemented using the ARIA "group" role by default, with optional support for the "region" landmark role.

Anatomy#


A group consists of a container element for a set of semantically related UI controls. It supports states such as hover, focus within, and disabled, which are useful to style visually adjoined children.

import {Group} from 'react-aria-components';

<Group>
  {/* ... */}
</Group>
import {Group} from 'react-aria-components';

<Group>
  {/* ... */}
</Group>
import {Group} from 'react-aria-components';

<Group>
  {/* ... */}
</Group>

Accessibility#


Labeling#

Group accepts the aria-label and aria-labelledby attributes to provide an accessible label to the group as a whole. This is read by assistive technology when navigating into the group from outside. When the labels of each child element of the group do not provide sufficient context on their own, the group should receive an additional label.

<span id="label-id">Serial number</span>
<Group aria-labelledby="label-id">
  <Input size={3} aria-label="First 3 digits" placeholder="000" /><Input size={2} aria-label="Middle 2 digits" placeholder="00" /><Input size={4} aria-label="Last 4 digits" placeholder="0000" />
</Group>
<span id="label-id">Serial number</span>
<Group aria-labelledby="label-id">
  <Input
    size={3}
    aria-label="First 3 digits"
    placeholder="000"
  /><Input
    size={2}
    aria-label="Middle 2 digits"
    placeholder="00"
  /><Input
    size={4}
    aria-label="Last 4 digits"
    placeholder="0000"
  />
</Group>
<span id="label-id">
  Serial number
</span>
<Group aria-labelledby="label-id">
  <Input
    size={3}
    aria-label="First 3 digits"
    placeholder="000"
  /><Input
    size={2}
    aria-label="Middle 2 digits"
    placeholder="00"
  /><Input
    size={4}
    aria-label="Last 4 digits"
    placeholder="0000"
  />
</Group>

Role#

By default, Group uses the group ARIA role. If the contents of the group is important enough to be included in the page table of contents, use role="region" instead, and ensure that an aria-label or aria-labelledby prop is assigned.

<Group role="region" aria-label="Object details">
  {/* ... */}
</Group>
<Group role="region" aria-label="Object details">
  {/* ... */}
</Group>
<Group
  role="region"
  aria-label="Object details"
>
  {/* ... */}
</Group>

If the Group component is used for styling purposes only, and does not include a set of related UI controls, then use role="presentation" instead.

Props#


NameTypeDescription
isDisabledbooleanWhether the group is disabled.
isInvalidbooleanWhether the group is invalid.
classNamestring( (values: GroupRenderProps )) => stringThe CSS className for the element. A function may be provided to compute the class based on component state.
styleCSSProperties( (values: GroupRenderProps )) => CSSPropertiesThe inline style for the element. A function may be provided to compute the style based on component state.
Accessibility
NameTypeDefaultDescription
role'group''region''presentation''group'

An accessibility role for the group. By default, this is set to 'group'. Use 'region' when the contents of the group is important enough to be included in the page table of contents. Use 'presentation' if the group is visual only and does not represent a semantic grouping of controls.

aria-labelstringDefines a string value that labels the current element.
aria-labelledbystringIdentifies the element (or elements) that labels the current element.
aria-describedbystringIdentifies the element (or elements) that describes the object.
aria-detailsstringIdentifies the element (or elements) that provide a detailed, extended description for the object.

Styling#


React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin className attribute which can be targeted using CSS selectors. These follow the react-aria-ComponentName naming convention.

.react-aria-Group {
  /* ... */
}
.react-aria-Group {
  /* ... */
}
.react-aria-Group {
  /* ... */
}

A custom className can also be specified on any component. This overrides the default className provided by React Aria with your own.

<Group className="my-group">
  {/* ... */}
</Group>
<Group className="my-group">
  {/* ... */}
</Group>
<Group className="my-group">
  {/* ... */}
</Group>

In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example:

.react-aria-Group[data-hovered] {
  /* ... */
}

.react-aria-Group[data-focus-visible] {
  /* ... */
}
.react-aria-Group[data-hovered] {
  /* ... */
}

.react-aria-Group[data-focus-visible] {
  /* ... */
}
.react-aria-Group[data-hovered] {
  /* ... */
}

.react-aria-Group[data-focus-visible] {
  /* ... */
}

The states, selectors, and render props for Group are documented below.

NameCSS SelectorDescription
isHovered[data-hovered]Whether the group is currently hovered with a mouse.
isFocusWithin[data-focus-within]Whether an element within the group is focused, either via a mouse or keyboard.
isFocusVisible[data-focus-visible]Whether an element within the group is keyboard focused.
isDisabled[data-disabled]Whether the group is disabled.
isInvalid[data-invalid]Whether the group is invalid.

Advanced customization#


Contexts#

All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps).

ComponentContextPropsRef
GroupGroupContextGroupPropsHTMLDivElement

This example shows a LabeledGroup component that accepts a label and a group as children. It uses the useId hook to generate a unique id for the label, and provides this to the group via the aria-labelledby prop.

import {LabelContext, GroupContext} from 'react-aria-components';
import {useId} from 'react-aria';

function LabeledGroup({children}) {
  let labelId = useId();

  return (
    <LabelContext.Provider value={{id: labelId, elementType: 'span'}}>
      <GroupContext.Provider value={{'aria-labelledby': labelId}}>
        {children}
      </GroupContext.Provider>
    </LabelContext.Provider>
  );
}

<LabeledGroup>
  <Label>Expiration date</Label>
  <Group>
    <Input size={3} aria-label="Month" placeholder="mm" />
    /
    <Input size={4} aria-label="Year" placeholder="yyyy" />
  </Group>
</LabeledGroup>
import {
  GroupContext,
  LabelContext
} from 'react-aria-components';
import {useId} from 'react-aria';

function LabeledGroup({ children }) {
  let labelId = useId();

  return (
    <LabelContext.Provider
      value={{ id: labelId, elementType: 'span' }}
    >
      <GroupContext.Provider
        value={{ 'aria-labelledby': labelId }}
      >
        {children}
      </GroupContext.Provider>
    </LabelContext.Provider>
  );
}

<LabeledGroup>
  <Label>Expiration date</Label>
  <Group>
    <Input size={3} aria-label="Month" placeholder="mm" />
    /
    <Input
      size={4}
      aria-label="Year"
      placeholder="yyyy"
    />
  </Group>
</LabeledGroup>
import {
  GroupContext,
  LabelContext
} from 'react-aria-components';
import {useId} from 'react-aria';

function LabeledGroup(
  { children }
) {
  let labelId = useId();

  return (
    <LabelContext.Provider
      value={{
        id: labelId,
        elementType:
          'span'
      }}
    >
      <GroupContext.Provider
        value={{
          'aria-labelledby':
            labelId
        }}
      >
        {children}
      </GroupContext.Provider>
    </LabelContext.Provider>
  );
}

<LabeledGroup>
  <Label>
    Expiration date
  </Label>
  <Group>
    <Input
      size={3}
      aria-label="Month"
      placeholder="mm"
    />
    /
    <Input
      size={4}
      aria-label="Year"
      placeholder="yyyy"
    />
  </Group>
</LabeledGroup>