diff --git a/docs/manifest-devhub.json b/docs/manifest-devhub.json
index b508f649fb7c9..a8d1a369642ee 100644
--- a/docs/manifest-devhub.json
+++ b/docs/manifest-devhub.json
@@ -641,6 +641,12 @@
"markdown_source": "../packages/components/src/date-time/README.md",
"parent": "components"
},
+ {
+ "title": "DimensionControl",
+ "slug": "dimension-control",
+ "markdown_source": "../packages/components/src/dimension-control/README.md",
+ "parent": "components"
+ },
{
"title": "Disabled",
"slug": "disabled",
diff --git a/packages/components/src/dimension-control/README.md b/packages/components/src/dimension-control/README.md
new file mode 100644
index 0000000000000..435fdf4e73b04
--- /dev/null
+++ b/packages/components/src/dimension-control/README.md
@@ -0,0 +1,120 @@
+DimensionControl
+=============================
+
+`DimensionControl` is a component designed to provide a UI to control spacing and/or dimensions.
+
+## Usage
+
+In a block's `edit` implementation, render a `` component.
+
+
+```jsx
+import { registerBlockType } from '@wordpress/blocks';
+import { __ } from '@wordpress/i18n';
+import {
+ DimensionControl,
+} from '@wordpress/block-editor';
+
+registerBlockType( 'my-plugin/my-block', {
+ // ...
+
+ attributes: {
+ // other attributes here
+ // ...
+
+ paddingSize: {
+ type: 'string',
+ },
+ },
+
+ edit( { attributes, setAttributes, clientId } ) {
+
+ const { paddingSize } = attributes;
+
+
+ const updateSpacing = ( dimension, size, device = '' ) => {
+ setAttributes( {
+ [ `${ dimension }${ device }` ]: size,
+ } );
+ };
+
+ return (
+
+ );
+ }
+} );
+```
+
+_Note:_ it is recommended to partially apply the value of the Block attribute to be updated (eg: `paddingSize`, `marginSize`...etc) to your callback functions. This avoids the need to unnecessarily couple the component to the Block attribute schema.
+
+_Note:_ by default, if you do not provide an initial `value` prop for the current dimension value, then no value will be selected (ie: there is no default dimension set).
+
+## Props
+
+### `label`
+* **Type:** `String`
+* **Default:** `undefined`
+* **Required:** Yes
+
+The human readable label for the control.
+
+### `value`
+* **Type:** `String`
+* **Default:** `''`
+* **Required:** No
+
+The current value of the dimension UI control. If provided the UI with automatically select the value.
+
+### `sizes`
+* **Type:** `Array`
+* **Default:** See `packages/block-editor/src/components/dimension-control/sizes.js`
+* **Required:** No
+
+An optional array of size objects in the following shape:
+
+```
+[
+ {
+ name: __( 'Small' ),
+ slug: 'small',
+ },
+ {
+ name: __( 'Medium' ),
+ slug: 'small',
+ },
+ // ...etc
+]
+```
+
+By default a set of relative sizes (`small`, `medium`...etc) are provided. See `packages/block-editor/src/components/dimension-control/sizes.js`.
+
+### `icon`
+* **Type:** `String`
+* **Default:** `undefined`
+* **Required:** No
+
+An optional dashicon to display before to the control label.
+
+### `onChange`
+* **Type:** `Function`
+* **Default:** `undefined`
+* **Required:** No
+* **Arguments:**:
+ - `size` - a string representing the selected size (eg: `medium`)
+
+A callback which is triggered when a spacing size value changes (is selected/clicked).
+
+
+### `className`
+* **Type:** `String`
+* **Default:** `''`
+* **Required:** No
+
+A string of classes to be added to the control component.
+
+
diff --git a/packages/components/src/dimension-control/index.js b/packages/components/src/dimension-control/index.js
new file mode 100644
index 0000000000000..c69b80f5c49e0
--- /dev/null
+++ b/packages/components/src/dimension-control/index.js
@@ -0,0 +1,76 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+import { isFunction } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+/**
+ * Internal dependencies
+ */
+import {
+ Icon,
+ SelectControl,
+} from '../';
+import { __ } from '@wordpress/i18n';
+
+import {
+ Fragment,
+} from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import sizesTable, { findSizeBySlug } from './sizes';
+
+export function DimensionControl( props ) {
+ const { label, value, sizes = sizesTable, icon, onChange, className = '' } = props;
+
+ const onChangeSpacingSize = ( val ) => {
+ const theSize = findSizeBySlug( sizes, val );
+
+ if ( ! theSize || value === theSize.slug ) {
+ onChange( undefined );
+ } else if ( isFunction( onChange ) ) {
+ onChange( theSize.slug );
+ }
+ };
+
+ const formatSizesAsOptions = ( theSizes ) => {
+ const options = theSizes.map( ( { name, slug } ) => ( {
+ label: name,
+ value: slug,
+ } ) );
+
+ return [ {
+ label: __( 'Default' ),
+ value: '',
+ } ].concat( options );
+ };
+
+ const selectLabel = (
+
+ { icon && (
+
+ ) }
+ { label }
+
+ );
+
+ return (
+
+ );
+}
+
+export default DimensionControl;
diff --git a/packages/components/src/dimension-control/sizes.js b/packages/components/src/dimension-control/sizes.js
new file mode 100644
index 0000000000000..41fa071702872
--- /dev/null
+++ b/packages/components/src/dimension-control/sizes.js
@@ -0,0 +1,45 @@
+/**
+ * Sizes
+ *
+ * defines the sizes used in dimension controls
+ * all hardcoded `size` values are based on the value of
+ * the Sass variable `$block-padding` from
+ * `packages/block-editor/src/components/dimension-control/sizes.js`.
+ */
+
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Finds the correct size object from the provided sizes
+ * table by size slug (eg: `medium`)
+ *
+ * @param {Array} sizes containing objects for each size definition
+ * @param {string} slug a string representation of the size (eg: `medium`)
+ * @return {Object} the matching size definition
+ */
+export const findSizeBySlug = ( sizes, slug ) => sizes.find( ( size ) => slug === size.slug );
+
+export default [
+ {
+ name: __( 'None' ),
+ slug: 'none',
+ },
+ {
+ name: __( 'Small' ),
+ slug: 'small',
+ },
+ {
+ name: __( 'Medium' ),
+ slug: 'medium',
+ },
+ {
+ name: __( 'Large' ),
+ slug: 'large',
+ }, {
+ name: __( 'Extra Large' ),
+ slug: 'xlarge',
+ },
+];
diff --git a/packages/components/src/dimension-control/style.scss b/packages/components/src/dimension-control/style.scss
new file mode 100644
index 0000000000000..7f1481747dfe1
--- /dev/null
+++ b/packages/components/src/dimension-control/style.scss
@@ -0,0 +1,22 @@
+.block-editor-dimension-control {
+
+ .components-base-control__field {
+ display: flex;
+ align-items: center;
+ }
+
+ .components-base-control__label {
+ display: flex;
+ align-items: center;
+ margin-right: 1em;
+ margin-bottom: 0;
+
+ .dashicon {
+ margin-right: 0.5em;
+ }
+ }
+
+ &.is-manual .components-base-control__label {
+ width: 10em;
+ }
+}
diff --git a/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap b/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap
new file mode 100644
index 0000000000000..c06662ee862c9
--- /dev/null
+++ b/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap
@@ -0,0 +1,163 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DimensionControl rendering renders with custom sizes 1`] = `
+
+ Custom Dimension
+
+ }
+ onChange={[Function]}
+ options={
+ Array [
+ Object {
+ "label": "Default",
+ "value": "",
+ },
+ Object {
+ "label": "Mini",
+ "value": "mini",
+ },
+ Object {
+ "label": "Middle",
+ "value": "middle",
+ },
+ Object {
+ "label": "Giant",
+ "value": "giant",
+ },
+ ]
+ }
+/>
+`;
+
+exports[`DimensionControl rendering renders with defaults 1`] = `
+
+ Padding
+
+ }
+ onChange={[Function]}
+ options={
+ Array [
+ Object {
+ "label": "Default",
+ "value": "",
+ },
+ Object {
+ "label": "None",
+ "value": "none",
+ },
+ Object {
+ "label": "Small",
+ "value": "small",
+ },
+ Object {
+ "label": "Medium",
+ "value": "medium",
+ },
+ Object {
+ "label": "Large",
+ "value": "large",
+ },
+ Object {
+ "label": "Extra Large",
+ "value": "xlarge",
+ },
+ ]
+ }
+/>
+`;
+
+exports[`DimensionControl rendering renders with icon and custom icon label 1`] = `
+
+
+ Margin
+
+ }
+ onChange={[Function]}
+ options={
+ Array [
+ Object {
+ "label": "Default",
+ "value": "",
+ },
+ Object {
+ "label": "None",
+ "value": "none",
+ },
+ Object {
+ "label": "Small",
+ "value": "small",
+ },
+ Object {
+ "label": "Medium",
+ "value": "medium",
+ },
+ Object {
+ "label": "Large",
+ "value": "large",
+ },
+ Object {
+ "label": "Extra Large",
+ "value": "xlarge",
+ },
+ ]
+ }
+/>
+`;
+
+exports[`DimensionControl rendering renders with icon and default icon label 1`] = `
+
+
+ Margin
+
+ }
+ onChange={[Function]}
+ options={
+ Array [
+ Object {
+ "label": "Default",
+ "value": "",
+ },
+ Object {
+ "label": "None",
+ "value": "none",
+ },
+ Object {
+ "label": "Small",
+ "value": "small",
+ },
+ Object {
+ "label": "Medium",
+ "value": "medium",
+ },
+ Object {
+ "label": "Large",
+ "value": "large",
+ },
+ Object {
+ "label": "Extra Large",
+ "value": "xlarge",
+ },
+ ]
+ }
+/>
+`;
diff --git a/packages/components/src/dimension-control/test/index.test.js b/packages/components/src/dimension-control/test/index.test.js
new file mode 100644
index 0000000000000..0b053a493ee12
--- /dev/null
+++ b/packages/components/src/dimension-control/test/index.test.js
@@ -0,0 +1,128 @@
+/**
+ * External dependencies
+ */
+import { shallow, mount } from 'enzyme';
+import { uniqueId } from 'lodash';
+
+/**
+ * Internal dependencies
+ */
+import { DimensionControl } from '../';
+
+describe( 'DimensionControl', () => {
+ const onChangeHandler = jest.fn();
+
+ afterEach( () => {
+ onChangeHandler.mockClear();
+ } );
+
+ describe( 'rendering', () => {
+ it( 'renders with defaults', () => {
+ const wrapper = shallow(
+
+ );
+ expect( wrapper ).toMatchSnapshot();
+ } );
+
+ it( 'renders with icon and default icon label', () => {
+ const wrapper = shallow(
+
+ );
+ expect( wrapper ).toMatchSnapshot();
+ } );
+
+ it( 'renders with icon and custom icon label', () => {
+ const wrapper = shallow(
+
+ );
+ expect( wrapper ).toMatchSnapshot();
+ } );
+
+ it( 'renders with custom sizes', () => {
+ const customSizes = [
+ {
+ name: 'Mini',
+ size: 1,
+ slug: 'mini',
+ },
+ {
+ name: 'Middle',
+ size: 5,
+ slug: 'middle',
+ },
+ {
+ name: 'Giant',
+ size: 10,
+ slug: 'giant',
+ },
+ ];
+
+ const wrapper = shallow(
+
+ );
+ expect( wrapper ).toMatchSnapshot();
+ } );
+ } );
+
+ describe( 'callbacks', () => {
+ it( 'should call onChange handler with correct args on size change', () => {
+ const wrapper = mount(
+
+ );
+
+ wrapper.find( 'select' ).at( 0 ).simulate( 'change', {
+ target: {
+ value: 'small',
+ },
+ } );
+
+ wrapper.find( 'select' ).at( 0 ).simulate( 'change', {
+ target: {
+ value: 'medium',
+ },
+ } );
+
+ expect( onChangeHandler ).toHaveBeenCalledTimes( 2 );
+ expect( onChangeHandler.mock.calls[ 0 ][ 0 ] ).toEqual( 'small' );
+ expect( onChangeHandler.mock.calls[ 1 ][ 0 ] ).toEqual( 'medium' );
+ } );
+
+ it( 'should call onChange handler with undefined value when no size is provided on change', () => {
+ const wrapper = mount(
+
+ );
+
+ wrapper.find( 'select' ).at( 0 ).simulate( 'change', {
+ target: {
+ value: '', // this happens when you select the "default"
+ },
+ } );
+
+ expect( onChangeHandler ).toHaveBeenNthCalledWith( 1, undefined );
+ } );
+ } );
+} );
diff --git a/packages/components/src/index.js b/packages/components/src/index.js
index 463241f06bf66..ac9694a49ba57 100644
--- a/packages/components/src/index.js
+++ b/packages/components/src/index.js
@@ -12,6 +12,7 @@ export { default as ColorPalette } from './color-palette';
export { default as ColorPicker } from './color-picker';
export { default as Dashicon } from './dashicon';
export { DateTimePicker, DatePicker, TimePicker } from './date-time';
+export { default as __experimentalDimensionControl } from './dimension-control';
export { default as Disabled } from './disabled';
export { default as Draggable } from './draggable';
export { default as DropZone } from './drop-zone';
diff --git a/packages/components/src/style.scss b/packages/components/src/style.scss
index 08fc9961be5e6..7a62ba0534539 100644
--- a/packages/components/src/style.scss
+++ b/packages/components/src/style.scss
@@ -9,6 +9,7 @@
@import "./color-picker/style.scss";
@import "./dashicon/style.scss";
@import "./date-time/style.scss";
+@import "./dimension-control/style.scss";
@import "./disabled/style.scss";
@import "./draggable/style.scss";
@import "./drop-zone/style.scss";