diff --git a/package-lock.json b/package-lock.json
index 2a28831d..9745ce01 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,8 @@
       "version": "0.24.0",
       "license": "MIT",
       "dependencies": {
-        "@floating-ui/dom": "^1.6.13"
+        "@floating-ui/dom": "^1.6.13",
+        "@lit/context": "^1.1.3"
       },
       "devDependencies": {
         "@babel/core": "^7.26.0",
@@ -1813,14 +1814,20 @@
     "node_modules/@lit-labs/ssr-dom-shim": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz",
-      "integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==",
-      "dev": true
+      "integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ=="
+    },
+    "node_modules/@lit/context": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.3.tgz",
+      "integrity": "sha512-Auh37F4S0PZM93HTDfZWs97mmzaQ7M3vnTc9YvxAGyP3UItSK/8Fs0vTOGT+njuvOwbKio/l8Cx/zWL4vkutpQ==",
+      "dependencies": {
+        "@lit/reactive-element": "^1.6.2 || ^2.0.0"
+      }
     },
     "node_modules/@lit/reactive-element": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz",
       "integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==",
-      "dev": true,
       "dependencies": {
         "@lit-labs/ssr-dom-shim": "^1.2.0"
       }
diff --git a/package.json b/package.json
index 19e706ca..f15625d2 100644
--- a/package.json
+++ b/package.json
@@ -62,6 +62,7 @@
     }
   },
   "dependencies": {
-    "@floating-ui/dom": "^1.6.13"
+    "@floating-ui/dom": "^1.6.13",
+    "@lit/context": "^1.1.3"
   }
 }
diff --git a/public/docdiff.html b/public/docdiff.html
index 97ad35b4..547e8369 100644
--- a/public/docdiff.html
+++ b/public/docdiff.html
@@ -1,5 +1,6 @@
 <html>
   <head>
+    <meta name="readthedocs-addons-api-version" content="1" />
     <title>DocDiff Addons - Read the Docs</title>
     <meta name="readthedocs-project-slug" content="test-builds" />
     <meta name="readthedocs-version-slug" content="latest" />
diff --git a/public/index.html b/public/index.html
index f24ff41a..68016500 100644
--- a/public/index.html
+++ b/public/index.html
@@ -22,6 +22,9 @@ <h1 id="documentation-addons">Documentation Addons</h1>
       <h2 id="docdiff">CustomEvent</h2>
       <p>Project slug using <em>CustomEvent</em>: <span id="custom-event-project-slug"></span></p>
 
+      <h2 id="flyout">Flyout</h2>
+      <readthedocs-flyout></readthedocs-flyout>
+
       <h2 id="docdiff">DocDiff</h2>
       <p>Visit <a href="docdiff.html">this page</a> to take a look at it.</p>
 
diff --git a/src/analytics.js b/src/analytics.js
index 447989a5..00be5e3a 100644
--- a/src/analytics.js
+++ b/src/analytics.js
@@ -5,36 +5,40 @@ import { default as fetch } from "unfetch";
 import { ajv } from "./data-validation";
 import { AddonBase } from "./utils";
 import { CLIENT_VERSION } from "./utils";
+import { ContextConsumer } from "@lit/context";
+import { configContext } from "./context.js";
+import { nothing, render, LitElement } from "lit";
 
 export const API_ENDPOINT = "/_/api/v2/analytics/";
 
