diff --git a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap
new file mode 100644
index 0000000000000..d18a2db614f55
--- /dev/null
+++ b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap
@@ -0,0 +1,20 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DragDrop droppable is reflected in the className 1`] = `
+
+ Hello!
+
+`;
+
+exports[`DragDrop renders if nothing is being dragged 1`] = `
+
+ Hello!
+
+`;
diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss
new file mode 100644
index 0000000000000..f0b3238f76f2e
--- /dev/null
+++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss
@@ -0,0 +1,7 @@
+.lnsDragDrop-isDropTarget {
+ background-color: transparentize($euiColorSecondary, .9);
+}
+
+.lnsDragDrop-isActiveDropTarget {
+ background-color: transparentize($euiColorSecondary, .75);
+}
diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx
new file mode 100644
index 0000000000000..9e78c9b48bf2b
--- /dev/null
+++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx
@@ -0,0 +1,104 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { render, shallow, mount } from 'enzyme';
+import { DragDrop } from './drag_drop';
+import { ChildDragDropProvider } from './providers';
+
+jest.useFakeTimers();
+
+describe('DragDrop', () => {
+ test('renders if nothing is being dragged', () => {
+ const component = render(
+
+ Hello!
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('dragover calls preventDefault if droppable is true', () => {
+ const preventDefault = jest.fn();
+ const component = shallow(Hello!);
+
+ component.find('[data-test-subj="lnsDragDrop"]').simulate('dragover', { preventDefault });
+
+ expect(preventDefault).toBeCalled();
+ });
+
+ test('dragover does not call preventDefault if droppable is false', () => {
+ const preventDefault = jest.fn();
+ const component = shallow(Hello!);
+
+ component.find('[data-test-subj="lnsDragDrop"]').simulate('dragover', { preventDefault });
+
+ expect(preventDefault).not.toBeCalled();
+ });
+
+ test('dragstart sets dragging in the context', async () => {
+ const setDragging = jest.fn();
+ const dataTransfer = {
+ setData: jest.fn(),
+ getData: jest.fn(),
+ };
+ const value = {};
+
+ const component = mount(
+
+ Hello!
+
+ );
+
+ component.find('[data-test-subj="lnsDragDrop"]').simulate('dragstart', { dataTransfer });
+
+ jest.runAllTimers();
+
+ expect(dataTransfer.setData).toBeCalledWith('text', 'dragging');
+ expect(setDragging).toBeCalledWith(value);
+ });
+
+ test('drop resets all the things', async () => {
+ const preventDefault = jest.fn();
+ const stopPropagation = jest.fn();
+ const setDragging = jest.fn();
+ const onDrop = jest.fn();
+ const value = {};
+
+ const component = mount(
+
+
+ Hello!
+
+
+ );
+
+ component
+ .find('[data-test-subj="lnsDragDrop"]')
+ .simulate('drop', { preventDefault, stopPropagation });
+
+ expect(preventDefault).toBeCalled();
+ expect(stopPropagation).toBeCalled();
+ expect(setDragging).toBeCalledWith(undefined);
+ expect(onDrop).toBeCalledWith('hola');
+ });
+
+ test('droppable is reflected in the className', () => {
+ const component = render(
+ {
+ throw x;
+ }}
+ droppable
+ >
+ Hello!
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx
new file mode 100644
index 0000000000000..35326a46a6820
--- /dev/null
+++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx
@@ -0,0 +1,138 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState, useContext } from 'react';
+import classNames from 'classnames';
+import { DragContext } from './providers';
+
+type DroppableEvent = React.DragEvent;
+
+/**
+ * A function that handles a drop event.
+ */
+type DropHandler = (item: unknown) => void;
+
+/**
+ * The argument to the DragDrop component.
+ */
+interface Props {
+ /**
+ * The CSS class(es) for the root element.
+ */
+ className?: string;
+
+ /**
+ * The event handler that fires when an item
+ * is dropped onto this DragDrop component.
+ */
+ onDrop?: DropHandler;
+
+ /**
+ * The value associated with this item, if it is draggable.
+ * If this component is dragged, this will be the value of
+ * "dragging" in the root drag/drop context.
+ */
+ value?: unknown;
+
+ /**
+ * The React children.
+ */
+ children: React.ReactNode;
+
+ /**
+ * Indicates whether or not the currently dragged item
+ * can be dropped onto this component.
+ */
+ droppable?: boolean;
+
+ /**
+ * Indicates whether or not this component is draggable.
+ */
+ draggable?: boolean;
+}
+
+/**
+ * A draggable / droppable item. Items can be both draggable and droppable at
+ * the same time.
+ *
+ * @param props
+ */
+export function DragDrop(props: Props) {
+ const { dragging, setDragging } = useContext(DragContext);
+ const [state, setState] = useState({ isActive: false });
+ const { className, onDrop, value, children, droppable, draggable } = props;
+ const isDragging = draggable && value === dragging;
+
+ const classes = classNames('lnsDragDrop', className, {
+ 'lnsDragDrop-isDropTarget': droppable,
+ 'lnsDragDrop-isActiveDropTarget': droppable && state.isActive,
+ 'lnsDragDrop-isDragging': isDragging,
+ });
+
+ const dragStart = (e: DroppableEvent) => {
+ // Setting stopPropgagation causes Chrome failures, so
+ // we are manually checking if we've already handled this
+ // in a nested child, and doing nothing if so...
+ if (e.dataTransfer.getData('text')) {
+ return;
+ }
+
+ e.dataTransfer.setData('text', 'dragging');
+
+ // Chrome causes issues if you try to render from within a
+ // dragStart event, so we drop a setTimeout to avoid that.
+ setTimeout(() => setDragging(value));
+ };
+
+ const dragEnd = (e: DroppableEvent) => {
+ e.stopPropagation();
+ setDragging(undefined);
+ };
+
+ const dragOver = (e: DroppableEvent) => {
+ if (!droppable) {
+ return;
+ }
+
+ e.preventDefault();
+
+ // An optimization to prevent a bunch of React churn.
+ if (!state.isActive) {
+ setState({ ...state, isActive: true });
+ }
+ };
+
+ const dragLeave = () => {
+ setState({ ...state, isActive: false });
+ };
+
+ const drop = (e: DroppableEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ setState({ ...state, isActive: false });
+ setDragging(undefined);
+
+ if (onDrop) {
+ onDrop(dragging);
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/x-pack/plugins/lens/public/drag_drop/index.ts b/x-pack/plugins/lens/public/drag_drop/index.ts
new file mode 100644
index 0000000000000..e597bb8b6e893
--- /dev/null
+++ b/x-pack/plugins/lens/public/drag_drop/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export * from './providers';
+export * from './drag_drop';
diff --git a/x-pack/plugins/lens/public/drag_drop/providers.tsx b/x-pack/plugins/lens/public/drag_drop/providers.tsx
new file mode 100644
index 0000000000000..c0b4eb563b32b
--- /dev/null
+++ b/x-pack/plugins/lens/public/drag_drop/providers.tsx
@@ -0,0 +1,85 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState } from 'react';
+
+/**
+ * The shape of the drag / drop context.
+ */
+export interface DragContextState {
+ /**
+ * The item being dragged or undefined.
+ */
+ dragging: unknown;
+
+ /**
+ * Set the item being dragged.
+ */
+ setDragging: (dragging: unknown) => void;
+}
+
+/**
+ * The drag / drop context singleton, used like so:
+ *
+ * const { dragging, setDragging } = useContext(DragContext);
+ */
+export const DragContext = React.createContext({
+ dragging: undefined,
+ setDragging: () => {},
+});
+
+/**
+ * The argument to DragDropProvider.
+ */
+export interface ProviderProps {
+ /**
+ * The item being dragged. If unspecified, the provider will
+ * behave as if it is the root provider.
+ */
+ dragging: unknown;
+
+ /**
+ * Sets the item being dragged. If unspecified, the provider
+ * will behave as if it is the root provider.
+ */
+ setDragging: (dragging: unknown) => void;
+
+ /**
+ * The React children.
+ */
+ children: React.ReactNode;
+}
+
+/**
+ * A React provider that tracks the dragging state. This should
+ * be placed at the root of any React application that supports
+ * drag / drop.
+ *
+ * @param props
+ */
+export function RootDragDropProvider({ children }: { children: React.ReactNode }) {
+ const [state, setState] = useState<{ dragging: unknown }>({
+ dragging: undefined,
+ });
+ const setDragging = (dragging: unknown) => setState({ dragging });
+
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * A React drag / drop provider that derives its state from a RootDragDropProvider. If
+ * part of a React application is rendered separately from the root, this provider can
+ * be used to enable drag / drop functionality within the disconnected part.
+ *
+ * @param props
+ */
+export function ChildDragDropProvider({ dragging, setDragging, children }: ProviderProps) {
+ return {children};
+}
diff --git a/x-pack/plugins/lens/public/drag_drop/readme.md b/x-pack/plugins/lens/public/drag_drop/readme.md
new file mode 100644
index 0000000000000..8d11cb6226927
--- /dev/null
+++ b/x-pack/plugins/lens/public/drag_drop/readme.md
@@ -0,0 +1,69 @@
+# Drag / Drop
+
+This is a simple drag / drop mechanism that plays nice with React.
+
+We aren't using EUI or another library, due to the fact that Lens visualizations and datasources may or may not be written in React. Even visualizations which are written in React will end up having their own ReactDOM.render call, and in that sense will be a standalone React application. We want to enable drag / drop across React and native DOM boundaries.
+
+## Getting started
+
+First, place a RootDragDropProvider at the root of your application.
+
+```js
+
+ ... your app here ...
+
+```
+
+If you have a child React application (e.g. a visualization), you will need to pass the drag / drop context down into it. This can be obtained like so:
+
+```js
+const context = useContext(DragContext);
+```
+
+In your child application, place a `ChildDragDropProvider` at the root of that, and spread the context into it:
+
+```js
+
+ ... your child app here ...
+
+```
+
+This enables your child application to share the same drag / drop context as the root application.
+
+## Dragging
+
+An item can be both draggable and droppable at the same time, but for simplicity's sake, we'll treat these two cases separately.
+
+To enable dragging an item, use `DragDrop` with both a `draggable` and a `value` attribute.
+
+```js
+
+ {fields.map(f => (
+
+ {f.name}
+
+ ))}
+
+```
+
+## Dropping
+
+To enable dropping, use `DragDrop` with both a `droppable` attribute and an `onDrop` handler attribute. Droppable should only be set to true if there is an item being dragged, and if a drop of the dragged item is supported.
+
+```js
+const { dragging } = useContext(DragContext);
+
+return (
+ onChange([...items, item])}
+ >
+ {items.map(x => {x.name}
)}
+
+);
+```
+
+## Limitations
+
+Currently this is a very simple drag / drop mechanism. We don't support reordering out of the box, though it could probably be built on top of this solution without modification of the core.