Repository: manusa/electronim
Pull request #131 (2022-10-04 05:20:57)

💄 theme (light/dark/system) can be selected

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=manusa_electronim&pullRequest=131)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=131&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=131&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=131&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=CODE_SMELL) [2 Code Smells](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=CODE_SMELL)

[![85.6%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '85.6%')](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=131&metric=new_coverage&view=list) [85.6% Coverage](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=131&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=131&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=131&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=manusa_electronim&pullRequest=131)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=131&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=131&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=131&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=CODE_SMELL) [2 Code Smells](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=CODE_SMELL)

[![85.3%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '85.3%')](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=131&metric=new_coverage&view=list) [85.3% Coverage](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=131&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=131&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=131&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=manusa_electronim&pullRequest=131)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=131&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=131&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=131&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=CODE_SMELL)

[![85.6%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '85.6%')](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=131&metric=new_coverage&view=list) [85.6% Coverage](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=131&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=131&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=131&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=manusa_electronim&pullRequest=131)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=131&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=131&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=131&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=131&resolved=false&types=CODE_SMELL)

[![87.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '87.7%')](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=131&metric=new_coverage&view=list) [87.7% Coverage](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=131&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=131&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=131&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/package-lock.json b/package-lock.json
index 0c24b7f..11a8c07 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -40,7 +40,8 @@
       },
       "devDependencies": {
         "@fortawesome/fontawesome-free": "5.15.4",
-        "@testing-library/dom": "8.18.0",
+        "@testing-library/dom": "8.18.1",
+        "@testing-library/user-event": "14.4.3",
         "bulma": "0.9.4",
         "chrome-tabs": "5.4.0",
         "css-loader": "6.7.1",
@@ -1458,9 +1459,9 @@
       }
     },
     "node_modules/@testing-library/dom": {
-      "version": "8.18.0",
-      "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.18.0.tgz",
-      "integrity": "sha512-hP7v7Ifiq104Lb4dTuQzUtlkbX4sgsmxVwOTnVSDOU4ntApvPfp+eFLvasS8GpMP6pVfdbNRXNF/bSK7LTriEg==",
+      "version": "8.18.1",
+      "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.18.1.tgz",
+      "integrity": "sha512-oEvsm2B/WtcHKE+IcEeeCqNU/ltFGaVyGbpcm4g/2ytuT49jrlH9x5qRKL/H3A6yfM4YAbSbC0ceT5+9CEXnLg==",
       "dev": true,
       "dependencies": {
         "@babel/code-frame": "^7.10.4",
@@ -1476,6 +1477,19 @@
         "node": ">=12"
       }
     },
+    "node_modules/@testing-library/user-event": {
+      "version": "14.4.3",
+      "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.4.3.tgz",
+      "integrity": "sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=12",
+        "npm": ">=6"
+      },
+      "peerDependencies": {
+        "@testing-library/dom": ">=7.21.4"
+      }
+    },
     "node_modules/@tootallnate/once": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
@@ -10003,9 +10017,9 @@
       }
     },
     "@testing-library/dom": {
-      "version": "8.18.0",
-      "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.18.0.tgz",
-      "integrity": "sha512-hP7v7Ifiq104Lb4dTuQzUtlkbX4sgsmxVwOTnVSDOU4ntApvPfp+eFLvasS8GpMP6pVfdbNRXNF/bSK7LTriEg==",
+      "version": "8.18.1",
+      "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.18.1.tgz",
+      "integrity": "sha512-oEvsm2B/WtcHKE+IcEeeCqNU/ltFGaVyGbpcm4g/2ytuT49jrlH9x5qRKL/H3A6yfM4YAbSbC0ceT5+9CEXnLg==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.10.4",
@@ -10018,6 +10032,13 @@
         "pretty-format": "^27.0.2"
       }
     },
+    "@testing-library/user-event": {
+      "version": "14.4.3",
+      "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.4.3.tgz",
+      "integrity": "sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==",
+      "dev": true,
+      "requires": {}
+    },
     "@tootallnate/once": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