-/**
- * Analytics addon
- *
- * Register page views that can be seen from the project's dashboard.
- * Besides, it injects the Global Read the Docs analytics.
- *
- * Read more at:
- *  - https://docs.readthedocs.io/en/stable/reference/analytics.html
- *  - https://docs.readthedocs.io/en/stable/advertising/advertising-details.html#analytics
- *
- * @param {Object} config - Addon configuration object
- */
-export class AnalyticsAddon extends AddonBase {
-  static jsonValidationURI =
-    "http://v1.schemas.readthedocs.org/addons.analytics.json";
-  static addonEnabledPath = "addons.analytics.enabled";
-  static addonName = "Analytics";
-  static enabledOnHttpStatus = [200, 404];
+export class AnalyticsElement extends LitElement {
+  static elementName = "readthedocs-analytics";
+
+  // `_config` is the context we are going to consume when it's updated.
+  _config = new ContextConsumer(this, {
+    context: configContext,
+    subscribe: true,
+  });
 
-  constructor(config) {
+  constructor() {
     super();
-    this.config = config;
+    this.config = null;
+  }
+
+  render() {
+    // Validate the context (`this._config.value`) on each update and return
+    // nothing if it's invalid. ``nothing`` is a special Lit response type.
+    if (!AnalyticsAddon.isEnabled(this._config.value)) {
+      return nothing;
+    }
+
+    this.config = this._config.value;
 
     // Only register pageviews on non-external versions
     if (this.config.versions.current.type !== "external") {
       this.registerPageView();
     }
+    return nothing;
   }
 
   registerPageView() {
@@ -59,3 +63,35 @@ export class AnalyticsAddon extends AddonBase {
       });
   }
 }
+
+/**
+ * Analytics addon
+ *
+ * Register page views that can be seen from the project's dashboard.
+ * Besides, it injects the Global Read the Docs analytics.
+ *
+ * Read more at:
+ *  - https://docs.readthedocs.io/en/stable/reference/analytics.html
+ *  - https://docs.readthedocs.io/en/stable/advertising/advertising-details.html#analytics
+ *
+ * @param {Object} config - Addon configuration object
+ */
+export class AnalyticsAddon extends AddonBase {
+  static jsonValidationURI =
+    "http://v1.schemas.readthedocs.org/addons.analytics.json";
+  static addonEnabledPath = "addons.analytics.enabled";
+  static addonName = "Analytics";
+  static enabledOnHttpStatus = [200, 404];
+
+  constructor() {
+    super();
+
+    // If there are no elements found, inject one
+    let elems = document.querySelectorAll("readthedocs-analytics");
+    if (!elems.length) {
+      render(new AnalyticsElement(), document.body);
+    }
+  }
+}
+
+customElements.define("readthedocs-analytics", AnalyticsElement);
diff --git a/src/context.js b/src/context.js
new file mode 100644
index 00000000..587ce754
--- /dev/null
+++ b/src/context.js
@@ -0,0 +1,26 @@
+import {
+  ContextProvider,
+  ContextRoot,
+  createContext,
+} from "@lit/context";
+import { EVENT_READTHEDOCS_ADDONS_DATA_READY } from "./events";
+
+export const contextRoot = new ContextRoot().attach(document.body);
+export const configContext = createContext(Symbol("readthedocs-config"));
+
+/**
+ * Because `config` provider is not attached to a ReactiveElement, and is
+ * instead connected to `document.html`, we have to call `hostConnected()`
+ * manually. See:
+ *
+ * https://github.com/lit/lit/blob/935697d47e62ed75e3157423400163a8371c62fc/packages/context/src/lib/controllers/context-provider.ts#L55-L58
+ **/
+const config = new ContextProvider(document.documentElement, {
+  context: configContext,
+});
+config.hostConnected();
+
+document.addEventListener(EVENT_READTHEDOCS_ADDONS_DATA_READY, (event) => {
+  console.log("Event:", EVENT_READTHEDOCS_ADDONS_DATA_READY);
+  config.setValue(event.detail.data());
+});
diff --git a/src/customscript.js b/src/customscript.js
index bdb594c3..f7d4d0b2 100644
--- a/src/customscript.js
+++ b/src/customscript.js
@@ -1,27 +1,38 @@
 import { default as objectPath } from "object-path";
 import { AddonBase } from "./utils";
+import { ContextConsumer } from "@lit/context";
+import { configContext } from "./context.js";
+import { nothing, render, LitElement } from "lit";
 
 const SCRIPT_ID = "readthedocs-addons-custom-script";
 
-/**
- * User JavaScript file.
- *
- * Allow a user to inject a custom JavaScript file in all the pages.
- */
-export class CustomScriptAddon extends AddonBase {
-  static jsonValidationURI =
-    "http://v1.schemas.readthedocs.org/addons.customscript.json";
-  static addonEnabledPath = "addons.customscript.enabled";
-  static addonName = "CustomScript";
-  static enabledOnHttpStatus = [200, 403, 404, 500];
+export class CustomScriptElement extends LitElement {
+  static elementName = "readthedocs-customscript";
+
+  // `_config` is the context we are going to consume when it's updated.
+  _config = new ContextConsumer(this, {
+    context: configContext,
+    subscribe: true,
+  });
 
-  constructor(config) {
+  constructor() {
     super();
-    this.config = config;
+    this.config = null;
+  }
+
+  render() {
+    // Validate the context (`this._config.value`) on each update and return
+    // nothing if it's invalid. ``nothing`` is a special Lit response type.
+    if (!CustomScriptAddon.isEnabled(this._config.value)) {
+      return nothing;
+    }
+
+    this.config = this._config.value;
 
     if (objectPath.get(this.config, "addons.customscript.src")) {
       this.injectJavaScriptFile();
     }
+    return nothing;
   }
 
   injectJavaScriptFile() {
@@ -33,3 +44,28 @@ export class CustomScriptAddon extends AddonBase {
     document.body.appendChild(script);
   }
 }
+
+/**
+ * User JavaScript file.
+ *
+ * Allow a user to inject a custom JavaScript file in all the pages.
+ */
+export class CustomScriptAddon extends AddonBase {
+  static jsonValidationURI =
+    "http://v1.schemas.readthedocs.org/addons.customscript.json";
+  static addonEnabledPath = "addons.customscript.enabled";
+  static addonName = "CustomScript";
+  static enabledOnHttpStatus = [200, 403, 404, 500];
+
+  constructor() {
+    super();
+
+    // If there are no elements found, inject one
+    let elems = document.querySelectorAll("readthedocs-customscript");
+    if (!elems.length) {
+      render(new CustomScriptElement(), document.body);
+    }
+  }
+}
+
+customElements.define("readthedocs-customscript", CustomScriptElement);
diff --git a/src/docdiff.js b/src/docdiff.js
index ac2beef4..350e7bda 100644
--- a/src/docdiff.js
+++ b/src/docdiff.js
@@ -17,9 +17,11 @@ import {
   EVENT_READTHEDOCS_DOCDIFF_HIDE,
   EVENT_READTHEDOCS_ROOT_DOM_CHANGED,
 } from "./events";
-import { nothing, LitElement } from "lit";
+import { render, nothing, LitElement } from "lit";
 import { default as objectPath } from "object-path";
 import { hasQueryParam, docTool } from "./utils";
+import { ContextConsumer } from "@lit/context";
+import { configContext } from "./context.js";
 
 export const DOCDIFF_URL_PARAM = "readthedocs-diff";
 
@@ -75,6 +77,12 @@ export class DocDiffElement extends LitElement {
 
   static styles = styleSheet;
 
+  // `_config` is the context we are going to consume when it's updated.
+  _config = new ContextConsumer(this, {
+    context: configContext,
+    subscribe: true,
+  });
+
   constructor() {
     super();
 
@@ -87,11 +95,7 @@ export class DocDiffElement extends LitElement {
     this.cachedRemoteContent = null;
   }
 
-  loadConfig(config) {
-    if (!DocDiffAddon.isEnabled(config)) {
-      return;
-    }
-    this.config = config;
+  firstUpdated() {
     this.rootSelector =
       objectPath.get(this.config, "options.root_selector") ||
       docTool.getRootSelector();
@@ -101,7 +105,17 @@ export class DocDiffElement extends LitElement {
     if (this.injectStyles) {
       document.adoptedStyleSheets.push(docdiffGeneralStyleSheet);
     }
+  }
+
+  render() {
+    // Validate the context (`this._config.value`) on each update and return
+    // nothing if it's invalid. ``nothing`` is a special Lit response type.
+    if (!DocDiffAddon.isEnabled(this._config.value)) {
+      return nothing;
+    }
+    console.log("DocDiff:", this._config.value);
 
+    this.config = this._config.value;
     // Enable DocDiff if the URL parameter is present
     if (hasQueryParam(DOCDIFF_URL_PARAM)) {
       const event = new CustomEvent(
@@ -109,31 +123,10 @@ export class DocDiffElement extends LitElement {
       );
       document.dispatchEvent(event);
     }
-  }
 
-  render() {
     return nothing;
-    // TODO: render a checkbox once we are settled on the UI.
-    // For now, we are only enabling/disabling via a hotkey.
-    //
-    // return html`
-    //   <label class="switch">
-    //     <input @click="${this.handleClick}" type="checkbox" />
-    //     <span class="slider round"></span>
-    //   </label>
-    // `;
   }
 
-  // This code isn't used until we show a UI,
-  // and even then we'll want to trigger events to match state?
-  // handleClick(e) {
-  //   if (e.target.checked) {
-  //     this.enableDocDiff();
-  //   } else {
-  //     this.disableDocDiff();
-  //   }
-  // }
-
   compare() {
     let promiseData;
 
@@ -264,13 +257,7 @@ export class DocDiffAddon extends AddonBase {
     customElements.define("readthedocs-docdiff", DocDiffElement);
     let elems = document.querySelectorAll("readthedocs-docdiff");
     if (!elems.length) {
-      elems = [new DocDiffElement()];
-      document.body.append(elems[0]);
-      elems[0].requestUpdate();
-    }
-
-    for (const elem of elems) {
-      elem.loadConfig(config);
+      render(new DocDiffElement(), document.body);
     }
   }
 
diff --git a/src/flyout.js b/src/flyout.js
index 45c13ec2..0d20dd0f 100644
--- a/src/flyout.js
+++ b/src/flyout.js
@@ -1,6 +1,7 @@
 import { ajv } from "./data-validation";
 import READTHEDOCS_LOGO_WORDMARK from "./images/logo-wordmark-light.svg";
 import READTHEDOCS_LOGO from "./images/logo-light.svg";
+import { ContextConsumer } from "@lit/context";
 import { library, icon } from "@fortawesome/fontawesome-svg-core";
 import {
   faCodeBranch,
@@ -10,6 +11,7 @@ import {
 import { html, nothing, render, LitElement } from "lit";
 import { classMap } from "lit/directives/class-map.js";
 import { default as objectPath } from "object-path";
+import { configContext } from "./context.js";
 
 import styleSheet from "./flyout.css";
 import { AddonBase, addUtmParameters, getLinkWithFilename } from "./utils";
@@ -24,7 +26,6 @@ export class FlyoutElement extends LitElement {
   static elementName = "readthedocs-flyout";
 
   static properties = {
-    config: { state: true },
     opened: { type: Boolean },
     floating: { type: Boolean },
     position: { type: String },
@@ -32,37 +33,26 @@ export class FlyoutElement extends LitElement {
 
   static styles = styleSheet;
 
+  // `_config` is the context we are going to consume when it's updated.
+  _config = new ContextConsumer(this, {
+    context: configContext,
+    subscribe: true,
+  });
+
   constructor() {
     super();
 
+    // `config` is the internal config for this addon that's updated based on `_config`.
     this.config = null;
+
     this.opened = false;
     this.floating = true;
     this.position = "bottom-right";
     this.readthedocsLogo = READTHEDOCS_LOGO;
-  }
-
-  loadConfig(config) {
-    // Validate the config object before assigning it to the Addon.
-    // Later, ``render()`` method will check whether this object exists and (not) render
-    // accordingly
-    if (!FlyoutAddon.isEnabled(config)) {
-      return;
-    }
-    this.config = config;
-
-    // The "position" is a value that can be defined from the dashboard.
-    // There are two main options: "Default" or a specific value.
-    // When "Default" is used, the value will be grabbed from the HTML element (e.g. explicitly set by the theme author).
-    // In case it's not defined, the value defined in the `constructor` will be used ("bottom-right")
-    const dashboardPosition = objectPath.get(
-      this.config,
-      "addons.flyout.position",
-      null,
+    console.log(
+      "Flyout _config (from constructor() method)",
+      this._config.value,
     );
-    if (dashboardPosition) {
-      this.position = dashboardPosition;
-    }
   }
 
   _close() {
@@ -333,11 +323,29 @@ export class FlyoutElement extends LitElement {
 
   render() {
     // The element doesn't yet have our config, don't render it.
-    if (this.config === null) {
-      // nothing is a special Lit response type
+    console.log("Flyout config (from render() method )", this._config.value);
+
+    // Validate the context (`this._config.value`) on each update and return
+    // nothing if it's invalid. ``nothing`` is a special Lit response type.
+    if (!FlyoutAddon.isEnabled(this._config.value)) {
       return nothing;
     }
 
+    this.config = this._config.value;
+
+    // The "position" is a value that can be defined from the dashboard.
+    // There are two main options: "Default" or a specific value.
+    // When "Default" is used, the value will be grabbed from the HTML element (e.g. explicitly set by the theme author).
+    // In case it's not defined, the value defined in the `constructor` will be used ("bottom-right")
+    const dashboardPosition = objectPath.get(
+      this.config,
+      "addons.flyout.position",
+      null,
+    );
+    if (dashboardPosition) {
+      this.position = dashboardPosition;
+    }
+
     this.updateCSSClasses();
 
     return html`
@@ -403,16 +411,7 @@ export class FlyoutAddon extends AddonBase {
     // If there are no elements found, inject one
     let elems = document.querySelectorAll("readthedocs-flyout");
     if (!elems.length) {
-      elems = [new FlyoutElement()];
-
-      // We cannot use `render(elems[0], document.body)` because there is a race conditions between all the addons.
-      // So, we append the web-component first and then request an update of it.
-      document.body.append(elems[0]);
-      elems[0].requestUpdate();
-    }
-
-    for (const elem of elems) {
-      elem.loadConfig(config);
+      render(new FlyoutElement(), document.body);
     }
   }
 }
diff --git a/src/index.js b/src/index.js
index 78781d15..a86940db 100644
--- a/src/index.js
+++ b/src/index.js
@@ -102,9 +102,12 @@ export function setup() {
         }
 
         for (const addon of addons) {
+          // TODO: always initialize _all the addons_, they will be rendered or not based on the dynamic config context
+          // TODO: move the `httpStatus` usage for `isEnabled` into `render()` for each addon
           if (addon.isEnabled(config, httpStatus)) {
             promises.push(
               new Promise((resolve) => {
+                // TODO: remove attribute `config` from here since it's not required anymore
                 return resolve(new addon(config));
               }),
             );
diff --git a/src/notification.js b/src/notification.js
index 3bf491ea..e1533089 100644
--- a/src/notification.js
+++ b/src/notification.js
@@ -1,5 +1,6 @@
 import { ajv } from "./data-validation";
 import { library, icon } from "@fortawesome/fontawesome-svg-core";
+import { ContextConsumer } from "@lit/context";
 import {
   faCircleXmark,
   faFlask,
@@ -8,6 +9,7 @@ import {
 } from "@fortawesome/free-solid-svg-icons";
 import { html, nothing, render, LitElement } from "lit";
 import { default as objectPath } from "object-path";
+import { configContext } from "./context.js";
 
 import styleSheet from "./notification.css";
 import { AddonBase, addUtmParameters, getLinkWithFilename } from "./utils";
@@ -18,7 +20,6 @@ export class NotificationElement extends LitElement {
 
   /** @static @property {Object} - Lit reactive properties */
   static properties = {
-    config: { state: true },
     urls: { state: true },
     highest_version: { state: true },
     dismissedTimestamp: { state: true },
@@ -29,6 +30,12 @@ export class NotificationElement extends LitElement {
   /** @static @property {Object} - Lit stylesheets to apply to elements */
   static styles = styleSheet;
 
+  // `_config` is the context we are going to consume when it's updated.
+  _config = new ContextConsumer(this, {
+    context: configContext,
+    subscribe: true,
+  });
+
   constructor() {
     super();
 
@@ -125,15 +132,32 @@ export class NotificationElement extends LitElement {
     this.timerID = null;
   }
 
-  loadConfig(config) {
-    // Validate the config object before assigning it to the Addon.
-    // Later, ``render()`` method will check whether this object exists and (not) render
-    // accordingly
-    if (!NotificationAddon.isEnabled(config)) {
-      return;
+  getLocalStorageKeyFromConfig(config) {
+    const projectSlug = config.projects.current.slug;
+    const languageCode = config.projects.current.language.code;
+    const versionSlug = config.versions.current.slug;
+    return `${projectSlug}-${languageCode}-${versionSlug}-notification`;
+  }
+
+  firstUpdated() {
+    // Add CSS classes to the element on ``firstUpdated`` because we need the
+    // HTML element to exist in the DOM before being able to add tag attributes.
+    this.className = this.className || "raised toast";
+  }
+
+  render() {
+    if (this.autoDismissed) {
+      return nothing;
     }
 
-    this.config = config;
+    if (!NotificationAddon.isEnabled(this._config.value)) {
+      return nothing;
+    }
+    this.config = this._config.value;
+
+    if (!this.config.addons.notifications.enabled) {
+      return nothing;
+    }
 
     if (
       this.config.addons.notifications.enabled &&
@@ -142,11 +166,11 @@ export class NotificationElement extends LitElement {
       this.urls = {
         // NOTE: point users to the new beta dashboard for now so we promote it more.
         // We will revert this once we are fully migrated to the new dashboard.
-        build: config.builds.current.urls.build
+        build: this.config.builds.current.urls.build
           .replace("readthedocs.org", "app.readthedocs.org")
           .replace("readthedocs.com", "app.readthedocs.com")
           .replace("app.app.", "app."),
-        external: config.versions.current.urls.vcs,
+        external: this.config.versions.current.urls.vcs,
       };
     }
 
@@ -156,48 +180,19 @@ export class NotificationElement extends LitElement {
         "addons.notifications.show_on_latest",
         false,
       ) &&
-      config.projects.current.versioning_scheme !==
+      this.config.projects.current.versioning_scheme !==
         "single_version_without_translations" &&
-      config.versions.current.type !== "external"
+      this.config.versions.current.type !== "external"
     ) {
       this.calculateStableLatestVersionWarning();
     }
-    this.loadDismissedTimestamp(this.config);
-  }
-
-  getLocalStorageKeyFromConfig(config) {
-    const projectSlug = config.projects.current.slug;
-    const languageCode = config.projects.current.language.code;
-    const versionSlug = config.versions.current.slug;
-    return `${projectSlug}-${languageCode}-${versionSlug}-notification`;
-  }
-
-  firstUpdated() {
-    // Add CSS classes to the element on ``firstUpdated`` because we need the
-    // HTML element to exist in the DOM before being able to add tag attributes.
-    this.className = this.className || "raised toast";
-  }
-
-  render() {
-    if (this.autoDismissed) {
-      return nothing;
-    }
-
-    // The element doesn't yet have our config, don't render it.
-    if (this.config === null) {
-      // nothing is a special Lit response type
-      return nothing;
-    }
 
+    this.loadDismissedTimestamp(this.config);
     // This notification has been dimissed, so don't render it
     if (this.dismissedTimestamp) {
       return nothing;
     }
 
-    if (!this.config.addons.notifications.enabled) {
-      return nothing;
-    }
-
     if (this.config.versions.current.type === "external") {
       if (
         objectPath.get(
@@ -411,19 +406,13 @@ export class NotificationAddon extends AddonBase {
   static addonEnabledPath = "addons.notifications.enabled";
   static addonName = "Notification";
 
-  constructor(config) {
+  constructor() {
     super();
 
     // If there are no elements found, inject one
     let elems = document.querySelectorAll("readthedocs-notification");
     if (!elems.length) {
-      elems = [new NotificationElement()];
-      document.body.append(elems[0]);
-      elems[0].requestUpdate();
-    }
-
-    for (const elem of elems) {
-      elem.loadConfig(config);
+      render(new NotificationElement(), document.body);
     }
   }
 }