beta

DropZone

A drop zone is an area into which one or multiple objects can be dragged and dropped.

installyarn add react-aria-components
version1.0.0-beta.1
usageimport {DropZone} from 'react-aria-components'

Example#


import {DropZone, Text} from 'react-aria-components';

function Example() {
  let [dropped, setDropped] = React.useState(false);

  return (
    <DropZone
      onDrop={() => {
        setDropped(true);
      }}>
      <Text slot="label">
        {dropped ? "You dropped something" : "Drop object here"}
      </Text>
    </DropZone>
  );
}
import {DropZone, Text} from 'react-aria-components';

function Example() {
  let [dropped, setDropped] = React.useState(false);

  return (
    <DropZone
      onDrop={() => {
        setDropped(true);
      }}
    >
      <Text slot="label">
        {dropped
          ? 'You dropped something'
          : 'Drop object here'}
      </Text>
    </DropZone>
  );
}
import {
  DropZone,
  Text
} from 'react-aria-components';

function Example() {
  let [
    dropped,
    setDropped
  ] = React.useState(
    false
  );

  return (
    <DropZone
      onDrop={() => {
        setDropped(true);
      }}
    >
      <Text slot="label">
        {dropped
          ? 'You dropped something'
          : 'Drop object here'}
      </Text>
    </DropZone>
  );
}
Show CSS
.react-aria-DropZone {
  --border-color: var(--spectrum-alias-border-color);
  --border-color-pressed: var(--spectrum-alias-border-color-focus);
  --background-color: var(--spectrum-global-color-gray-50);
  --background-color-pressed: var(--spectrum-global-color-gray-100);
  --text-color: var(--spectrum-alias-text-color);
  --focus-ring-color: var(--spectrum-alias-border-color-focus);

  color: var(--text-color);
  background: var(--background-color);
  border: 1px solid var(--border-color);
  border-radius: 4px;
  appearance: none;
  vertical-align: middle;
  font-size: 1.2rem;
  text-align: center;
  margin: 0;
  outline: none;
  padding: 24px 12px;
  width: 25%;
  display: inline-block;

  &[data-drop-target] {
    box-shadow: 0 0 0 1px var(--focus-ring-color);
    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-DropZone {
    forced-color-adjust: none;
    --border-color: ButtonBorder;
    --border-color-drop-target: ButtonBorder;
    --background-color: ButtonFace;
    --background-color-pressed: ButtonFace;
    --text-color: ButtonText;
    --focus-ring-color: Highlight;
  }
}
.react-aria-DropZone {
  --border-color: var(--spectrum-alias-border-color);
  --border-color-pressed: var(--spectrum-alias-border-color-focus);
  --background-color: var(--spectrum-global-color-gray-50);
  --background-color-pressed: var(--spectrum-global-color-gray-100);
  --text-color: var(--spectrum-alias-text-color);
  --focus-ring-color: var(--spectrum-alias-border-color-focus);

  color: var(--text-color);
  background: var(--background-color);
  border: 1px solid var(--border-color);
  border-radius: 4px;
  appearance: none;
  vertical-align: middle;
  font-size: 1.2rem;
  text-align: center;
  margin: 0;
  outline: none;
  padding: 24px 12px;
  width: 25%;
  display: inline-block;

  &[data-drop-target] {
    box-shadow: 0 0 0 1px var(--focus-ring-color);
    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-DropZone {
    forced-color-adjust: none;
    --border-color: ButtonBorder;
    --border-color-drop-target: ButtonBorder;
    --background-color: ButtonFace;
    --background-color-pressed: ButtonFace;
    --text-color: ButtonText;
    --focus-ring-color: Highlight;
  }
}
.react-aria-DropZone {
  --border-color: var(--spectrum-alias-border-color);
  --border-color-pressed: var(--spectrum-alias-border-color-focus);
  --background-color: var(--spectrum-global-color-gray-50);
  --background-color-pressed: var(--spectrum-global-color-gray-100);
  --text-color: var(--spectrum-alias-text-color);
  --focus-ring-color: var(--spectrum-alias-border-color-focus);

  color: var(--text-color);
  background: var(--background-color);
  border: 1px solid var(--border-color);
  border-radius: 4px;
  appearance: none;
  vertical-align: middle;
  font-size: 1.2rem;
  text-align: center;
  margin: 0;
  outline: none;
  padding: 24px 12px;
  width: 25%;
  display: inline-block;

  &[data-drop-target] {
    box-shadow: 0 0 0 1px var(--focus-ring-color);
    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-DropZone {
    forced-color-adjust: none;
    --border-color: ButtonBorder;
    --border-color-drop-target: ButtonBorder;
    --background-color: ButtonFace;
    --background-color-pressed: ButtonFace;
    --text-color: ButtonText;
    --focus-ring-color: Highlight;
  }
}

Features#


There is no native element to implement a drop zone in HTML. DropZone helps achieve accessible dropzone components that can be styled as needed.

  • Styleable – Hover, keyboard focus, and the drop target states are provided for easy styling. These styles only apply when interacting with an appropriate input device, unlike CSS pseudo classes.
  • Accessible – Support for native drag and drop via mouse and touch, as well as keyboard and screen reader interactions. Copy and paste is also supported as a keyboard accessible alternative.
  • Flexible – Files, directories, and custom data types can be dropped, and the contents of the drop zone can be fully customized.

Anatomy#


A drop zone consists of a target element for the dropped objects. Users may drop objects via mouse, keyboard, or touch. DropZone accepts any content as its children, which may change when the user drops content. A FileTrigger is commonly paired with a DropZone to allow a user to choose files from their device.

A visual label should be provided to DropZone using a Text element with a label slot. If it is not provided, then an aria-label or aria-labelledby prop must be passed to identify the visually hidden button to assistive technology.

import {DropZone, Text} from 'react-aria-components';

<DropZone>
  <Text slot="label" />
</DropZone>
import {DropZone, Text} from 'react-aria-components';

<DropZone>
  <Text slot="label" />
</DropZone>
import {
  DropZone,
  Text
} from 'react-aria-components';

<DropZone>
  <Text slot="label" />
</DropZone>

Composed Components#

A drop zone can include a FileTrigger as a child, which may also be used standalone or reused in other components.

FileTrigger
A file trigger allows a user to access the file system with any pressable componet such as a Button.

Events#


DropZone supports user interactions via mouse, keyboard, and touch. You can handle all of these via the onDrop prop. In addition, the onDropEnter, onDropMove, and onDropExit events are fired as the user interacts with the dropzone.

import type {TextDropItem} from '@react-aria/dnd';

function Example() {
  let [dropped, setDropped] = React.useState(null);

  return (
    <>
      <Draggable />
      <DropZone
        onDrop={async (e) => {
          let items = await Promise.all(
            e.items
              .filter((item) =>
                item.kind === 'text' && item.types.has('text/plain')
              )
              .map((item: TextDropItem) => item.getText('text/plain'))
          );
          setDropped(items.join('\n'));
        }}
      >
        <Text slot="label">
          {dropped || 'Drop here'}
        </Text>
      </DropZone>
    </>
  );
}

<Example />
import type {TextDropItem} from '@react-aria/dnd';

function Example() {
  let [dropped, setDropped] = React.useState(null);

  return (
    <>
      <Draggable />
      <DropZone
        onDrop={async (e) => {
          let items = await Promise.all(
            e.items
              .filter((item) =>
                item.kind === 'text' &&
                item.types.has('text/plain')
              )
              .map((item: TextDropItem) =>
                item.getText('text/plain')
              )
          );
          setDropped(items.join('\n'));
        }}
      >
        <Text slot="label">
          {dropped || 'Drop here'}
        </Text>
      </DropZone>
    </>
  );
}

<Example />
import type {TextDropItem} from '@react-aria/dnd';

function Example() {
  let [
    dropped,
    setDropped
  ] = React.useState(
    null
  );

  return (
    <>
      <Draggable />
      <DropZone
        onDrop={async (
          e
        ) => {
          let items =
            await Promise
              .all(
                e.items
                  .filter(
                    (item) =>
                      item
                          .kind ===
                        'text' &&
                      item
                        .types
                        .has(
                          'text/plain'
                        )
                  )
                  .map((
                    item:
                      TextDropItem
                  ) =>
                    item
                      .getText(
                        'text/plain'
                      )
                  )
              );
          setDropped(
            items.join(
              '\n'
            )
          );
        }}
      >
        <Text slot="label">
          {dropped ||
            'Drop here'}
        </Text>
      </DropZone>
    </>
  );
}

<Example />

The Draggable component used above is defined below. See useDrag for more details and documentation.

Show code
import {useDrag} from '@react-aria/dnd';

function Draggable() {
  let { dragProps, isDragging } = useDrag({
    getItems() {
      return [{
        'text/plain': 'hello world',
        'my-app-custom-type': JSON.stringify({ message: 'hello world' })
      }];
    }
  });

  return (
    <div
      {...dragProps}
      role="button"
      tabIndex={0}
      className={`draggable ${isDragging ? 'dragging' : ''}`}
    >
      Drag me
    </div>
  );
}
import {useDrag} from '@react-aria/dnd';

function Draggable() {
  let { dragProps, isDragging } = useDrag({
    getItems() {
      return [{
        'text/plain': 'hello world',
        'my-app-custom-type': JSON.stringify({
          message: 'hello world'
        })
      }];
    }
  });

  return (
    <div
      {...dragProps}
      role="button"
      tabIndex={0}
      className={`draggable ${
        isDragging ? 'dragging' : ''
      }`}
    >
      Drag me
    </div>
  );
}
import {useDrag} from '@react-aria/dnd';

function Draggable() {
  let {
    dragProps,
    isDragging
  } = useDrag({
    getItems() {
      return [{
        'text/plain':
          'hello world',
        'my-app-custom-type':
          JSON.stringify(
            {
              message:
                'hello world'
            }
          )
      }];
    }
  });

  return (
    <div
      {...dragProps}
      role="button"
      tabIndex={0}
      className={`draggable ${
        isDragging
          ? 'dragging'
          : ''
      }`}
    >
      Drag me
    </div>
  );
}
Show CSS
.draggable {
  display: inline-block;
  vertical-align: top;
  border: 1px solid gray;
  padding: 10px;
  margin-right: 20px;
  border-radius: 4px;
}

.draggable.dragging {
  opacity: 0.5;
}
.draggable {
  display: inline-block;
  vertical-align: top;
  border: 1px solid gray;
  padding: 10px;
  margin-right: 20px;
  border-radius: 4px;
}

.draggable.dragging {
  opacity: 0.5;
}
.draggable {
  display: inline-block;
  vertical-align: top;
  border: 1px solid gray;
  padding: 10px;
  margin-right: 20px;
  border-radius: 4px;
}

.draggable.dragging {
  opacity: 0.5;
}

Labeling#


The label slot enables the user to reference the text content to define the dropzone's accessible name.

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

function Example() {
  let [dropped, setDropped] = React.useState(false);

  return (
    <DropZone
      onDrop={() => setDropped(true)}>
      <Text slot="label">
        {dropped ? 'Successful drop!' : 'Drop files here'}
      </Text>
    </DropZone>
  );
}
import {Text} from 'react-aria-components';

function Example() {
  let [dropped, setDropped] = React.useState(false);

  return (
    <DropZone
      onDrop={() => setDropped(true)}>
      <Text slot="label">
        {dropped ? 'Successful drop!' : 'Drop files here'}
      </Text>
    </DropZone>
  );
}
import {Text} from 'react-aria-components';

function Example() {
  let [
    dropped,
    setDropped
  ] = React.useState(
    false
  );

  return (
    <DropZone
      onDrop={() =>
        setDropped(true)}
    >
      <Text slot="label">
        {dropped
          ? 'Successful drop!'
          : 'Drop files here'}
      </Text>
    </DropZone>
  );
}

FileTrigger#


To allow the selection of files from the user's device, pass FileTrigger as a child of DropZone.

import {FileTrigger, Button} from 'react-aria-components';

function Example() {
  let [file, setFile] = React.useState(null);

  return(
    <DropZone>
      <FileTrigger
        onSelect={(e) => {
          let files = Array.from(e);
          let urls = files.map((file) => file.name);
          setFile(urls);
        }}>
        <Button>Select a file</Button>
      </FileTrigger>
      {file && file}
    </DropZone>
  );
}
import {FileTrigger, Button} from 'react-aria-components';

function Example() {
  let [file, setFile] = React.useState(null);

  return(
    <DropZone>
      <FileTrigger
        onSelect={(e) => {
          let files = Array.from(e);
          let urls = files.map((file) => file.name);
          setFile(urls);
        }}>
        <Button>Select a file</Button>
      </FileTrigger>
      {file && file}
    </DropZone>
  );
}
import {
  Button,
  FileTrigger
} from 'react-aria-components';

function Example() {
  let [file, setFile] =
    React.useState(null);

  return (
    <DropZone>
      <FileTrigger
        onSelect={(
          e
        ) => {
          let files =
            Array.from(
              e
            );
          let urls =
            files.map((
              file
            ) =>
              file.name
            );
          setFile(urls);
        }}
      >
        <Button>
          Select a file
        </Button>
      </FileTrigger>
      {file && file}
    </DropZone>
  );
}
Show CSS
.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;

  &[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);
  }

  &:disabled {
    border-color: var(--border-color-disabled);
    color: var(--text-color-disabled);
  }
}

@media (forced-colors: active) {
  .react-aria-Button {
    forced-color-adjust: none;
    --border-color: ButtonBorder;
    --border-color-pressed: ButtonBorder;
    --border-color-disabled: GrayText;
    --background-color: ButtonFace;
    --background-color-pressed: ButtonFace;
    --text-color: ButtonText;
    --text-color-disabled: GrayText;
    --focus-ring-color: Highlight;
  }
}
.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;

  &[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);
  }

  &:disabled {
    border-color: var(--border-color-disabled);
    color: var(--text-color-disabled);
  }
}

