alpha

Dropzone

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

installyarn add react-aria-components
version1.0.0-alpha.5
usageimport {DropZone} from 'react-aria-components'

Example#


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

<DropZone>
  Drop object here
</DropZone>
import {DropZone} from 'react-aria-components';

<DropZone>
  Drop object here
</DropZone>
import {DropZone} from 'react-aria-components';

<DropZone>
  Drop object here
</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 dropzone in HTML that is widely supported. DropZone helps achieve accessible dropzone components that can be styled as needed.

  • Styleable – Hover, keyboard focus, and the drop target state is provided for easy styling. These styles only apply when interacting with an appropriate input device, unlike CSS pseudo classes.
  • Accessible – Uses a visually hidden <button> under the hood to allow interactions like dropping objects with a keyboard.
  • Flexible – Can be used standalone or with FileTrigger to allow file uploads.

Anatomy#


A dropzone consists of a target element for the dropped objects. Users may drop objects via mouse or keyboard. In addition, a FileTrigger may be passed as a child to allow access to computer's file system.

If a visual label is not provided, then an aria-label or aria-labelledby prop must be passed to identify the button to assistive technology.

Composed Components#

A dropzone can pass FileTrigger as a child, which may also be used standalone or reused in other components.

Props#


NameTypeDefaultDescription
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
NameTypeDefaultDescription
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
NameTypeDefaultDescription
slotstringA slot name for the component. Slots allow the component to receive props from a parent component.
Accessibility
NameTypeDefaultDescription
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 DOM attributes, which you can target in CSS selectors. These are ARIA attributes wherever possible, or data attributes when a relevant ARIA attribute does not exist. 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 button is in a pressed 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.

Usage#


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="heading">
          {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="heading">
          {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="heading">
          {dropped ||
            'Drop here'}
        </Text>
      </DropZone>
    </>
  );
}

<Example />

Draggable#

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;
}

Heading#

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

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

<DropZone>
  <Text slot="heading">
    Drop item here
  </Text>
</DropZone>
import {Text} from 'react-aria-components';

<DropZone>
  <Text slot="heading">
    Drop item here
  </Text>
</DropZone>
import {Text} from 'react-aria-components';

<DropZone>
  <Text slot="heading">
    Drop item here
  </Text>
</DropZone>

File Upload#

To upload files, pass FileTrigger as a child of DropZone.

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

<DropZone>
  <FileTrigger>
    <Link>Select a file</Link> from your computer
  </FileTrigger>
</DropZone>
import {FileTrigger, Link} from 'react-aria-components';

<DropZone>
  <FileTrigger>
    <Link>Select a file</Link> from your computer
  </FileTrigger>
</DropZone>
import {
  FileTrigger,
  Link
} from 'react-aria-components';

<DropZone>
  <FileTrigger>
    <Link>
      Select a file
    </Link>{' '}
    from your computer
  </FileTrigger>
</DropZone>
Show CSS
.react-aria-Link {
  --focus-ring-color: slateblue;
  --text-color: var(--spectrum-global-color-blue-600);
  --text-color-pressed: var(--spectrum-global-color-blue-700);
  --text-color-disabled: gray;

  color: var(--text-color);
  font-size: 18px;
  transition: all 200ms;
  text-decoration: underline;
  cursor: pointer;
  outline: none;
  position: relative;

  &[data-hovered] {
    text-decoration-style: wavy;
  }

  &[data-pressed] {
    color: var(--text-color-pressed);
  }

  &[data-focus-visible]:after {
    content: '';
    position: absolute;
    inset: -3px -6px;
    border-radius: 6px;
    border: 2px solid var(--focus-ring-color);
  }
}
.react-aria-Link {
  --focus-ring-color: slateblue;
  --text-color: var(--spectrum-global-color-blue-600);
  --text-color-pressed: var(--spectrum-global-color-blue-700);
  --text-color-disabled: gray;

  color: var(--text-color);
  font-size: 18px;
  transition: all 200ms;
  text-decoration: underline;
  cursor: pointer;
  outline: none;
  position: relative;

  &[data-hovered] {
    text-decoration-style: wavy;
  }

  &[data-pressed] {
    color: var(--text-color-pressed);
  }

  &[data-focus-visible]:after {
    content: '';
    position: absolute;
    inset: -3px -6px;
    border-radius: 6px;
    border: 2px solid var(--focus-ring-color);
  }
}
.react-aria-Link {
  --focus-ring-color: slateblue;
  --text-color: var(--spectrum-global-color-blue-600);
  --text-color-pressed: var(--spectrum-global-color-blue-700);
  --text-color-disabled: gray;

  color: var(--text-color);
  font-size: 18px;
  transition: all 200ms;
  text-decoration: underline;
  cursor: pointer;
  outline: none;
  position: relative;

  &[data-hovered] {
    text-decoration-style: wavy;
  }

  &[data-pressed] {
    color: var(--text-color-pressed);
  }

  &[data-focus-visible]:after {
    content: '';
    position: absolute;
    inset: -3px -6px;
    border-radius: 6px;
    border: 2px solid var(--focus-ring-color);
  }
}

Visual feedback#

A dropzone can provide visual feedback to the user when a drag hovers over the drop target by passing the getDropOpertation function. If a drop target only supports data of specific types (e.g. images, videos, text, etc.), then it should implement getDropOperation 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.

<DropZone
  getDropOperation={(types) =>  types.has('image/png') ? 'copy' : 'cancel'} >
  Drop files here
</DropZone>
<DropZone
  getDropOperation={(types) =>
    types.has('image/png') ? 'copy' : 'cancel'}
>
  Drop files here
</DropZone>
<DropZone
  getDropOperation={(
    types
  ) =>
    types.has(
        'image/png'
      )
      ? 'copy'
      : 'cancel'}
>
  Drop files here
</DropZone>

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.