diff --git a/package.json b/package.json
index 92040d3..bc4afc7 100644
--- a/package.json
+++ b/package.json
@@ -156,14 +156,14 @@
       ]
     },
     "testMatch": [
-      "**/__tests__/**/*.?(m)js?(x)",
-      "**/?(*.**/?(*.)(spec|test).?(m)js?(x)"
+      "**/(*.)(spec|test).?(m)js?(x)"
     ],
     "verbose": true
   },
   "devDependencies": {
     "@fortawesome/fontawesome-free": "5.15.4",
-    "@testing-library/dom": "8.18.0",
+    "@testing-library/dom": "8.18.1",
+    "@testing-library/user-event": "14.4.3",
     "bulma": "0.9.4",
     "chrome-tabs": "5.4.0",
     "css-loader": "6.7.1",
diff --git a/src/chrome-tabs/chrome-tabs.browser.js b/src/chrome-tabs/chrome-tabs.browser.js
index c9b2fee..aa12d77 100644
--- a/src/chrome-tabs/chrome-tabs.browser.js
+++ b/src/chrome-tabs/chrome-tabs.browser.js
@@ -18,7 +18,7 @@ const {render} = window.preact;
 const {useLayoutEffect, useReducer, useState} = window.preactHooks;
 const html = window.html;
 
-const shouldUseDarkColors = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
+const shouldUseDarkColors = () => window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
 const getTabContainer = () => document.querySelector('.tab-container');
 const getChromeTabs = () => getTabContainer().querySelector('.chrome-tabs');
 
@@ -198,7 +198,7 @@ const Tab = ({dispatch, numberOfTabs, idx, id, active, offsetX = 0, title, url,
     onDrag: tabDrag,
     onDragEnd: tabDrag,
     style: tabStyle(width, idx, offsetX),
-    onclick: tabClick
+    onClick: tabClick
   };
   if (active === true) {
     props.active = '';
@@ -229,7 +229,7 @@ const ChromeTabs = ({dispatch, state: {tabs}}) => {
   });
   return html`
     <div
-      class=${`chrome-tabs ${shouldUseDarkColors ? 'chrome-tabs-dark-theme' : ''}`}
+      class=${`chrome-tabs ${shouldUseDarkColors() ? 'chrome-tabs-dark-theme' : ''}`}
       style=${`--tab-content-margin: ${TAB_CONTENT_MARGIN}px`}
       ondragover=${event => event.preventDefault()}
     >
diff --git a/src/components/README.md b/src/components/README.md
index d9c50b6..99af042 100644
--- a/src/components/README.md
+++ b/src/components/README.md
@@ -1,33 +1,37 @@
 # Components
 
-This module contains the library of shared components to be used across the application.
+This ES module contains the library of shared components to be used across the application.
 
 ## Structure
 
-Since ElectronIM is based on Preact in combination with Electron, to be able to use the component within a browser, you need to pass the `html` bound `h` function from `preact` to the component.
-
-Every component definition will start with a function that accepts the `html` parameter that returns an arrow function with the component.
+To be able to use the component within a browser, there needs to be an  `html` bound `h` function from `preact` globally available.
 
 ```javascript
-module.exports.Component = html => ({property1}) => html`
+export const Component = ({property1}) => html`
   <div>Your Component Definition</div>
 `;
 ```
 
-## Using Components
-
-Components need to be imported in a `preload.js` script in order to be used ina `browser-xxx.js` file.
+To expose the component in the public API, it needs to be referenced from the `index.mjs` file.
 
 ```javascript
-window.preact = require('preact');
-window.html = require('htm').bind(window.preact.h);
-window.Component = require('../components').component(window.html);
+export {Component} from './component.mjs';
 ```
 
-Then you can use the component in the `browser-xxx.js` file.
+## Using Components
+
+Components are ES modules that can be imported from the browser js (`xxx.browser.mjs` files).
 
 ```javascript
-const {html, Component, preact: {render}} = window;
+const {html} = window;
+import {Component} from '../components/index.mjs';
+
 const App = () => html`<${Component} property1='value1' />`;
 render(html`<${App} />`, document.querySelector('.root'));
 ```
+
+Note that the `xxx.browser.mjs` files need to be loaded as JavaScript ES modules in the `index.html` file.
+
+```html
+<script src="./xxx.browser.mjs" type="module"></script>
+```
diff --git a/src/components/form/checkbox.mjs b/src/components/form/checkbox.mjs
new file mode 100644
index 0000000..f8bf9a5
--- /dev/null
+++ b/src/components/form/checkbox.mjs
@@ -0,0 +1,25 @@
+/*
+   Copyright 2022 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+const {html} = window;
+
+export const Checkbox = ({label, title = '', icon, checked, value, onClick, ...properties}) => (html`
+  <div class='control'>
+    <label class='checkbox' title=${title}>
+      <input type='checkbox' checked=${checked} title=${title} value=${value} onClick=${onClick} ...${properties} />
+      ${icon && html`<i class='checkbox__icon fas ${icon}'></i>`} ${label}
+    </label>
+  </div>
+`);
diff --git a/src/components/form/field.mjs b/src/components/form/field.mjs
new file mode 100644
index 0000000..8cb5051
--- /dev/null
+++ b/src/components/form/field.mjs
@@ -0,0 +1,43 @@
+/*
+   Copyright 2022 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+const {html} = window;
+
+import {sizes} from './index.mjs';
+
+const FieldContainer = ({className = '', children, ...properties}) => (html`
+  <div class=${`field ${className}`} ...${properties}>
+    ${children}
+  </div>
+`);
+
+export const Field = ({label, children, ...properties}) => (html`
+  <${FieldContainer} ...${properties}>
+    ${label && html`<label class='label'>${label}</label>`}
+    <div class='control'>${children}</div>
+  </${FieldContainer}>
+`);
+
+export const HorizontalField = ({label, labelSize = sizes.normal, children, ...properties}) => {
+  const labelClassName = `field-label ${labelSize}`;
+  return html`
+    <${FieldContainer} className='is-horizontal' ...${properties}>
+      ${label && html`<div class=${labelClassName}><label class='label'>${label}</label></div>`}
+      <div class='field-body'>
+        <div class='control'>${children}</div>
+      </div>
+    </${FieldContainer}>
+  `;
+};
diff --git a/src/components/form/index.mjs b/src/components/form/index.mjs
new file mode 100644
index 0000000..6ca6028
--- /dev/null
+++ b/src/components/form/index.mjs
@@ -0,0 +1,26 @@
+/*
+   Copyright 2022 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+
+export const sizes = {
+  small: 'is-small',
+  normal: 'is-normal',
+  medium: 'is-medium',
+  large: 'is-large'
+};
+
+export {Checkbox} from './checkbox.mjs';
+export {Field, HorizontalField} from './field.mjs';
+export {Select} from './select.mjs';
diff --git a/src/components/form/select.mjs b/src/components/form/select.mjs
new file mode 100644
index 0000000..18bc3f0
--- /dev/null
+++ b/src/components/form/select.mjs
@@ -0,0 +1,40 @@
+/*
+   Copyright 2022 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+const {html} = window;
+
+import {sizes} from './index.mjs';
+
+export const Select = ({
+  value, onChange, size = sizes.normal, children
+}) => (html`
+  <div class='control'>
+    <div class=${`select ${size}`}>
+      <select value=${value} onChange=${onChange}>
+        ${children}
+      </select>
+    </div>
+  </div>
+`);
+
+Select.Option = ({value, children}) => {
+  const props = {};
+  if (value) {
+    props.value = value;
+  }
+  return html`
+    <option ...${props}>${children}</option>
+  `;
+};
diff --git a/src/components/index.js b/src/components/index.js
deleted file mode 100644
index 91c80db..0000000
--- a/src/components/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
-  topBar: require('./top-bar').topBar
-};
diff --git a/src/components/index.mjs b/src/components/index.mjs
new file mode 100644
index 0000000..5f71b8a
--- /dev/null
+++ b/src/components/index.mjs
@@ -0,0 +1,19 @@
+/*
+   Copyright 2022 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+
+export {Checkbox, Field, HorizontalField, Select, sizes} from './form/index.mjs';
+export {Panel} from './panel.mjs';
+export {TopBar} from './top-bar.mjs';
diff --git a/src/components/panel.mjs b/src/components/panel.mjs
new file mode 100644
index 0000000..5f26162
--- /dev/null
+++ b/src/components/panel.mjs
@@ -0,0 +1,27 @@
+/*
+   Copyright 2022 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+const {html} = window;
+
+export const Panel = ({heading, className = '', children}) => (html`
+  <nav class=${`panel ${className}`}>
+    ${heading && html`<p class='panel-heading'>${heading}</p>`}
+    ${children}
+  </nav>
+`);
+
+Panel.Block = ({className = '', children}) => (html`
+  <div class=${`panel-block ${className}`}>${children}</div>
+`);
diff --git a/src/components/top-bar.js b/src/components/top-bar.js
deleted file mode 100644
index e88e388..0000000
--- a/src/components/top-bar.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * A TopBar partially based on Bulma's Navbar component.
- */
-module.exports.topBar = html => ({
-  title,
-  titleClass = 'navbar-item is-size-5 has-text-info has-text-weight-bold',
-  containerClass = '',
-  endComponents = '',
-  endComponentsClass = '',
-  fixed = false
-}) => {
-  if (fixed) {
-    document.body.classList.add('has-navbar-fixed-top');
-  }
-  return html`
-    <nav class=${`navbar ${fixed ? 'is-fixed-top' : ''}`}>
-      <div class=${`not-navbar-brand is-flex is-flex-grow-1 is-align-content-center ${containerClass}`}>
-        <div class=${`not-navbar-brand is-flex is-flex-grow-1 ${titleClass}`}>${title}</div>
-        <div class=${endComponentsClass}>${endComponents}</div>
-      </div>
-    </nav>
-  `;
-};
diff --git a/src/components/top-bar.mjs b/src/components/top-bar.mjs
new file mode 100644
index 0000000..0714726
--- /dev/null
+++ b/src/components/top-bar.mjs
@@ -0,0 +1,40 @@
+/*
+   Copyright 2022 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+const {html} = window;
+
+/**
+ * A TopBar partially based on Bulma's Navbar component.
+ */
+export const TopBar = ({
+  title,
+  titleClass = 'navbar-item is-size-5 has-text-info has-text-weight-bold',
+  containerClass = '',
+  endComponents = '',
+  endComponentsClass = '',
+  fixed = false
+}) => {
+  if (fixed) {
+    document.body.classList.add('has-navbar-fixed-top');
+  }
+  return html`
+    <nav class=${`top-bar navbar ${fixed ? 'is-fixed-top' : ''}`}>
+      <div class=${`not-navbar-brand is-flex is-flex-grow-1 is-align-content-center ${containerClass}`}>
+        <div class=${`not-navbar-brand is-flex is-flex-grow-1 ${titleClass}`}>${title}</div>
+        <div class=${endComponentsClass}>${endComponents}</div>
+      </div>
+    </nav>
+  `;
+};
diff --git a/src/help/help.browser.js b/src/help/help.browser.mjs
similarity index 92%
rename from src/help/help.browser.js
rename to src/help/help.browser.mjs
index a51fdb6..5ef57bc 100644
--- a/src/help/help.browser.js
+++ b/src/help/help.browser.mjs
@@ -13,7 +13,9 @@
    See the License for the specific language governing permissions and
    limitations under the License.
  */
-const {APP_EVENTS, ELECTRONIM_VERSION, docs, html, ipcRenderer, preact: {render}, TopBar} = window;
+const {APP_EVENTS, ELECTRONIM_VERSION, docs, html, ipcRenderer, preact: {render}} = window;
+
+import {TopBar} from '../components/index.mjs';
 
 const helpRoot = () => document.querySelector('.help-root');
 const close = () => ipcRenderer.send(APP_EVENTS.closeDialog);
@@ -55,7 +57,7 @@ const Help = () => html`
     <${TopBar} fixed=${true} title='Help'
       endComponents=${html`
         <div class='navbar-item'>
-          <button class="button is-link is-light" onclick=${close}>Close</button>
+          <button class="button is-link is-light" onClick=${close}>Close</button>
         </div>
       `}
     />
diff --git a/src/help/index.html b/src/help/index.html
index 49f29a5..e914992 100644
--- a/src/help/index.html
+++ b/src/help/index.html
@@ -26,6 +26,6 @@ limitations under the License.
     <script src="../../bundles/lib/bulma/css/bulma.css.js"></script>
     <script src="../../bundles/lib/bulma/bulma.dark.scss.js"></script>
     <script src="../../bundles/lib/@fortawesome/fontawesome-free/css/all.css.js"></script>
-    <script src="./help.browser.js"></script>
+    <script src="./help.browser.mjs" type="module"></script>
   </body>
 </html>
diff --git a/src/help/preload.js b/src/help/preload.js
index 49319cc..ec6d896 100644
--- a/src/help/preload.js
+++ b/src/help/preload.js
@@ -16,7 +16,6 @@
 /* eslint-disable no-undef */
 const {ipcRenderer} = require('electron');
 const {docs} = require('!val-loader!./docs.browser.val-loader');
-const {topBar} = require('../components/top-bar');
 
 require('./help.browser.css');
 
@@ -26,6 +25,5 @@ window.ELECTRONIM_VERSION = ELECTRONIM_VERSION;
 window.preact = require('preact');
 window.preactHooks = require('preact/hooks');
 window.html = require('htm').bind(window.preact.h);
-window.TopBar = topBar(window.html);
 
 window.docs = docs;
diff --git a/src/main/index.js b/src/main/index.js
index 71149a0..1aaf899 100644
--- a/src/main/index.js
+++ b/src/main/index.js
@@ -13,7 +13,7 @@
    See the License for the specific language governing permissions and
    limitations under the License.
  */
-const {BrowserWindow, Notification, app, desktopCapturer, ipcMain: ipc} = require('electron');
+const {BrowserWindow, Notification, app, desktopCapturer, ipcMain: ipc, nativeTheme} = require('electron');
 const {APP_EVENTS} = require('../constants');
 const {TABS_CONTAINER_HEIGHT, initTabContainer} = require('../chrome-tabs');
 const {loadSettings, updateSettings, openSettingsDialog} = require('../settings');
@@ -154,6 +154,7 @@ const closeDialog = () => {
 const saveSettings = (_event, settings) => {
   updateSettings(settings);
   loadDictionaries();
+  nativeTheme.themeSource = settings.theme ?? 'system';
   const currentBrowserView = mainWindow.getBrowserView();
   mainWindow.removeBrowserView(currentBrowserView);
   tabManager.removeAll();
@@ -179,7 +180,8 @@ const browserVersionsReady = () => {
 const init = () => {
   fixUserDataLocation();
   loadDictionaries();
-  const {width = 800, height = 600} = loadSettings();
+  const {width = 800, height = 600, theme} = loadSettings();
+  nativeTheme.themeSource = theme ?? 'system';
   mainWindow = new BrowserWindow({
     width, height, resizable: true, maximizable: true, webPreferences
   });
diff --git a/src/settings/index.html b/src/settings/index.html
index 8622542..93bea7f 100644
--- a/src/settings/index.html
+++ b/src/settings/index.html
@@ -23,9 +23,9 @@ limitations under the License.
   </head>
   <body>
     <div class="settings container is-fluid"></div>
-    <script src="./settings.browser.mjs" type="module"></script>
     <script src="../../bundles/lib/bulma/css/bulma.css.js"></script>
     <script src="../../bundles/lib/bulma/bulma.dark.scss.js"></script>
     <script src="../../bundles/lib/@fortawesome/fontawesome-free/css/all.css.js"></script>
+    <script src="./settings.browser.mjs" type="module"></script>
   </body>
 </html>
diff --git a/src/settings/preload.js b/src/settings/preload.js
index 081592c..1ba6a45 100644
--- a/src/settings/preload.js
+++ b/src/settings/preload.js
@@ -15,7 +15,6 @@
  */
 /* eslint-disable no-undef */
 const {ipcRenderer} = require('electron');
-const {topBar} = require('../components/top-bar');
 
 require('./settings.browser.css');
 
@@ -25,4 +24,3 @@ window.ELECTRONIM_VERSION = ELECTRONIM_VERSION;
 window.preact = require('preact');
 window.preactHooks = require('preact/hooks');
 window.html = require('htm').bind(window.preact.h);
-window.TopBar = topBar(window.html);
diff --git a/src/settings/settings.browser.css b/src/settings/settings.browser.css
index 3ea981a..20cadce 100644
--- a/src/settings/settings.browser.css
+++ b/src/settings/settings.browser.css
@@ -20,15 +20,6 @@
     --border-color: #dbdbdb;
 }
 
-.control .checkbox {
-    user-select: none;
-}
-
-.control .checkbox .checkbox__icon {
-    width: calc(1em + var(--default-spacing));
-    text-align: center;
-}
-
 .settings.container {
     margin:0;
     padding: 15px 0;
diff --git a/src/settings/settings.browser.mjs b/src/settings/settings.browser.mjs
index 50f52d8..2991521 100644
--- a/src/settings/settings.browser.mjs
+++ b/src/settings/settings.browser.mjs
@@ -15,14 +15,16 @@
  */
 /* eslint-disable no-undef */
 const {
-  APP_EVENTS, ELECTRONIM_VERSION, html, ipcRenderer, preact: {render}, preactHooks: {useReducer}, TopBar
+  APP_EVENTS, ipcRenderer, preact: {render}, preactHooks: {useReducer}
 } = window;
+
+import {Checkbox, TopBar} from '../components/index.mjs';
 import {
   ACTIONS, reducer, dictionariesEnabled, setTabProperty, toggleTabProperty
 } from './settings.reducer.browser.mjs';
 import {
-  Checkbox
-} from './settings.common.browser.mjs';
+  OtherContainer
+} from './settings.other.browser.mjs';
 import {
   SpellCheckContainer
 } from './settings.spell-check.browser.mjs';
@@ -36,8 +38,8 @@ const newTabClass = state => {
   return state.newTabValid ? 'is-success' : 'is-danger';
 };
 
-const SettingsButton = ({icon, disabled = false, title, onclick}) => (html`
-  <button class='settings__button button' disabled=${disabled} onclick=${onclick}>
+const SettingsButton = ({icon, disabled = false, title, onClick}) => (html`
+  <button class='settings__button button' disabled=${disabled} onClick=${onClick}>
     <span class='icon is-medium' title='${title}'>
       <i class='fas ${icon}'></i>
     </span>
@@ -48,7 +50,7 @@ const ExpandButton = ({dispatch, id, expanded = false}) => {
   const properties = {
     icon: expanded ? 'fa-chevron-down' : 'fa-chevron-right',
     title: expanded ? 'Collapse' : 'Expand (show advanced settings)',
-    onclick: () => dispatch({type: ACTIONS.TOGGLE_TAB_EXPANDED, payload: id})
+    onClick: () => dispatch({type: ACTIONS.TOGGLE_TAB_EXPANDED, payload: id})
   };
   return html`
     <${SettingsButton} ...${properties} />
@@ -65,7 +67,7 @@ const TabAdvancedSettings = (
     <${Checkbox} label="Sandbox" checked=${sandboxed} value=${id}
       title='Use an isolated/sandboxed session for this tab'
       icon=${sandboxedIcon(sandboxed)}
-      onclick=${toggleTabProperty(dispatch, 'sandboxed', id)}
+      onClick=${toggleTabProperty(dispatch, 'sandboxed', id)}
     />
   </div>
 `);
@@ -83,14 +85,14 @@ const TabEntry = ({
       </div>
       <${SettingsButton} icon=${disabledIcon(disabled)}
         title=${disabled ? 'Tab disabled. Click to enable' : 'Tab enabled. Click to disable'}
-        onclick=${toggleTabProperty(dispatch, 'disabled', id)}
+        onClick=${toggleTabProperty(dispatch, 'disabled', id)}
       />
       <${SettingsButton} icon=${disableNotifications ? 'fa-bell-slash' : 'fa-bell'}
         title=${disableNotifications ? 'Notifications disabled. Click to enable' : 'Notifications enabled. Click to disable'}
-        onclick=${toggleTabProperty(dispatch, 'disableNotifications', id)}
+        onClick=${toggleTabProperty(dispatch, 'disableNotifications', id)}
       />
       <${SettingsButton} icon='fa-trash' title='Delete tab'
-        onclick=${() => dispatch({type: ACTIONS.REMOVE, payload: {id}})}
+        onClick=${() => dispatch({type: ACTIONS.REMOVE, payload: {id}})}
       /> 
     </div>
     <${TabAdvancedSettings} dispatch=${dispatch} id=${id} expanded=${expanded} disabled=${disabled}
@@ -113,12 +115,12 @@ const Settings = ({initialState}) => {
       addTab();
     }
   };
-  const toggleNotifications = () => dispatch({type: ACTIONS.TOGGLE_GLOBAL_NOTIFICATIONS});
   const save = () => ipcRenderer.send(APP_EVENTS.settingsSave, {
     tabs: state.tabs,
     useNativeSpellChecker: state.useNativeSpellChecker,
     enabledDictionaries,
-    disableNotificationsGlobally: state.disableNotificationsGlobally
+    disableNotificationsGlobally: state.disableNotificationsGlobally,
+    theme: state.theme
   });
   const cancel = () => ipcRenderer.send(APP_EVENTS.closeDialog);
   return html`
@@ -127,10 +129,10 @@ const Settings = ({initialState}) => {
       <div class="navbar-item field is-grouped">
         <div class="control">
           <button class="settings__submit button is-link"
-            disabled=${!state.canSave || state.invalidTabs.size !== 0} onclick=${save}>Ok</button>
+            disabled=${!state.canSave || state.invalidTabs.size !== 0} onClick=${save}>Ok</button>
         </div>
         <div class="control">
-          <button class="settings__cancel button is-link is-light" onclick=${cancel}>Cancel</button>
+          <button class="settings__cancel button is-link is-light" onClick=${cancel}>Cancel</button>
         </div>
       </div>
     `}
@@ -149,7 +151,7 @@ const Settings = ({initialState}) => {
               onkeydown=${onNewKeyDown}
             />
           </div>
-          <${SettingsButton} icon='fa-plus' onclick=${addTab} disabled=${!state.newTabValid} />
+          <${SettingsButton} icon='fa-plus' onClick=${addTab} disabled=${!state.newTabValid} />
         </div>
         <div class="settings__tabs container field">
           ${state.tabs.map(tab => (html`
@@ -161,23 +163,7 @@ const Settings = ({initialState}) => {
         </div>
       </nav>
       <${SpellCheckContainer} dispatch=${dispatch} state=${state} />
-      <nav class="panel">
-        <p class="panel-heading">Other</p>
-        <div class="panel-block">
-          <div class="settings__global-notifications container">
-            <${Checkbox}
-              label="Disable notifications globally"
-              icon=${state.disableNotificationsGlobally ? 'fa-bell-slash' : 'fa-bell'}
-              checked=${state.disableNotificationsGlobally}
-              value=${state.disableNotificationsGlobally}
-              onclick=${toggleNotifications}
-            />
-          </div>
-        </div>
-        <div class="panel-block is-italic">
-          ElectronIM version ${ELECTRONIM_VERSION}
-        </div>
-      </nav>
+      <${OtherContainer} dispatch=${dispatch} state=${state} />
     </div>
   </div>
 `;
@@ -205,7 +191,8 @@ Promise.all([
       newTabValue: '',
       invalidTabs: new Set(),
       canSave: currentSettings.tabs.length > 0,
-      disableNotificationsGlobally: currentSettings.disableNotificationsGlobally
+      disableNotificationsGlobally: currentSettings.disableNotificationsGlobally,
+      theme: currentSettings.theme
     };
     render(html`<${Settings} initialState=${initialState} />`, settingsRoot());
   }
diff --git a/src/settings/settings.common.browser.mjs b/src/settings/settings.common.browser.mjs
index 0d17fdf..fa96ddb 100644
--- a/src/settings/settings.common.browser.mjs
+++ b/src/settings/settings.common.browser.mjs
@@ -13,7 +13,6 @@
    See the License for the specific language governing permissions and
    limitations under the License.
  */
-const {html} = window;
 
 export const prependProtocol = url => {
   if (url && !url.match(/^https?:\/\/.+/)) {
@@ -41,11 +40,3 @@ export const newId = () => (
   new Date().getTime().toString(36) + Math.random().toString(36).slice(2) // NOSONAR
 );
 
-export const Checkbox = ({label, title = '', icon, checked, value, onclick, ...properties}) => (html`
-  <div class='control'>
-    <label class='checkbox' title=${title}>
-      <input type='checkbox' checked=${checked} title=${title} value=${value} onclick=${onclick} ...${properties} />
-      ${icon && html`<i class='checkbox__icon fas ${icon}'></i>`} ${label}
-    </label>
-  </div>
-`);
diff --git a/src/settings/settings.other.browser.mjs b/src/settings/settings.other.browser.mjs
new file mode 100644
index 0000000..3d9e4d6
--- /dev/null
+++ b/src/settings/settings.other.browser.mjs
@@ -0,0 +1,50 @@
+/*
+   Copyright 2022 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+const {ELECTRONIM_VERSION, html} = window;
+
+import {Checkbox, HorizontalField, Panel, Select} from '../components/index.mjs';
+import {
+  setTheme,
+  toggleNotifications
+} from './settings.reducer.browser.mjs';
+
+export const OtherContainer = ({dispatch, state}) => html`
+  <${Panel} heading='Other' className='settings__other'>
+    <${Panel.Block}>
+      <${HorizontalField} label='Theme' data-testid='settings-theme-select'>
+        <${Select} value=${state.theme} onChange=${e => setTheme({dispatch})(e.target.value)}>
+          <${Select.Option} value='system'>system</${Select.Option}>
+          <${Select.Option} value='light'>light</${Select.Option}>
+          <${Select.Option} value='dark'>dark</${Select.Option}>
+        </${Select}>
+      </${HorizontalField}>
+    </${Panel.Block}>
+    <${Panel.Block}>
+      <div class="settings__global-notifications container">
+        <${Checkbox}
+          label="Disable notifications globally"
+          icon=${state.disableNotificationsGlobally ? 'fa-bell-slash' : 'fa-bell'}
+          checked=${state.disableNotificationsGlobally}
+          value=${state.disableNotificationsGlobally}
+          onClick=${toggleNotifications({dispatch})}
+        />
+      </div>
+    </${Panel.Block}>
+    <${Panel.Block} className='is-italic'>
+      ElectronIM version ${ELECTRONIM_VERSION}
+    </${Panel.Block}>
+  </${Panel}>
+`;
diff --git a/src/settings/settings.reducer.browser.mjs b/src/settings/settings.reducer.browser.mjs
index e89286f..312da3d 100644
--- a/src/settings/settings.reducer.browser.mjs
+++ b/src/settings/settings.reducer.browser.mjs
@@ -19,6 +19,7 @@ export const ACTIONS = {
   ADD: 'ADD',
   REMOVE: 'REMOVE',
   SET_TAB_PROPERTY: 'SET_TAB_PROPERTY',
+  SET_THEME: 'SET_THEME',
   TOGGLE_USE_NATIVE_SPELL_CHECKER: 'TOGGLE_USE_NATIVE_SPELL_CHECKER',
   TOGGLE_DICTIONARY: 'TOGGLE_DICTIONARY',
   TOGGLE_TAB_EXPANDED: 'TOGGLE_TAB_EXPANDED',
@@ -85,6 +86,11 @@ export const reducer = (state, action) => {
       });
       return newState;
     }
+    case ACTIONS.SET_THEME: {
+      return {...state,
+        theme: action.payload.theme
+      };
+    }
     case ACTIONS.TOGGLE_USE_NATIVE_SPELL_CHECKER: {
       return {...state,
         useNativeSpellChecker: !state.useNativeSpellChecker
@@ -137,11 +143,15 @@ export const reducer = (state, action) => {
 };
 
 // Action creators
+export const setTabProperty = ({dispatch, property, value, id}) =>
+  dispatch({type: ACTIONS.SET_TAB_PROPERTY, payload: {id, property, value}});
+export const setTheme = ({dispatch}) =>
+  theme => dispatch({type: ACTIONS.SET_THEME, payload: {theme}});
 export const toggleUseNativeSpellChecker = ({dispatch}) =>
   () => dispatch({type: ACTIONS.TOGGLE_USE_NATIVE_SPELL_CHECKER});
 export const toggleDictionary = ({dispatch, languageKey}) =>
   () => dispatch({type: ACTIONS.TOGGLE_DICTIONARY, payload: languageKey});
-export const setTabProperty = ({dispatch, property, value, id}) =>
-  dispatch({type: ACTIONS.SET_TAB_PROPERTY, payload: {id, property, value}});
+export const toggleNotifications = ({dispatch}) =>
+  () => dispatch({type: ACTIONS.TOGGLE_GLOBAL_NOTIFICATIONS});
 export const toggleTabProperty = (dispatch, property, id) =>
   () => dispatch({type: ACTIONS.TOGGLE_TAB_PROPERTY, payload: {id, property}});
diff --git a/src/settings/settings.spell-check.browser.mjs b/src/settings/settings.spell-check.browser.mjs
index 219c12b..6d4b884 100644
--- a/src/settings/settings.spell-check.browser.mjs
+++ b/src/settings/settings.spell-check.browser.mjs
@@ -15,6 +15,7 @@
  */
 const {html} = window;
 
+import {Checkbox, Panel} from '../components/index.mjs';
 import {
   dictionaries,
   dictionariesEnabled,
@@ -22,12 +23,11 @@ import {
   toggleDictionary,
   useNativeSpellChecker
 } from './settings.reducer.browser.mjs';
-import {Checkbox} from './settings.common.browser.mjs';
 
 const LanguageEntry = ({dispatch, languageKey, name, enabled = false}) => html`
   <${Checkbox} label=${`${name} (${languageKey})`} checked=${enabled}
     value=${languageKey}
-    onclick=${toggleDictionary({dispatch, languageKey})}
+    onClick=${toggleDictionary({dispatch, languageKey})}
   />
 `;
 
@@ -35,16 +35,15 @@ export const SpellCheckContainer = ({dispatch, state}) => {
   const useNative = useNativeSpellChecker(state);
   const enabledDictionaries = dictionariesEnabled(state);
   return html`
-    <nav class="settings__spell-check panel">
-      <p class="panel-heading">Spell check</p>
-      <div class="settings__spell-check-common panel-block">
+    <${Panel} heading='Spell check' className='settings__spell-check'>
+      <${Panel.Block} className='settings__spell-check-common'>
         <${Checkbox}
             label='Use Native Spell Checker' checked=${useNative} value=${useNative}
-            onclick="${toggleUseNativeSpellChecker({dispatch})}"
+            onClick="${toggleUseNativeSpellChecker({dispatch})}"
             data-testid='use-native-spell-checker'
         />
-      </div>
-      <div class="panel-block">
+      </${Panel.Block} >
+      <${Panel.Block} >
         <div class="settings__dictionaries container">${
   Object.entries(dictionaries(state))
     .sort(([, {name: name1}], [, {name: name2}]) => name1.localeCompare(name2))
@@ -54,7 +53,7 @@ export const SpellCheckContainer = ({dispatch, state}) => {
       />
     `))}
         </div>
-      </div>
-    </nav>
+      </${Panel.Block} >
+    </${Panel}>
   `;
 };
diff --git a/src/styles/components/checkbox.css b/src/styles/components/checkbox.css
new file mode 100644
index 0000000..95c7ac6
--- /dev/null
+++ b/src/styles/components/checkbox.css
@@ -0,0 +1,24 @@
+/*
+   Copyright 2022 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+
+.electronim .control .checkbox {
+  user-select: none;
+}
+
+.electronim .control .checkbox .checkbox__icon {
+  width: calc(1em + var(--default-spacing));
+  text-align: center;
+}
diff --git a/src/styles/components/index.css b/src/styles/components/index.css
index 974679a..3ab628e 100644
--- a/src/styles/components/index.css
+++ b/src/styles/components/index.css
@@ -13,4 +13,5 @@
    See the License for the specific language governing permissions and
    limitations under the License.
  */
-@import './navbar.css';
+@import './checkbox.css';
+@import './top-bar.css';
diff --git a/src/styles/components/navbar.css b/src/styles/components/top-bar.css
similarity index 95%
rename from src/styles/components/navbar.css
rename to src/styles/components/top-bar.css
index 5356bc6..4a5775f 100644
--- a/src/styles/components/navbar.css
+++ b/src/styles/components/top-bar.css
@@ -13,6 +13,6 @@
    See the License for the specific language governing permissions and
    limitations under the License.
  */
-.electronim .navbar {
+.electronim .top-bar.navbar {
   background: var(--color-canvas-default);
 }


Test patch:
diff --git a/src/__tests__/dom.mjs b/src/__tests__/dom.mjs
new file mode 100644
index 0000000..1c7a9ae
--- /dev/null
+++ b/src/__tests__/dom.mjs
@@ -0,0 +1,32 @@
+/*
+   Copyright 2022 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+
+import {fileURLToPath} from 'url';
+import {dirname, resolve} from 'path';
+import {readFileSync} from 'fs';
+
+const parser = new DOMParser();
+
+export const loadDOM = async ({meta, path}) => {
+  const pathSegments = [dirname(fileURLToPath(meta.url)), ...path];
+  const htmlPath = resolve(...pathSegments);
+  const html = readFileSync(htmlPath, 'utf8');
+  const index = parser.parseFromString(html, 'text/html');
+  document.body.innerHTML = index.body.innerHTML;
+  for (const script of index.querySelectorAll('script[src]')) {
+    await import(`${dirname(htmlPath)}/${script.getAttribute('src')}`);
+  }
+};
diff --git a/src/__tests__/index.mjs b/src/__tests__/index.mjs
new file mode 100644
index 0000000..bc7f030
--- /dev/null
+++ b/src/__tests__/index.mjs
@@ -0,0 +1,17 @@
+/*
+   Copyright 2022 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+
+export {loadDOM} from './dom.mjs';
diff --git a/src/constants/__tests/index.test.js b/src/constants/__tests__/index.test.js
similarity index 100%
rename from src/constants/__tests/index.test.js
rename to src/constants/__tests__/index.test.js
diff --git a/src/help/__tests__/help.browser.test.js b/src/help/__tests__/help.browser.test.mjs
similarity index 81%
rename from src/help/__tests__/help.browser.test.js
rename to src/help/__tests__/help.browser.test.mjs
index b465f7b..15b4e75 100644
--- a/src/help/__tests__/help.browser.test.js
+++ b/src/help/__tests__/help.browser.test.mjs
@@ -13,28 +13,21 @@
    See the License for the specific language governing permissions and
    limitations under the License.
  */
-const {fireEvent} = require('@testing-library/dom');
-
-const mockDOM = () => {
-  document.body.innerHTML = '';
-  const $root = document.createElement('div');
-  $root.innerHTML = '<div class="help-root"></div>';
-  document.body.append($root);
-};
+import {jest} from '@jest/globals';
+import {loadDOM} from '../../__tests__/index.mjs';
+import {fireEvent} from '@testing-library/dom';
 
 describe('Help in Browser test suite', () => {
   let mockIpcRenderer;
-  beforeEach(() => {
-    require('../../../bundles/help.preload');
+  beforeEach(async () => {
+    jest.resetModules();
     mockIpcRenderer = {
       send: jest.fn()
     };
-    mockDOM();
-    window.ipcRenderer = mockIpcRenderer;
+    await import('../../../bundles/help.preload');
     window.ELECTRONIM_VERSION = '1.33.7';
-    jest.isolateModules(() => {
-      require('../help.browser');
-    });
+    window.ipcRenderer = mockIpcRenderer;
+    await loadDOM({meta: import.meta, path: ['..', 'index.html']});
   });
   test('render, should render all documents', () => {
     // Then
diff --git a/src/help/__tests__/preload.test.js b/src/help/__tests__/preload.test.js
index f546148..6308ee4 100644
--- a/src/help/__tests__/preload.test.js
+++ b/src/help/__tests__/preload.test.js
@@ -43,19 +43,19 @@ describe('Help Module preload test suite', () => {
       // Then
       await waitFor(() => expect(document.head.children.length).toBeGreaterThan(0));
       const styles = Array.from(document.querySelectorAll('style'));
-      expect(styles).toHaveLength(8);
+      expect(styles).toHaveLength(9);
       expect(styles[0].innerHTML).toMatch(/:root \{.+--color-accent-fg:/s); // Variables
       expect(styles[1].innerHTML).toContain('html.electronim,'); // Base
       expect(styles[2].innerHTML).toContain('.electronim h1,'); // Typography
-      expect(styles[4].innerHTML).toContain('.electronim .navbar {'); // Components
-      expect(styles[7].innerHTML).toContain('.help-root {'); // Help-specific
+      expect(styles[4].innerHTML).toContain('.electronim .control .checkbox {'); // CheckBox
+      expect(styles[5].innerHTML).toContain('.electronim .top-bar.navbar {'); // NavBar
+      expect(styles[8].innerHTML).toContain('.help-root {'); // Help-specific
     });
     test('adds required libraries', () => {
       expect(window.ELECTRONIM_VERSION).toEqual('0.0.0');
       expect(window.preact).not.toBeUndefined();
       expect(window.preactHooks).not.toBeUndefined();
       expect(window.html).not.toBeUndefined();
-      expect(window.TopBar).not.toBeUndefined();
     });
     test('loads document contents with valid asset URLs', () => {
       expect(window.docs).toEqual(expect.objectContaining({
diff --git a/src/main/__tests__/index.test.js b/src/main/__tests__/index.test.js
index 9e6d593..cf431db 100644
--- a/src/main/__tests__/index.test.js
+++ b/src/main/__tests__/index.test.js
@@ -19,6 +19,7 @@ describe('Main module test suite', () => {
   let mockApp;
   let mockDesktopCapturer;
   let mockIpc;
+  let mockNativeTheme;
   let mockTabContainer;
   let mockSettings;
   let settingsModule;
@@ -49,6 +50,7 @@ describe('Main module test suite', () => {
         mockIpc.listeners[eventName] = func;
       })
     };
+    mockNativeTheme = {};
     mockTabContainer = {};
     mockSettings = {};
     jest.mock('electron', () => ({
@@ -56,7 +58,8 @@ describe('Main module test suite', () => {
       Notification: jest.fn(() => mockNotification),
       app: mockApp,
       desktopCapturer: mockDesktopCapturer,
-      ipcMain: mockIpc
+      ipcMain: mockIpc,
+      nativeTheme: mockNativeTheme
     }));
     jest.mock('../../chrome-tabs', () => ({
       TABS_CONTAINER_HEIGHT: 46,
@@ -83,6 +86,22 @@ describe('Main module test suite', () => {
     main = require('../');
   });
   describe('init - environment preparation', () => {
+    describe('theme', () => {
+      test('fallbacks to system', () => {
+        // When
+        main.init();
+        // Then
+        expect(mockNativeTheme.themeSource).toBe('system');
+      });
+      test('uses theme from saved settings', () => {
+        // Given
+        mockSettings.theme = 'dark';
+        // When
+        main.init();
+        // Then
+        expect(mockNativeTheme.themeSource).toBe('dark');
+      });
+    });
     test('initBrowserVersions, successful, should be set to defaultUserAgent', () => {
       // When
       main.init();
@@ -389,21 +408,36 @@ describe('Main module test suite', () => {
       settingsView = {webContents: {destroy: jest.fn()}};
       mockBrowserWindow.getBrowserView = jest.fn(() => settingsView);
     });
-    test('saveSettings, should reload settings and reset all views', () => {
-      // Given
-      mockBrowserWindow.removeBrowserView = jest.fn();
-      mockTabContainer.webContents = {destroy: jest.fn()};
-      main.init();
-      // When
-      mockIpc.listeners.settingsSave({}, {tabs: [{id: 1337}], enabledDictionaries: []});
-      // Then
-      expect(spellCheckModule.loadDictionaries).toHaveBeenCalledTimes(2);
-      expect(settingsModule.updateSettings).toHaveBeenCalledTimes(1);
-      expect(mockBrowserWindow.removeBrowserView).toHaveBeenCalledTimes(1);
-      expect(mockBrowserWindow.removeBrowserView).toHaveBeenCalledWith(settingsView);
-      expect(tabManagerModule.removeAll).toHaveBeenCalledTimes(1);
-      expect(settingsView.webContents.destroy).toHaveBeenCalledTimes(1);
-      expect(mockTabContainer.webContents.destroy).toHaveBeenCalledTimes(1);
+    describe('saveSettings', () => {
+      beforeEach(() => {
+        mockBrowserWindow.removeBrowserView = jest.fn();
+        mockTabContainer.webContents = {destroy: jest.fn()};
+        main.init();
+      });
+      test('should reload settings and reset all views', () => {
+        // When
+        mockIpc.listeners.settingsSave({}, {tabs: [{id: 1337}], enabledDictionaries: []});
+        // Then
+        expect(spellCheckModule.loadDictionaries).toHaveBeenCalledTimes(2);
+        expect(settingsModule.updateSettings).toHaveBeenCalledTimes(1);
+        expect(mockBrowserWindow.removeBrowserView).toHaveBeenCalledTimes(1);
+        expect(mockBrowserWindow.removeBrowserView).toHaveBeenCalledWith(settingsView);
+        expect(tabManagerModule.removeAll).toHaveBeenCalledTimes(1);
+        expect(settingsView.webContents.destroy).toHaveBeenCalledTimes(1);
+        expect(mockTabContainer.webContents.destroy).toHaveBeenCalledTimes(1);
+      });
+      test('should set saved theme', () => {
+        // When
+        mockIpc.listeners.settingsSave({}, {theme: 'light'});
+        // Then
+        expect(mockNativeTheme.themeSource).toEqual('light');
+      });
+      test('should fallback theme to system', () => {
+        // When
+        mockIpc.listeners.settingsSave({}, {});
+        // Then
+        expect(mockNativeTheme.themeSource).toEqual('system');
+      });
     });
     test('closeDialog, should destroy dialog view and activate current tab', () => {
       // Given
diff --git a/src/settings/__tests__/preload.test.js b/src/settings/__tests__/preload.test.js
index e9d72bf..f5fae0e 100644
--- a/src/settings/__tests__/preload.test.js
+++ b/src/settings/__tests__/preload.test.js
@@ -39,12 +39,13 @@ describe('Settings Module preload test suite', () => {
       // Then
       await waitFor(() => expect(document.head.children.length).toBeGreaterThan(0));
       const styles = Array.from(document.querySelectorAll('style'));
-      expect(styles).toHaveLength(8);
+      expect(styles).toHaveLength(9);
       expect(styles[0].innerHTML).toMatch(/:root \{.+--color-accent-fg:/s); // Variables
       expect(styles[1].innerHTML).toContain('html.electronim,'); // Base
       expect(styles[2].innerHTML).toContain('.electronim h1,'); // Typography
-      expect(styles[4].innerHTML).toContain('.electronim .navbar {'); // Components
-      expect(styles[7].innerHTML).toContain('.settings.container {'); // Settings-specific
+      expect(styles[4].innerHTML).toContain('.electronim .control .checkbox {'); // CheckBox
+      expect(styles[5].innerHTML).toContain('.electronim .top-bar.navbar {'); // NavBar
+      expect(styles[8].innerHTML).toContain('.settings.container {'); // Settings-specific
     });
     test('adds required libraries', async () => {
       expect(window.ELECTRONIM_VERSION).toEqual('0.0.0');
@@ -52,7 +53,6 @@ describe('Settings Module preload test suite', () => {
       expect(window.preact).not.toBeUndefined();
       expect(window.preactHooks).not.toBeUndefined();
       expect(window.html).not.toBeUndefined();
-      expect(window.TopBar).not.toBeUndefined();
     });
   });
 });
diff --git a/src/settings/__tests__/settings.browser.mjs b/src/settings/__tests__/settings.browser.mjs
new file mode 100644
index 0000000..bc57963
--- /dev/null
+++ b/src/settings/__tests__/settings.browser.mjs
@@ -0,0 +1,54 @@
+/*
+   Copyright 2022 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+import {jest} from '@jest/globals';
+
+export const ipcRenderer = () => {
+  const mockDictionariesAvailableNative = ['en'];
+  const mockDictionariesAvailable = {
+    en: {name: 'English'},
+    es: {name: 'Spanish'}
+  };
+  const mockDictionariesEnabled = ['en'];
+  const mockCurrentSettings = {
+    disableNotificationsGlobally: false,
+    tabs: [
+      {id: '1', url: 'https://initial-tab.com', sandboxed: true},
+      {id: '2', url: 'https://initial-tab-2.com', disabled: true, disableNotifications: true}
+    ],
+    theme: 'dark'
+  };
+  return {
+    mockDictionariesAvailableNative,
+    mockDictionariesAvailable,
+    mockDictionariesEnabled,
+    mockCurrentSettings,
+    send: jest.fn(),
+    invoke: jest.fn(async channel => {
+      switch (channel) {
+        case 'settingsLoad':
+          return mockCurrentSettings;
+        case 'dictionaryGetAvailableNative':
+          return mockDictionariesAvailableNative;
+        case 'dictionaryGetAvailable':
+          return mockDictionariesAvailable;
+        case 'dictionaryGetEnabled':
+          return mockDictionariesEnabled;
+        default:
+          return {};
+      }
+    })
+  };
+};
diff --git a/src/settings/__tests__/settings.browser.test.mjs b/src/settings/__tests__/settings.browser.test.mjs
index 11c7738..610b8bb 100644
--- a/src/settings/__tests__/settings.browser.test.mjs
+++ b/src/settings/__tests__/settings.browser.test.mjs
@@ -14,63 +14,19 @@
    limitations under the License.
  */
 import {jest} from '@jest/globals';
+import {loadDOM} from '../../__tests__/index.mjs';
+import {ipcRenderer} from './settings.browser.mjs';
 import {fireEvent, waitFor} from '@testing-library/dom';
-import htm from 'htm';
-
-const mockDOM = () => {
-  document.body.innerHTML = '';
-  const $root = document.createElement('div');
-  $root.innerHTML = '<div class="settings container is-fluid"></div>';
-  document.body.append($root);
-};
 
 describe('Settings in Browser test suite', () => {
-  let mockDictionariesAvailableNative;
-  let mockDictionariesAvailable;
-  let mockDictionariesEnabled;
-  let mockCurrentSettings;
   let mockIpcRenderer;
   beforeEach(async () => {
     jest.resetModules();
-    mockDictionariesAvailableNative = ['en'];
-    mockDictionariesAvailable = {
-      en: {name: 'English'},
-      es: {name: 'Spanish'}
-    };
-    mockDictionariesEnabled = ['en'];
-    mockCurrentSettings = {
-      disableNotificationsGlobally: false,
-      tabs: [
-        {id: '1', url: 'https://initial-tab.com', sandboxed: true},
-        {id: '2', url: 'https://initial-tab-2.com', disabled: true, disableNotifications: true}
-      ]
-    };
-    mockIpcRenderer = {
-      send: jest.fn(),
-      invoke: jest.fn(async channel => {
-        switch (channel) {
-          case 'settingsLoad':
-            return mockCurrentSettings;
-          case 'dictionaryGetAvailableNative':
-            return mockDictionariesAvailableNative;
-          case 'dictionaryGetAvailable':
-            return mockDictionariesAvailable;
-          case 'dictionaryGetEnabled':
-            return mockDictionariesEnabled;
-          default:
-            return {};
-        }
-      })
-    };
-    window.preact = await import('preact');
-    window.preactHooks = await import('preact/hooks');
-    window.html = htm.bind(window.preact.h);
-    window.TopBar = (await import('../../components')).default.topBar(window.html);
-    window.APP_EVENTS = (await import('../../constants')).APP_EVENTS;
+    mockIpcRenderer = ipcRenderer();
+    await import('../../../bundles/settings.preload');
     window.ELECTRONIM_VERSION = '1.33.7';
     window.ipcRenderer = mockIpcRenderer;
-    mockDOM();
-    await import('../settings.browser');
+    await loadDOM({meta: import.meta, path: ['..', 'index.html']});
   });
   describe('Main Button events', () => {
     test('Submit should send form data', () => {
@@ -85,7 +41,8 @@ describe('Settings in Browser test suite', () => {
             {id: '2', url: 'https://initial-tab-2.com', disabled: true, disableNotifications: true}
           ],
           enabledDictionaries: ['en'],
-          disableNotificationsGlobally: false
+          disableNotificationsGlobally: false,
+          theme: 'dark'
         });
     });
     test('Cancel should send close dialog event', () => {
diff --git a/src/settings/__tests__/settings.other.browser.test.mjs b/src/settings/__tests__/settings.other.browser.test.mjs
new file mode 100644
index 0000000..0b64156
--- /dev/null
+++ b/src/settings/__tests__/settings.other.browser.test.mjs
@@ -0,0 +1,50 @@
+/*
+   Copyright 2022 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+import {jest} from '@jest/globals';
+import {loadDOM} from '../../__tests__/index.mjs';
+import {findByTestId, getByTestId} from '@testing-library/dom';
+import userEvent from '@testing-library/user-event';
+import {ipcRenderer} from './settings.browser.mjs';
+
+describe('Settings (Other) in Browser test suite', () => {
+  let mockIpcRenderer;
+  let user;
+  beforeEach(async () => {
+    jest.resetModules();
+    mockIpcRenderer = ipcRenderer();
+    await import('../../../bundles/settings.preload');
+    window.ipcRenderer = mockIpcRenderer;
+    await loadDOM({meta: import.meta, path: ['..', 'index.html']});
+    user = userEvent.setup(document);
+  });
+  describe('Theme selection', () => {
+    let $themeContainer;
+    beforeEach(async () => {
+      await findByTestId(document, 'settings-theme-select');
+      $themeContainer = getByTestId(document, 'settings-theme-select');
+    });
+    test('Select shows the current theme', () => {
+      expect($themeContainer.querySelector('select').value).toBe('dark');
+    });
+    test('Different theme can be selected', async () => {
+      await user.selectOptions($themeContainer.querySelector('select'), 'light');
+      await user.click(document.querySelector('.settings__submit'));
+      expect(mockIpcRenderer.send).toHaveBeenCalledWith('settingsSave', expect.objectContaining({
+        theme: 'light'
+      }));
+    });
+  });
+});