@media (forced-colors: active) {
  .react-aria-Button {
    forced-color-adjust: none;
    --border-color: ButtonBorder;
    --border-color-pressed: ButtonBorder;
    --border-color-disabled: GrayText;
    --background-color: ButtonFace;
    --background-color-pressed: ButtonFace;
    --text-color: ButtonText;
    --text-color-disabled: GrayText;
    --focus-ring-color: Highlight;
  }
}
.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;

  &[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);
  }

  &:disabled {
    border-color: var(--border-color-disabled);
    color: var(--text-color-disabled);
  }
}

@media (forced-colors: active) {
  .react-aria-Button {
    forced-color-adjust: none;
    --border-color: ButtonBorder;
    --border-color-pressed: ButtonBorder;
    --border-color-disabled: GrayText;
    --background-color: ButtonFace;
    --background-color-pressed: ButtonFace;
    --text-color: ButtonText;
    --text-color-disabled: GrayText;
    --focus-ring-color: Highlight;
  }
}

Visual feedback#


A dropzone displays visual feedback to the user when a drag hovers over the drop target by passing the getDropOperation function. If a drop target only supports data of specific types (e.g. images, videos, text, etc.), then it should implement the getDropOperation prop and return cancel for types that aren't supported. This will prevent visual feedback indicating that the drop target accepts the dragged data when this is not true. Read more about getDropOperation.

