diff --git a/config/etc/mime.types b/config/etc/mime.types
index af82d4886..bb3816db9 100644
--- a/config/etc/mime.types
+++ b/config/etc/mime.types
@@ -252,6 +252,7 @@ multipart/voice-message
 text/css			css
 text/directory
 text/enriched
+text/javascript		mjs
 text/plain			asc txt
 text/prs.lines.tag
 text/rfc822-headers
diff --git a/html/html/include/wui.js b/html/html/include/wui.js
new file mode 100644
index 000000000..e65924e29
--- /dev/null
+++ b/html/html/include/wui.js
@@ -0,0 +1,47 @@
+/*#############################################################################
+#                                                                             #
+# IPFire.org - A linux based firewall                                         #
+# Copyright (C) 2007-2023  IPFire Team  <info@ipfire.org>                     #
+#                                                                             #
+# This program is free software: you can redistribute it and/or modify        #
+# it under the terms of the GNU General Public License as published by        #
+# the Free Software Foundation, either version 3 of the License, or           #
+# (at your option) any later version.                                         #
+#                                                                             #
+# This program is distributed in the hope that it will be useful,             #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
+# GNU General Public License for more details.                                #
+#                                                                             #
+# You should have received a copy of the GNU General Public License           #
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
+#                                                                             #
+#############################################################################*/
+
+// IPFire Web User Interface
+// Collection of JavaScript functions and modules (requires jQuery)
+
+import {WUIcore_i18n as WUI_i18n} from "./wui_core.mjs";
+
+import {WUImodule_rrdimage as WUI_rrdimage} from "./wui_rrdimage.mjs";
+
+//--- WUI main class ---
+class WUImain {
+	constructor() {
+		//- Public properties -
+		// Translation strings
+		this.i18n = new WUI_i18n();
+
+		//- Modules -
+		// RRDtool graph images
+		this.rrdimage = new WUI_rrdimage(this.i18n);
+
+		//- Defaults -
+		// These modules are available on every page:
+		this.rrdimage.enabled = true;
+	}
+}
+
+//### Initialize WUI ###
+const wui = new WUImain();
+export default wui;
diff --git a/html/html/include/wui_core.mjs b/html/html/include/wui_core.mjs
new file mode 100644
index 000000000..b7b729396
--- /dev/null
+++ b/html/html/include/wui_core.mjs
@@ -0,0 +1,154 @@
+/*#############################################################################
+#                                                                             #
+# IPFire.org - A linux based firewall                                         #
+# Copyright (C) 2007-2023  IPFire Team  <info@ipfire.org>                     #
+#                                                                             #
+# This program is free software: you can redistribute it and/or modify        #
+# it under the terms of the GNU General Public License as published by        #
+# the Free Software Foundation, either version 3 of the License, or           #
+# (at your option) any later version.                                         #
+#                                                                             #
+# This program is distributed in the hope that it will be useful,             #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
+# GNU General Public License for more details.                                #
+#                                                                             #
+# You should have received a copy of the GNU General Public License           #
+# along with this program.  If not, see <http://www. gnu.org/licenses/>.       #
+#                                                                             #
+#############################################################################*/
+
+// IPFire Web User Interface - JavaScript module
+// Core functions & module helpers
+
+//--- Module template ---
+// Make sure that overridden functions are still executed with super()!
+export class WUIcore_moduleBase {
+	//- Private properties -
+	#enabled;		// Activation state, disabled by default
+	#readyState; 	// Loading state similar to Document.readyState
+	#namespace;		// Namespace derived from the class name (without "WUImod_" prefix)
+
+	//- Class constructor -
+	constructor(translations) {
+		this.i18n = translations;
+
+		this.#enabled = false;
+
+		this.#readyState = "loading";
+		this.#namespace = this.constructor.name.slice(10);
+	}
+
+	// Module activation state
+	// Note: Because a module likely changes the DOM, it is not intended that it can
+	// be cleanly disabled again. Disabling it anyway will trigger "_handleModuleDestroy".
+	set enabled(state) {
+		if(this.#enabled !== state) {
+			this.#enabled = state;
+
+			if(state) {
+				this._handleModuleEnable();
+			} else {
+				this._handleModuleDestroy();
+			}
+		}
+	}
+	get enabled() {
+		return this.#enabled;
+	}
+
+	// Module loading state
+	// loading:		Instance created
+	// interactive:	Module enabled, document not ready
+	// complete:	DOMContentLoaded/jQuery.ready event fired
+	// destroyed:	Module disabled, event handlers removed
+	get readyState() {
+		return this.#readyState;
+	}
+
+	// Module namespace
+	get namespace() {
+		return this.#namespace;
+	}
+
+	//### Protected properties ###
+
+	// Module activation state event handlers
+	_handleModuleEnable() {
+		if(this.#readyState === "loading") {
+			this.#readyState = "interactive";
+
+			// Attach jQuery.ready event
+			$(this._handleDOMReady.bind(this));
+		}
+	}
+	_handleModuleDestroy() {
+		this.#readyState = "destroyed";
+
+		// Attempt to remove all event handlers of this module
+		$("body").off(`.${this.namespace}`);
+	}
+
+	// DOMContentLoaded/jQuery.ready event handler
+	// The module must be enabled for this event to be triggered once
+	_handleDOMReady() {
+		this.#readyState = "complete";
+	}
+
+	// Translations quick access in module namespace
+	_i18n(key) {
+		return this.i18n.get(key, this.namespace);
+	}
+}
+
+//--- Simple translation strings helper ---
+export class WUIcore_i18n {
+	//- Private properties -
+	#strings;
+
+	//- Class constructor -
+	constructor() {
+		this.#strings = Object.create(null); //Object without prototypes
+	}
+
+	// Default module prefix for general translations
+	get #mainModule() {
+		return "wui";
+	}
+
+	// Get translation
+	get(key, module = this.#mainModule) {
+		const index = `${module}%${key}`;
+
+		if(Object.hasOwn(this.#strings, index)) {
+			return this.#strings[index];
+		}
+		return `(missing string '${key}' in '${module}')`;
+	}
+
+	// Add translation
+	add(key, value, module = this.#mainModule) {
+		if(typeof value === "string" || typeof value === "number") {
+			this.#strings[`${module}%${key}`] = value;
+		}
+	}
+
+	// Load key/translation JS object
+	// Format: {"key": "translation"}
+	load(translations, module = this.#mainModule) {
+		if(translations instanceof Object) {
+			Object.entries(translations).forEach(([key, value]) => {
+				this.add(key, value, module);
+			});
+		}
+	}
+}
+
+//--- Static utility functions ---
+export class WUIcore_utilities {
+	// Reload document (clears POST/GET data from history)
+	static reloadDocument() {
+		const url = window.location.origin + window.location.pathname;
+		window.location.replace(url);
+	}
+}
diff --git a/html/html/include/wui_rrdimage.mjs b/html/html/include/wui_rrdimage.mjs
new file mode 100644
index 000000000..5254b1e98
--- /dev/null
+++ b/html/html/include/wui_rrdimage.mjs
@@ -0,0 +1,30 @@
+/*#############################################################################
+#                                                                             #
+# IPFire.org - A linux based firewall                                         #
+# Copyright (C) 2007-2023  IPFire Team  <info@ipfire.org>                     #
+#                                                                             #
+# This program is free software: you can redistribute it and/or modify        #
+# it under the terms of the GNU General Public License as published by        #
+# the Free Software Foundation, either version 3 of the License, or           #
+# (at your option) any later version.                                         #
+#                                                                             #
+# This program is distributed in the hope that it will be useful,             #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
+# GNU General Public License for more details.                                #
+#                                                                             #
+# You should have received a copy of the GNU General Public License           #
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
+#                                                                             #
+#############################################################################*/
+
+// IPFire Web User Interface - JavaScript module
+
+import {WUIcore_moduleBase as WUI_module} from "./wui_core.mjs";
+
+//--- RRDtool graph images ---
+export class WUImodule_rrdimage extends WUI_module {
+	constructor(translations) {
+		super(translations);
+	}
+}
diff --git a/html/html/themes/ipfire/include/functions.pl b/html/html/themes/ipfire/include/functions.pl
index cbd05d109..445597e51 100644
--- a/html/html/themes/ipfire/include/functions.pl
+++ b/html/html/themes/ipfire/include/functions.pl
@@ -113,7 +113,8 @@ print <<END;
 	<title>$headline - $title</title>
 	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
 	<link rel="shortcut icon" href="/favicon.ico" />
-	<script type="text/javascript" src="/include/jquery.js"></script>
+	<script src="/include/jquery.js"></script>
+	<script src="/include/wui.js" type="module"></script>
 	<script src="/include/rrdimage.js"></script>
 
 	$extrahead
