diff --git a/README.md b/README.md
index 9b500c1..fbcfc2d 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,7 @@ One way to ensure all dependencies are loaded is to use the [`@wordpress/depende
- [`ConditionalComponent`](src/components/ConditionalComponent)
- [`FetchAllTermSelectControl`](src/components/FetchAllTermSelectControl)
- [`FileControls`](src/components/FileControls)
+- [`HeadingSelectControl`](src/components/HeadingSelectControl)
- [`ImageControl`](src/components/ImageControl)
- [`InnerBlockSlider`](src/components/InnerBlockSlider)
- [`LinkToolbar`](src/components/LinkToolbar)
diff --git a/assets/images/heading-select-control--custom.png b/assets/images/heading-select-control--custom.png
new file mode 100644
index 0000000..53395aa
Binary files /dev/null and b/assets/images/heading-select-control--custom.png differ
diff --git a/assets/images/heading-select-control--default.png b/assets/images/heading-select-control--default.png
new file mode 100644
index 0000000..e5d3356
Binary files /dev/null and b/assets/images/heading-select-control--default.png differ
diff --git a/src/components/HeadingSelectControl/README.md b/src/components/HeadingSelectControl/README.md
new file mode 100644
index 0000000..19eae9f
--- /dev/null
+++ b/src/components/HeadingSelectControl/README.md
@@ -0,0 +1,153 @@
+# HeadingSelectControl
+
+The `HeadingSelectControl` component allows for choosing one of a pre-defined list of heading levels.
+It is intended to be used for blocks or plugin sidebars where some text element needs to be rendered as a user-defined heading.
+The component wraps a regular [`SelectControl`](https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/select-control/index.tsx) component.
+
+| ![heading-select-control--default.png](../../../assets/images/heading-select-control--default.png) |
+|----------------------------------------------------------------------------------------------------|
+| _`HeadingSelectControl` component._ |
+
+| ![heading-select-control--custom.png](../../../assets/images/heading-select-control--custom.png) |
+|--------------------------------------------------------------------------------------------------|
+| _`HeadingSelectControl` component with custom `min` and `max` value specified._ |
+
+## Usage
+
+For a minimum working setup, all you need to do is pass a heading level as `value` to `HeadingSelectControl`, as well as an `onChange` callback that accepts a heading level.
+
+```js
+import { HeadingSelectControl } from '@humanmade/block-editor-components';
+import { InspectorControls } from '@wordpress/block-editor';
+
+function BlockEdit( props ) {
+ const { attributes, setAttributes } = props;
+ const { deckLevel } = attributes;
+
+ return (
+
+ setAttributes( { deckLevel } ) }
+ />
+
+ );
+}
+```
+
+Additionally, you can also specify a minimum and/or maximum heading level by passing `min` and `max`, respectively.
+For accessibility reasons, the default minimum heading level is set to `2`, so if you want to allow for selecting a Heading 1, you have to explicitly pass `min={ 1 }`.
+
+```js
+import { HeadingSelectControl } from '@humanmade/block-editor-components';
+import { InspectorControls } from '@wordpress/block-editor';
+
+function BlockEdit( props ) {
+ const { attributes, setAttributes } = props;
+ const { deckLevel } = attributes;
+
+ return (
+
+ setAttributes( { deckLevel } ) }
+ />
+
+ );
+}
+```
+
+Since the component only allows for selecting a heading level, but does not actually render any heading element, you can also allow for something like a Heading 7 or Heading 8, if you really need to.
+The HTML specification includes dedicated tags for 6 headings only, but sometimes editorial teams use special fake or pseudo headings, which will then end up in them having more than just 6 heading levels.
+By passing a number greater than 6 to `max`, you can allow for that, and then handle rendering a Heading 7 or so, for example, as a paragraph with a custom class.
+
+```js
+import { HeadingSelectControl } from '@humanmade/block-editor-components';
+import { InspectorControls } from '@wordpress/block-editor';
+
+function BlockEdit( props ) {
+ const { attributes, setAttributes } = props;
+ const { level } = attributes;
+
+ return (
+
+ setAttributes( { level } ) }
+ />
+
+ );
+}
+```
+
+Also, you can pass a custom `createLabel` function that takes a numeric heading level and returns the label to use for the heading.
+
+```js
+import { HeadingSelectControl } from '@humanmade/block-editor-components';
+import { InspectorControls } from '@wordpress/block-editor';
+
+function createLabel( level ) {
+ if ( level === 8 ) {
+ return 'Subheading';
+ }
+
+ return `Heading ${ level }`;
+}
+
+function BlockEdit( props ) {
+ const { attributes, setAttributes } = props;
+ const { level } = attributes;
+
+ return (
+
+ setAttributes( { level } ) }
+ />
+
+ );
+}
+```
+
+## Props
+
+The `HeadingSelectControl` component does not have any custom props other than the optional `max` and `min`, but you can pass anything that is supported by the nested [`SelectControl`](https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/select-control/index.tsx) component.
+
+### `createLabel`
+
+An optional function to create the label for the heading with the given level.
+The first and only argument passed to the function is the numeric heading level.
+
+| Type | Required | Default |
+|------------------------------|------------------------------|------------------------------------------------------|
+| `Function` | no | `( level ) => sprintf( __( 'Heading %d' ), level )` |
+
+### `max`
+
+An optional maximum heading level.
+
+| Type | Required | Default |
+|--------------------------------------|--------------------------------------|--------------------------------------|
+| `number` | no | `6` |
+
+### `min`
+
+An optional minimum heading level.
+This value is also being used as default value.
+
+| Type | Required | Default |
+|--------------------------------------|--------------------------------------|--------------------------------------|
+| `number` | no | `2` |
+
+## Dependencies
+
+The `HeadingSelectControl` component requires the following dependencies, which are expected to be available:
+
+- `@wordpress/components`
+- `@wordpress/i18n`
diff --git a/src/components/HeadingSelectControl/index.js b/src/components/HeadingSelectControl/index.js
new file mode 100644
index 0000000..7271e4f
--- /dev/null
+++ b/src/components/HeadingSelectControl/index.js
@@ -0,0 +1,64 @@
+import React, { ReactNode, useMemo } from 'react';
+
+import { SelectControl } from '@wordpress/components';
+import { __, sprintf } from '@wordpress/i18n';
+
+/**
+ * Get the label for the heading with the given level.
+ *
+ * @param {number} level - Heading level.
+ * @returns {string} Label.
+ */
+function _createLabel( level ) {
+ return sprintf(
+ // translators: %s: heading level (e.g.: 1, 2, 3).
+ __( 'Heading %d', 'block-editor-components' ),
+ level
+ );
+}
+
+/**
+ * A dropdown control that allows for selecting a heading level.
+ *
+ * @param {object} props - Component props.
+ * @returns {ReactNode} Component.
+ */
+function HeadingSelectControl( props ) {
+ const {
+ createLabel = _createLabel,
+ max = 6,
+ min = 2,
+ onChange,
+ value = min,
+ ...selectProps
+ } = props;
+
+ const options = useMemo(
+ () => {
+ if ( min > max ) {
+ return undefined;
+ }
+
+ return Array( max - min + 1 ).fill().map( ( _, index ) => {
+ const level = min + index;
+
+ return {
+ label: createLabel( level ),
+ value: level,
+ };
+ } );
+ },
+ [ createLabel, max, min ]
+ );
+
+ return (
+ onChange( Number( value ) ) }
+ />
+ );
+}
+
+export default HeadingSelectControl;