function Example() {
  let [dropped, setDropped] = React.useState(false);

  return (
    <DropZone
      getDropOperation={(types) => types.has('image/png') ? 'copy' : 'cancel'}
      onDrop={() => setDropped(true)}>
      {dropped ? 'Successful drop!' : 'Drop files here'}
    </DropZone>
  );
}
function Example() {
  let [dropped, setDropped] = React.useState(false);

  return (
    <DropZone
      getDropOperation={(types) =>
        types.has('image/png') ? 'copy' : 'cancel'}
      onDrop={() => setDropped(true)}
    >
      {dropped ? 'Successful drop!' : 'Drop files here'}
    </DropZone>
  );
}
function Example() {
  let [
    dropped,
    setDropped
  ] = React.useState(
    false
  );

  return (
    <DropZone
      getDropOperation={(
        types
      ) =>
        types.has(
            'image/png'
          )
          ? 'copy'
          : 'cancel'}
      onDrop={() =>
        setDropped(true)}
    >
      {dropped
        ? 'Successful drop!'
        : 'Drop files here'}
    </DropZone>
  );
}

Props#


NameTypeDescription
getDropOperation( (types: DragTypes, , allowedOperations: DropOperation[] )) => DropOperation

A function returning the drop operation to be performed when items matching the given types are dropped on the drop target.

