diff --git a/packages/controllers/resize-controller/README.md b/packages/controllers/resize-controller/README.md
new file mode 100644
index 000000000..62006747c
--- /dev/null
+++ b/packages/controllers/resize-controller/README.md
@@ -0,0 +1,94 @@
+# Resize Controller
+
+The `resize-controller` is a utility module that allows you to observe and react to changes in the size of a web component. It provides a simple way to handle resize events and perform actions based on the new size of the component.
+
+## Installation
+
+You can install the `resize-controller` package using yarn:
+
+```
+yarn add -D @phase2/outline-controller-resize-controller
+```
+
+## Usage
+
+To use the `resize-controller` in your web component, follow these steps:
+
+1. Import the necessary classes and functions from the `lit` package:
+
+```javascript
+import { ResizeController } from '@phase2/outline-controller-resize-controller';
+```
+
+2. Create an instance of the `ResizeController` and pass the host element and options:
+
+```javascript
+resizeController = new ResizeController(this);
+```
+
+## API Reference
+
+### `ResizeController`
+
+The `ResizeController` class provides methods to observe resize events and perform actions based on the new size of the host element.
+
+#### Constructor
+
+```javascript
+new ResizeController(host: ReactiveControllerHost & HTMLElement, options?: ResizeControllerOptions)
+```
+
+- `host`: The host element of the web component.
+- `options` (optional): An object specifying the options for the `ResizeController`. It can include the following properties:
+  - `debounce`: The delay in milliseconds to debounce the resize event. Defaults to `200`.
+  - `breakpoints`: An array of breakpoints for different size ranges. Defaults to `[768]`.
+  - `elementToRerender`: The element to trigger a re-render when the size changes. Defaults to the `host` element.
+
+#### Properties
+
+- `onResize`: A callback function that will be called when the element is resized. Override this method in your component to handle the resize event.
+
+#### Methods
+
+- `hostConnected()`: Called when the host element is connected to the DOM. Observes the element for size changes.
+- `hostDisconnected()`: Called when the host element is disconnected from the DOM. Stops observing size changes.
+
+## Example
+
+Here's an example that demonstrates how to use the `resize-controller` in a web component:
+
+```javascript
+@customElement('my-component')
+export class MyComponent extends LitElement {
+  resizeController = new ResizeController(this, {
+    breakpoints: [768, 1440],
+  });
+
+  render() {
+    const Classes = {
+      'mobile': this.resizeController.currentBreakpointRange === 0,
+      'medium': this.resizeController.currentBreakpointRange === 1,
+      'large': this.resizeController.currentBreakpointRange === 2,
+    };
+    return html`
+      
+        Hello World
+      
+    `;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'my-component': MyComponent;
+  }
+}
+```
+
+In this example, `resizeController` is initialized to support the following breakpoints:
+
+- 0: 0-767px
+- 1: 768px-1439px
+- 2: 1440px - 100000px
+
+When my-component's width crosses from one range to another, the resize controller will call the component's `render()` function.
diff --git a/packages/controllers/resize-controller/index.ts b/packages/controllers/resize-controller/index.ts
new file mode 100644
index 000000000..726f3c8f8
--- /dev/null
+++ b/packages/controllers/resize-controller/index.ts
@@ -0,0 +1 @@
+export { ResizeController } from './src/resize-controller';
diff --git a/packages/controllers/resize-controller/package.json b/packages/controllers/resize-controller/package.json
new file mode 100644
index 000000000..a2ce96877
--- /dev/null
+++ b/packages/controllers/resize-controller/package.json
@@ -0,0 +1,41 @@
+{
+  "name": "@phase2/outline-controller-resize-controller",
+  "version": "0.0.0",
+  "description": "Controller to help with managing classes / markup updates based on component's width",
+  "keywords": [
+    "outline components",
+    "outline design",
+    "resize"
+  ],
+  "main": "index.ts",
+  "types": "index.ts",
+  "typings": "index.d.ts",
+  "files": [
+    "/dist/",
+    "/src/",
+    "!/dist/tsconfig.build.tsbuildinfo"
+  ],
+  "author": "Phase2 Technology",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/phase2/outline.git",
+    "directory": "packages/controllers/resize-controller"
+  },
+  "license": "BSD-3-Clause",
+  "scripts": {
+    "build": "node ../../../scripts/build.js",
+    "package": "yarn publish"
+  },
+  "dependencies": {
+    "lit": "^2.3.1"
+  },
+  "devDependencies": {
+    "tslib": "^2.1.0"
+  },
+  "publishConfig": {
+    "access": "public"
+  },
+  "exports": {
+    ".": "./index.ts"
+  }
+}
diff --git a/packages/controllers/resize-controller/src/resize-controller.ts b/packages/controllers/resize-controller/src/resize-controller.ts
new file mode 100644
index 000000000..acfe8f160
--- /dev/null
+++ b/packages/controllers/resize-controller/src/resize-controller.ts
@@ -0,0 +1,186 @@
+import { ReactiveControllerHost, ReactiveController } from 'lit';
+
+/**
+ * Debounces a function
+ * @template T
+ * @param {T} func - The function to debounce
+ * @param {number} delay - The delay in milliseconds
+ * @param {boolean} [immediate=false] - Whether to execute the function immediately
+ * @returns {(...args: Parameters) => void} - The debounced function
+ */
+export const debounce = ) => void>(
+  func: T,
+  delay: number,
+  immediate = false
+): ((...args: Parameters) => void) => {
+  let timeoutId: ReturnType | undefined = undefined;
+
+  return function debounced(...args: Parameters) {
+    const executeFunc = () => func(...args);
+
+    clearTimeout(timeoutId);
+
+    if (immediate && timeoutId === undefined) {
+      executeFunc();
+    }
+
+    timeoutId = setTimeout(executeFunc, delay);
+  };
+};
+
+export type breakpointsRangeType = {
+  min: number;
+  max: number;
+};
+
+/**
+ * ResizeController class
+ * @implements {ReactiveController}
+ */
+export class ResizeController implements ReactiveController {
+  host: ReactiveControllerHost & HTMLElement;
+  resizeObserver: ResizeObserver;
+  elementToObserve: Element;
+  options: {
+    debounce: number;
+    breakpoints: number[];
+    elementToRerender: ReactiveControllerHost & HTMLElement;
+  };
+  currentComponentWidth: number;
+  currentBreakpointRange: number;
+  breakpointsRangeArray: breakpointsRangeType[] = [];
+
+  /**
+   * Create a constructor that takes a host and options
+   * @param {ReactiveControllerHost & Element} host - The host element
+   * @param {{debounce?: number; breakpoints?: number[]}} [options={}] - The options object
+   */
+  constructor(
+    host: ReactiveControllerHost & HTMLElement,
+    options: {
+      debounce?: number;
+      breakpoints?: number[];
+      elementToRerender?: ReactiveControllerHost & HTMLElement;
+    } = {}
+  ) {
+    const defaultOptions = {
+      debounce: 200,
+      breakpoints: [768],
+      elementToRerender: host,
+    };
+
+    /**
+     * Remove any undefined variables from options object
+     */
+    const filteredOptionsObject = Object.fromEntries(
+      Object.entries(options).filter(([_, value]) => value !== undefined)
+    );
+    this.options = { ...defaultOptions, ...filteredOptionsObject };
+
+    this.host = host;
+    this.host.addController(this);
+
+    this.initializeBreakpointsRangeType();
+  }
+
+  /**
+   * Initialize the breakpoints range array
+   *
+   * The default breakpoints array ([768]) will create this breakpoints range array:
+   * [{min: 0, max: 767}, {min: 768, max: 100000}]
+   *
+   * If custom breakpoints array is provided, (for example [768, 1200, 2000]) this breakpoints range array will be created:
+   * [{min: 0, max: 767}, {min: 768, max: 1199}, {min: 1200, max: 1999}, {min: 2000, max: 100000}]
+   *
+   */
+  initializeBreakpointsRangeType() {
+    // This will allow create an additional breakpoint from the last custom breakpoint to 100000
+    this.options.breakpoints?.push(100000);
+
+    let minBreakpoint = 0;
+    this.options.breakpoints?.forEach(breakpoint => {
+      const newBreakpointRange = {
+        min: minBreakpoint,
+        max: breakpoint - 1,
+      };
+      minBreakpoint = breakpoint;
+      this.breakpointsRangeArray.push(newBreakpointRange);
+    });
+  }
+
+  /**
+   * Called when the host element is connected to the DOM
+   */
+  hostConnected() {
+    if (!this.host.style.display) {
+      // adding `display: block` to :host of component
+      this.host.style.setProperty(
+        'display',
+        'var(--style-added-by-resize-controller, block)'
+      );
+    }
+
+    // Create a new ResizeObserver and pass in the function to be called when the element is resized
+    this.resizeObserver = new ResizeObserver(
+      (entries: ResizeObserverEntry[]) => {
+        // Create a debounced version of the onElementResize function
+        debounce(
+          this.onElementResize.bind(this),
+          this.options.debounce
+        )(entries);
+      }
+    );
+
+    // Get a reference to the element you want to observe
+    this.elementToObserve = this.host;
+
+    // Observe the element for size changes
+    this.resizeObserver.observe(this.elementToObserve);
+  }
+
+  /**
+   * Called when the host element is disconnected from the DOM
+   */
+  hostDisconnected() {
+    this.resizeObserver.disconnect();
+  }
+
+  /**
+   * Called when the element is resized
+   * @param {ResizeObserverEntry[]} _entries - The ResizeObserverEntry array
+   */
+  onElementResize(_entries: ResizeObserverEntry[]) {
+    this.currentComponentWidth = _entries[0].contentRect.width;
+
+    // skip if width is not yet set
+    if (this.currentComponentWidth) {
+      this.calculateNewBreakpointRange();
+    } else if (this.currentComponentWidth === 0) {
+      // eslint-disable-next-line no-console
+      console.warn(
+        `resize-controller: No width detected in <${this.host.localName}>. Please confirm it has display: block`
+      );
+    }
+  }
+
+  /**
+   * Calculate the new breakpoint based on the current width
+   */
+  calculateNewBreakpointRange() {
+    let newBreakpointRange = this.currentBreakpointRange;
+
+    this.breakpointsRangeArray.forEach((breakpoint, index) => {
+      if (
+        this.currentComponentWidth >= breakpoint.min &&
+        this.currentComponentWidth <= breakpoint.max
+      ) {
+        newBreakpointRange = index;
+      }
+    });
+
+    if (newBreakpointRange !== this.currentBreakpointRange) {
+      this.currentBreakpointRange = newBreakpointRange;
+      this.options.elementToRerender.requestUpdate();
+    }
+  }
+}
diff --git a/packages/controllers/resize-controller/tsconfig.build.json b/packages/controllers/resize-controller/tsconfig.build.json
new file mode 100644
index 000000000..5eac9d313
--- /dev/null
+++ b/packages/controllers/resize-controller/tsconfig.build.json
@@ -0,0 +1,9 @@
+{
+  "extends": "../../../tsconfig.json",
+  "compilerOptions": {
+    "rootDir": ".",
+    "outDir": "./dist"
+  },
+  "include": ["index.ts", "src/**/*", "tests/**/*"],
+  "references": [{ "path": "../../outline-core/tsconfig.build.json" }]
+}