diff --git a/packages/interface/src/components/index.js b/packages/interface/src/components/index.js
index 27b41f4ecb999..aa03fa34ae8c6 100644
--- a/packages/interface/src/components/index.js
+++ b/packages/interface/src/components/index.js
@@ -6,3 +6,7 @@ export { default as PinnedItems } from './pinned-items';
export { default as MoreMenuDropdown } from './more-menu-dropdown';
export { default as MoreMenuFeatureToggle } from './more-menu-feature-toggle';
export { default as ActionItem } from './action-item';
+export { default as PreferencesModal } from './preferences-modal';
+export { default as PreferencesModalTabs } from './preferences-modal-tabs';
+export { default as PreferencesModalSection } from './preferences-modal-section';
+export { default as ___unstablePreferencesModalBaseOption } from './preferences-modal-base-option';
diff --git a/packages/interface/src/components/preferences-modal-base-option/README.md b/packages/interface/src/components/preferences-modal-base-option/README.md
new file mode 100644
index 0000000000000..03c89960b6850
--- /dev/null
+++ b/packages/interface/src/components/preferences-modal-base-option/README.md
@@ -0,0 +1,42 @@
+#__unstablePreferencesModalBaseOption
+
+`__unstablePreferencesModalBaseOption` renders a toggle meant to be used with `PreferencesModal`.
+
+This component implements a `ToggleControl` component from the `@wordpress/components` package.
+
+**It is an unstable component so is subject to breaking changes at any moment. Use at own risk.**
+
+## Example
+
+```jsx
+function MyEditorPreferencesOption() {
+ return (
+ <__unstablePreferencesModalBaseOption
+ label={ label }
+ isChecked={ isChecked }
+ onChange={ setIsChecked }
+ >
+ { isChecked !== areCustomFieldsEnabled && (
+
+ ) }
+
+ )
+}
+```
+
+## Props
+
+### help
+### label
+### isChecked
+### onChange
+
+These props are passed directly to ToggleControl, so see [ToggleControl readme](https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/toggle-control/README.md) for more info.
+
+### children
+
+Components to be rendered as content.
+
+- Type: `Element`
+- Required: No.
+
diff --git a/packages/interface/src/components/preferences-modal-base-option/index.js b/packages/interface/src/components/preferences-modal-base-option/index.js
new file mode 100644
index 0000000000000..bf027b17354a2
--- /dev/null
+++ b/packages/interface/src/components/preferences-modal-base-option/index.js
@@ -0,0 +1,20 @@
+/**
+ * WordPress dependencies
+ */
+import { ToggleControl } from '@wordpress/components';
+
+function BaseOption( { help, label, isChecked, onChange, children } ) {
+ return (
+
+
+ { children }
+
+ );
+}
+
+export default BaseOption;
diff --git a/packages/interface/src/components/preferences-modal-base-option/style.scss b/packages/interface/src/components/preferences-modal-base-option/style.scss
new file mode 100644
index 0000000000000..88143354a497b
--- /dev/null
+++ b/packages/interface/src/components/preferences-modal-base-option/style.scss
@@ -0,0 +1,21 @@
+.interface-preferences-modal__option {
+ .components-base-control {
+ .components-base-control__field {
+ align-items: center;
+ display: flex;
+ margin-bottom: 0;
+
+ & > label {
+ flex-grow: 1;
+ padding: 0.6rem 0 0.6rem 10px;
+ }
+ }
+ }
+
+ .components-base-control__help {
+ margin: -$grid-unit-10 0 $grid-unit-10 58px;
+ font-size: $helptext-font-size;
+ font-style: normal;
+ color: $gray-700;
+ }
+}
diff --git a/packages/interface/src/components/preferences-modal-section/README.md b/packages/interface/src/components/preferences-modal-section/README.md
new file mode 100644
index 0000000000000..6e78ca05c0aa8
--- /dev/null
+++ b/packages/interface/src/components/preferences-modal-section/README.md
@@ -0,0 +1,30 @@
+
+`PreferencesModalSection` renders a section (as a fieldset) meant to be used with `PreferencesModal`.
+
+## Example
+
+See the `PreferencesModal` readme for usage info.
+
+
+## Props
+
+### title
+
+The title of the section
+
+- Type: `String`
+- Required: Yes.
+
+### description
+
+The description for the section.
+
+- Type: `String`
+- Required: No.
+
+### children
+
+Components to be rendered as content.
+
+- Type: `Element`
+- Required: Yes.
diff --git a/packages/interface/src/components/preferences-modal-section/index.js b/packages/interface/src/components/preferences-modal-section/index.js
new file mode 100644
index 0000000000000..3ad08b557ff85
--- /dev/null
+++ b/packages/interface/src/components/preferences-modal-section/index.js
@@ -0,0 +1,17 @@
+const Section = ( { description, title, children } ) => (
+
+);
+
+export default Section;
diff --git a/packages/interface/src/components/preferences-modal-section/style.scss b/packages/interface/src/components/preferences-modal-section/style.scss
new file mode 100644
index 0000000000000..135545986836a
--- /dev/null
+++ b/packages/interface/src/components/preferences-modal-section/style.scss
@@ -0,0 +1,20 @@
+.interface-preferences-modal__section {
+ margin: 0 0 2.5rem 0;
+
+ &:last-child {
+ margin: 0;
+ }
+}
+
+.interface-preferences-modal__section-title {
+ font-size: 0.9rem;
+ font-weight: 600;
+ margin-top: 0;
+}
+
+.interface-preferences-modal__section-description {
+ margin: -$grid-unit-10 0 $grid-unit-10 0;
+ font-size: $helptext-font-size;
+ font-style: normal;
+ color: $gray-700;
+}
diff --git a/packages/interface/src/components/preferences-modal-tabs/README.md b/packages/interface/src/components/preferences-modal-tabs/README.md
new file mode 100644
index 0000000000000..e1ffbd86a41b6
--- /dev/null
+++ b/packages/interface/src/components/preferences-modal-tabs/README.md
@@ -0,0 +1,14 @@
+# PreferencesModalTabs
+
+`PreferencesModalTabs` creates a tabbed interface meant to be used inside a `PreferencesModal`. Markup differs between small and large viewports; on small the tabs are closed by default.
+
+## Example
+
+See the `PreferencesModal` readme for usage info.
+## Props
+### sections
+
+Sections to populate the modal with. Takes an array of objects, where each should include `name`, `tablabel` and `content`.
+
+- Type: `Array`
+- Required: Yes.
\ No newline at end of file
diff --git a/packages/interface/src/components/preferences-modal-tabs/index.js b/packages/interface/src/components/preferences-modal-tabs/index.js
new file mode 100644
index 0000000000000..bc8f7358b834d
--- /dev/null
+++ b/packages/interface/src/components/preferences-modal-tabs/index.js
@@ -0,0 +1,156 @@
+/**
+ * WordPress dependencies
+ */
+import { useViewportMatch } from '@wordpress/compose';
+import {
+ __experimentalNavigatorProvider as NavigatorProvider,
+ __experimentalNavigatorScreen as NavigatorScreen,
+ __experimentalNavigatorButton as NavigatorButton,
+ __experimentalNavigatorBackButton as NavigatorBackButton,
+ __experimentalItemGroup as ItemGroup,
+ __experimentalItem as Item,
+ __experimentalHStack as HStack,
+ __experimentalText as Text,
+ __experimentalTruncate as Truncate,
+ FlexItem,
+ TabPanel,
+ Card,
+ CardHeader,
+ CardBody,
+} from '@wordpress/components';
+import { useMemo, useCallback, useState } from '@wordpress/element';
+import { chevronLeft, chevronRight, Icon } from '@wordpress/icons';
+import { isRTL, __ } from '@wordpress/i18n';
+
+const PREFERENCES_MENU = 'preferences-menu';
+
+export default function PreferencesModalTabs( { sections } ) {
+ const isLargeViewport = useViewportMatch( 'medium' );
+
+ // This is also used to sync the two different rendered components
+ // between small and large viewports.
+ const [ activeMenu, setActiveMenu ] = useState( PREFERENCES_MENU );
+ /**
+ * Create helper objects from `sections` for easier data handling.
+ * `tabs` is used for creating the `TabPanel` and `sectionsContentMap`
+ * is used for easier access to active tab's content.
+ */
+ const { tabs, sectionsContentMap } = useMemo( () => {
+ let mappedTabs = {
+ tabs: [],
+ sectionsContentMap: {},
+ };
+ if ( sections.length ) {
+ mappedTabs = sections.reduce(
+ ( accumulator, { name, tabLabel: title, content } ) => {
+ accumulator.tabs.push( { name, title } );
+ accumulator.sectionsContentMap[ name ] = content;
+ return accumulator;
+ },
+ { tabs: [], sectionsContentMap: {} }
+ );
+ }
+ return mappedTabs;
+ }, [ sections ] );
+
+ const getCurrentTab = useCallback(
+ ( tab ) => sectionsContentMap[ tab.name ] || null,
+ [ sectionsContentMap ]
+ );
+
+ let modalContent;
+ // We render different components based on the viewport size.
+ if ( isLargeViewport ) {
+ modalContent = (
+
+ { getCurrentTab }
+
+ );
+ } else {
+ modalContent = (
+
+
+
+
+
+ { tabs.map( ( tab ) => {
+ return (
+
+
+
+
+ { tab.title }
+
+
+
+
+
+
+
+ );
+ } ) }
+
+
+
+
+ { sections.length &&
+ sections.map( ( section ) => {
+ return (
+
+
+
+
+
+ { section.tabLabel }
+
+
+ { section.content }
+
+
+ );
+ } ) }
+
+ );
+ }
+
+ return modalContent;
+}
diff --git a/packages/interface/src/components/preferences-modal-tabs/style.scss b/packages/interface/src/components/preferences-modal-tabs/style.scss
new file mode 100644
index 0000000000000..d09c8b3572bd2
--- /dev/null
+++ b/packages/interface/src/components/preferences-modal-tabs/style.scss
@@ -0,0 +1,35 @@
+$vertical-tabs-width: 160px;
+
+.interface-preferences__tabs {
+ .components-tab-panel__tabs {
+ position: absolute;
+ top: $header-height + $grid-unit-30;
+ // Aligns button text instead of button box.
+ left: $grid-unit-20;
+ width: $vertical-tabs-width;
+ .components-tab-panel__tabs-item {
+ border-radius: $radius-block-ui;
+ font-weight: 400;
+ &.is-active {
+ background: $gray-100;
+ box-shadow: none;
+ font-weight: 500;
+ }
+ &:focus:not(:disabled) {
+ box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color);
+ }
+ }
+ }
+ .components-tab-panel__tab-content {
+ padding-left: $grid-unit-30;
+ margin-left: $vertical-tabs-width;
+ }
+}
+
+@media (max-width: #{ ($break-medium - 1) }) {
+ // Keep the navigator component from overflowing the modal content area
+ // to ensure that sticky position elements stick where intended.
+ .interface-preferences__provider {
+ height: 100%;
+ }
+}
diff --git a/packages/interface/src/components/preferences-modal/README.md b/packages/interface/src/components/preferences-modal/README.md
new file mode 100644
index 0000000000000..96ecdf03dcc13
--- /dev/null
+++ b/packages/interface/src/components/preferences-modal/README.md
@@ -0,0 +1,69 @@
+# PreferencesModal
+
+`PreferencesModal` renders a modal with editor preferences. It can take a `PreferencesModalTabs` component, which accepts multiple tabs, and/or other child components. On small viewports, the modal is fullscreen.
+
+This component implements a `Modal` component from the `@wordpress/components` package.
+
+Sections passed to this component should use `PreferencesModalSection` component from the `@wordpress/interface` package.
+
+
+## Example
+
+```jsx
+function MyEditorPreferencesModal() {
+ const { closeModal } = useDispatch( editPostStore );
+ const sections = [
+ {
+ name: 'section 1',
+ tabLabel: __( 'Section 1' ),
+ content: (
+
+
+
+ )
+
+ }
+ {
+ name: 'section 2',
+ tabLabel: __( 'Section 2' ),
+ content: (
+
+ // Section content here
+
+ )
+
+ }
+ ]
+ return (
+
+
+
+ );
+}
+```
+
+## Props
+
+### closeModal
+
+A function to call when closing the modal.
+
+- Type: `Function`
+- Required: Yes.
diff --git a/packages/interface/src/components/preferences-modal/index.js b/packages/interface/src/components/preferences-modal/index.js
new file mode 100644
index 0000000000000..b91be8b21204a
--- /dev/null
+++ b/packages/interface/src/components/preferences-modal/index.js
@@ -0,0 +1,18 @@
+/**
+ * WordPress dependencies
+ */
+import { Modal } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+
+export default function PreferencesModal( { closeModal, children } ) {
+ return (
+
+ { children }
+
+ );
+}
diff --git a/packages/interface/src/components/preferences-modal/style.scss b/packages/interface/src/components/preferences-modal/style.scss
new file mode 100644
index 0000000000000..007b4b7c4e5e5
--- /dev/null
+++ b/packages/interface/src/components/preferences-modal/style.scss
@@ -0,0 +1,25 @@
+.interface-preferences-modal {
+ // To keep modal dimensions consistent as subsections are navigated, width
+ // and height are used instead of max-(width/height).
+ @include break-small() {
+ width: calc(100% - #{ $grid-unit-20 * 2 });
+ height: calc(100% - #{ $header-height * 2 });
+ }
+ @include break-medium() {
+ width: $break-medium - $grid-unit-20 * 2;
+ }
+ @include break-large() {
+ height: 70%;
+ }
+
+ // Clears spacing to flush fit the navigator component to the modal edges.
+ @media (max-width: #{ ($break-medium - 1) }) {
+ .components-modal__content {
+ padding: 0;
+
+ &::before {
+ content: none;
+ }
+ }
+ }
+}
diff --git a/packages/interface/src/style.scss b/packages/interface/src/style.scss
index e6950de411156..be111640a0a79 100644
--- a/packages/interface/src/style.scss
+++ b/packages/interface/src/style.scss
@@ -4,3 +4,7 @@
@import "./components/interface-skeleton/style.scss";
@import "./components/more-menu-dropdown/style.scss";
@import "./components/pinned-items/style.scss";
+@import "./components/preferences-modal/style.scss";
+@import "./components/preferences-modal-tabs/style.scss";
+@import "./components/preferences-modal-section/style.scss";
+@import "./components/preferences-modal-base-option/style.scss";