childrenReactNode( (values: DropZoneRenderProps )) => ReactNodeThe children of the component. A function may be provided to alter the children based on component state.
classNamestring( (values: DropZoneRenderProps )) => stringThe CSS className for the element. A function may be provided to compute the class based on component state.
styleCSSProperties( (values: DropZoneRenderProps )) => CSSPropertiesThe inline style for the element. A function may be provided to compute the style based on component state.
Events
NameTypeDescription
onDropEnter( (e: DropEnterEvent )) => voidHandler that is called when a valid drag enters the drop target.
onDropMove( (e: DropMoveEvent )) => voidHandler that is called when a valid drag is moved within the drop target.
onDropExit( (e: DropExitEvent )) => voidHandler that is called when a valid drag exits the drop target.
onDrop( (e: DropEvent )) => voidHandler that is called when a valid drag is dropped on the drop target.
Layout
NameTypeDescription
slotstringnull

A slot name for the component. Slots allow the component to receive props from a parent component. An explicit null value indicates that the local props completely override all props received from a parent.

Accessibility
NameTypeDescription
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-DropZone {
  /* ... */
}
.react-aria-DropZone {
  /* ... */
}
.react-aria-DropZone {
  /* ... */
}

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

<DropZone className="my-dropzone">
  {/* ... */}
</DropZone>
<DropZone className="my-dropzone">
  {/* ... */}
</DropZone>
<DropZone className="my-dropzone">
  {/* ... */}
</DropZone>

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-DropZone[data-drop-target] {
  /* ... */
}
.react-aria-DropZone[data-drop-target] {
  /* ... */
}
.react-aria-DropZone[data-drop-target] {
  /* ... */
}

The className and style props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind.

<DropZone
  className={({ isDropTarget }) =>
    isDropTarget ? 'bg-gray-700' : 'bg-gray-600'}
/>
<DropZone
  className={({ isDropTarget }) =>
    isDropTarget ? 'bg-gray-700' : 'bg-gray-600'}
/>
<DropZone
  className={(
    { isDropTarget }
  ) =>
    isDropTarget
      ? 'bg-gray-700'
      : 'bg-gray-600'}
/>

Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render an extra element when the drop target is in an active state.

<DropZone>
  {({isDropTarget}) => (
    <>
      {isDropTarget && <DropHighlight/>}
      Drop item here
    </>
  )}
</DropZone>
<DropZone>
  {({isDropTarget}) => (
    <>
      {isDropTarget && <DropHighlight/>}
      Drop item here
    </>
  )}
</DropZone>
<DropZone>
  {(
    { isDropTarget }
  ) => (
    <>
      {isDropTarget &&
        (
          <DropHighlight />
        )}
      Drop item here
    </>
  )}
</DropZone>

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

NameCSS SelectorDescription
isHovered[data-hovered]Whether the dropzone is currently hovered with a mouse.
isFocused[data-focused]Whether the dropzone is focused, either via a mouse or keyboard.
isFocusVisible[data-focus-visible]Whether the dropzone is keyboard focused.
isDropTarget[data-drop-target]Whether the dropzone is the drop target.

Advanced customization#


Hooks#

If you need to customize things further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useDrop for more details.