diff --git a/api/js/etemplate/Et2Button/Et2ButtonToggle.md b/api/js/etemplate/Et2Button/Et2ButtonToggle.md
new file mode 100644
index 0000000000..8daea6504f
--- /dev/null
+++ b/api/js/etemplate/Et2Button/Et2ButtonToggle.md
@@ -0,0 +1,55 @@
+```html:preview
+
+```
+
+:::tip
+
+There are multiple components for dealing with boolean yes / no.
+
+* [ButtonToggle](../et2-button-toggle): This one
+* [Checkbox](../et2-checkbox): Classic checkbox
+* [Switch](../et2-switch): Switch to turn something on or off
+* [SwitchIcon](../et2-switch-icon): Switch between two icons
+
+:::
+
+## Examples
+
+### Variants
+
+Use the variant attribute to set the button’s variant, same as regular buttons
+
+```html:preview
+
+
+
+
+
+
+```
+
+### Color
+
+Buttons are designed to have a uniform appearance, so their color is not inherited. However, you can still customize
+them by setting `--indicator-color`.
+
+```html:preview
+
+
+```
+
+### Custom icon
+
+Use the `icon` property to set the icon used
+
+```html:preview
+
+```
+
+### Custom icons
+
+Use the `onIcon` and `offIcon` properties to customise what is displayed
+
+```html:preview
+
+```
\ No newline at end of file
diff --git a/api/js/etemplate/Et2Button/Et2ButtonToggle.ts b/api/js/etemplate/Et2Button/Et2ButtonToggle.ts
new file mode 100644
index 0000000000..cfc0152340
--- /dev/null
+++ b/api/js/etemplate/Et2Button/Et2ButtonToggle.ts
@@ -0,0 +1,180 @@
+import {Et2SwitchIcon} from "../Et2Switch/Et2SwitchIcon";
+import {customElement} from "lit/decorators/custom-element.js";
+import {css, PropertyValues} from "lit";
+import {property} from "lit/decorators/property.js";
+
+/**
+ * @summary A button to allow turning something on or off, displayed with two images instead of the normal button UI
+ *
+ * @slot - Add an image directly instead of setting the `icon` property
+ * @slot help-text - Text that describes how to use the button. Alternatively, you can use the `help-text` attribute.
+ *
+ * @cssproperty --indicator-color - The color of the selected image
+ */
+@customElement("et2-button-toggle")
+export class Et2ButtonToggle extends Et2SwitchIcon
+{
+ static get styles()
+ {
+ return [
+ ...super.styles,
+ css`
+ slot[name] {
+ display: none;
+ }
+
+ sl-switch {
+ --width: var(--height);
+ }
+
+ sl-switch:not([checked]) slot[name="off"] {
+ color: var(--sl-color-neutral-400);
+ }
+
+ sl-switch[checked] slot[name="on"], sl-switch:not([checked]) slot[name="off"] {
+ display: inline-block;
+ }
+
+ .label {
+ border: var(--sl-input-border-width) solid var(--sl-input-border-color);
+ border-radius: var(--sl-input-border-radius-medium);
+ }
+
+
+ /* Success */
+
+ :host([variant=success]) .label {
+ background-color: var(--sl-color-success-600);
+ border-color: var(--sl-color-success-600);
+ --indicator-color: var(--sl-color-neutral-0);
+ }
+
+ :host([variant=success]) .label:hover {
+ background-color: var(--sl-color-success-500);
+ border-color: var(--sl-color-success-500);
+ --indicator-color: var(--sl-color-neutral-0);
+ }
+
+ /* Neutral */
+
+ :host([variant=neutral]) .label {
+ background-color: var(--sl-color-neutral-600);
+ border-color: var(--sl-color-neutral-600);
+ --indicator-color: var(--sl-color-neutral-0);
+ }
+
+ :host([variant=neutral]) .label:hover {
+ background-color: var(--sl-color-neutral-500);
+ border-color: var(--sl-color-neutral-500);
+ --indicator-color: var(--sl-color-neutral-0);
+ }
+
+ /* Warning */
+
+ :host([variant=warning]) .label {
+ background-color: var(--sl-color-warning-600);
+ border-color: var(--sl-color-warning-600);
+ --indicator-color: var(--sl-color-neutral-0);
+ }
+
+ :host([variant=warning]) .label:hover {
+ background-color: var(--sl-color-warning-500);
+ border-color: var(--sl-color-warning-500);
+ --indicator-color: var(--sl-color-neutral-0);
+ }
+
+ /* Danger */
+
+ :host([variant=danger]) .label {
+ background-color: var(--sl-color-danger-600);
+ border-color: var(--sl-color-danger-600);
+ --indicator-color: var(--sl-color-neutral-0);
+ }
+
+ :host([variant=danger]) .label:hover {
+ background-color: var(--sl-color-danger-500);
+ border-color: var(--sl-color-danger-500);
+ --indicator-color: var(--sl-color-neutral-0);
+ }
+
+ `
+ ]
+ }
+
+ /**
+ * Name of the icon used.
+ * Alternatively, you can add an `et2-image` as a child
+ * @type {string}
+ */
+ @property() icon = "check";
+
+ /**
+ * Specify the icon used when the toggle is off. Defaults to `icon` but dimmed.
+ * @type {string}
+ */
+ @property() offIcon = ""
+
+ /**
+ *
+ * @type {string}
+ */
+ @property() variant = "neutral"
+
+ private mutationObserver : MutationObserver;
+
+ constructor()
+ {
+ super();
+
+ this.handleIconChanged = this.handleIconChanged.bind(this);
+ }
+
+ async connectedCallback()
+ {
+ super.connectedCallback();
+
+ // If a child was added as part of loading, set up 1 child into both on/off slots
+ if(this.children && this.childElementCount == 1 && !this.children[0].hasAttribute("slot"))
+ {
+ this.adoptIcon(this.children[0]);
+ }
+
+ await this.updateComplete;
+
+ this.mutationObserver = new MutationObserver(this.handleIconChanged);
+ this.mutationObserver.observe(this, {subtree: true, childList: true});
+ }
+
+ willUpdate(changedProperties : PropertyValues)
+ {
+ if(changedProperties.has("icon") || this.icon && (!this.onIcon || this.onIcon == "check"))
+ {
+ this.onIcon = this.icon;
+ this.offIcon = this.icon;
+ }
+ }
+
+ // Take a single element and give it the needed slots so it works
+ protected adoptIcon(icon : HTMLElement)
+ {
+ const off = icon.cloneNode();
+ icon.setAttribute("slot", "on");
+ off.setAttribute("slot", "off");
+ this.append(off);
+ }
+
+ // Listen for added icon and adopt it (needs to not have a slot)
+ protected handleIconChanged(mutations : MutationRecord[])
+ {
+ for(const mutation of mutations)
+ {
+ mutation.addedNodes.forEach((n : HTMLElement) =>
+ {
+ if(typeof n.hasAttribute == "function" && !n.hasAttribute("slot"))
+ {
+ this.adoptIcon(n);
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/api/js/etemplate/Et2Button/test/Et2ButtonToggle.test.ts b/api/js/etemplate/Et2Button/test/Et2ButtonToggle.test.ts
new file mode 100644
index 0000000000..0d487b2d1e
--- /dev/null
+++ b/api/js/etemplate/Et2Button/test/Et2ButtonToggle.test.ts
@@ -0,0 +1,76 @@
+/**
+ * Test file for Etemplate webComponent Et2Switch
+ */
+import {assert, expect, fixture, html} from '@open-wc/testing';
+import * as sinon from 'sinon';
+import {Et2ButtonToggle} from "../Et2ButtonToggle";
+
+describe("Toggle button widget", () =>
+{
+ // Reference to component under test
+ let element : Et2ButtonToggle;
+
+
+ // Setup run before each test
+ beforeEach(async() =>
+ {
+ // Create an element to test with, and wait until it's ready
+ element = await fixture(html`
+
+ `);
+
+ // Stub egw()
+ sinon.stub(element, "egw").returns({
+ tooltipUnbind: () => {},
+ // Image always give check mark. Use data URL to avoid having to serve an actual image
+ image: i => "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNS4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkViZW5lXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMzJweCIgaGVpZ2h0PSIzMnB4IiB2aWV3Qm94PSIwIDAgMzIgMzIiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDMyIDMyIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjNjk2OTY5IiBkPSJNNi45NDMsMjguNDUzDQoJYzAuOTA2LDAuNzY1LDIuMDk3LDEuMTI3LDMuMjg2LDEuMTA5YzAuNDMsMC4wMTQsMC44NTItMC4wNjgsMS4yNjUtMC4yMDdjMC42NzktMC4xOCwxLjMyOC0wLjQ1LDEuODY2LTAuOTAyTDI5LjQwMywxNC45DQoJYzEuNzcyLTEuNDk4LDEuNzcyLTMuOTI1LDAtNS40MjJjLTEuNzcyLTEuNDk3LTQuNjQ2LTEuNDk3LTYuNDE4LDBMMTAuMTE5LDIwLjM0OWwtMi4zODktMi40MjRjLTEuNDQtMS40NTctMy43NzItMS40NTctNS4yMTIsMA0KCWMtMS40MzgsMS40Ni0xLjQzOCwzLjgyNSwwLDUuMjgxQzIuNTE4LDIzLjIwNiw1LjQ3NCwyNi45NDcsNi45NDMsMjguNDUzeiIvPg0KPC9zdmc+DQo="
+ });
+ });
+
+ // Make sure it works
+ it('is defined', () =>
+ {
+ assert.instanceOf(element, Et2ButtonToggle);
+ });
+
+ it('has a label', () =>
+ {
+ element.set_label("Label set");
+
+ assert.equal(element.textContent.trim(), "Label set");
+ })
+
+ it("click happens", () =>
+ {
+ // Setup
+ let clickSpy = sinon.spy();
+ element.onclick = clickSpy;
+
+ // Click
+ element.click();
+
+ // Check for once & only once
+ assert(clickSpy.calledOnce, "Click only once");
+ });
+
+
+ it("shows only 'on' icon when on", async() =>
+ {
+ element.value = true;
+ const on = element.shadowRoot.querySelector(".label .on");
+ const off = element.shadowRoot.querySelector(".label .off");
+ expect(on, "Only 'on' icon should be visible").to.be.displayed;
+ // TODO: This takes a really long time, not sure why
+ //expect(off, "Only 'on' icon should be visible").not.to.be.displayed;
+ });
+
+ it("shows only 'off' icon when off", async() =>
+ {
+ element.value = false;
+ const on = element.shadowRoot.querySelector(".label .on");
+ const off = element.shadowRoot.querySelector(".label .off");
+ expect(off, "Only 'off' icon should be visible").to.be.displayed;
+ // TODO: This takes a really long time, not sure why
+ //expect(on, "Only 'off' icon should be visible").not.to.be.displayed;
+ });
+});
\ No newline at end of file
diff --git a/api/js/etemplate/Et2Switch/Et2SwitchIcon.md b/api/js/etemplate/Et2Switch/Et2SwitchIcon.md
new file mode 100644
index 0000000000..d9bbf03c17
--- /dev/null
+++ b/api/js/etemplate/Et2Switch/Et2SwitchIcon.md
@@ -0,0 +1,24 @@
+```html:preview
+
+```
+
+:::tip
+
+There are multiple components for dealing with boolean yes / no.
+
+* Et2SwitchIcon: This one
+* [ButtonToggle](../et2-button-toggle): A button that shows an icon
+* [Checkbox](../et2-checkbox): Classic checkbox
+* [Switch](../et2-switch): Switch to turn something on or off
+
+:::
+
+## Examples
+
+### Custom icons
+
+Use the `onIcon` and `offIcon` properties to customise what is displayed
+
+```html:preview
+
+```
\ No newline at end of file
diff --git a/api/js/etemplate/Et2Switch/Et2SwitchIcon.ts b/api/js/etemplate/Et2Switch/Et2SwitchIcon.ts
new file mode 100644
index 0000000000..2dfb27961a
--- /dev/null
+++ b/api/js/etemplate/Et2Switch/Et2SwitchIcon.ts
@@ -0,0 +1,145 @@
+import {css, html, LitElement, nothing} from "lit";
+import {customElement} from "lit/decorators/custom-element.js";
+import {property} from "lit/decorators/property.js";
+import {classMap} from "lit/directives/class-map.js";
+import {live} from "lit/directives/live.js";
+import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
+import {SlSwitch} from "@shoelace-style/shoelace";
+
+/**
+ * @summary Switch to allow choosing between two options, displayed with two images
+ *
+ * @slot on - Content shown when the switch is on
+ * @slot off - Content shown when the switch is off
+ * @slot help-text - Text that describes how to use the switch. Alternatively, you can use the `help-text` attribute.
+ *
+ * @cssproperty --height - The height of the switch.
+ * @cssproperty --width - The width of the switch.
+ * @cssproperty --indicator-color - The color of the selected image
+ */
+@customElement("et2-switch-icon")
+export class Et2SwitchIcon extends Et2InputWidget(LitElement)
+{
+ static get styles()
+ {
+ return [
+ ...super.styles,
+ css`
+ :host {
+ --indicator-color: var(--sl-color-primary-600);
+ }
+
+ sl-switch {
+ --sl-toggle-size-medium: 2em;
+ }
+
+ ::part(control) {
+ display: none;
+ }
+
+ ::part(label) {
+ width: 100%;
+ height: 100%;
+ }
+
+ .label {
+ display: inline-flex;
+ flex: 1 1 auto;
+ font-size: var(--height);
+ user-select: none;
+ }
+
+ et2-image, ::slotted(:scope > *) {
+ flex: 1 1 50%;
+ font-size: var(--width);
+ }
+
+ slot {
+ color: var(--sl-color-neutral-400);
+ }
+
+ sl-switch[checked] slot[name="on"], sl-switch:not([checked]) slot[name="off"] {
+ color: var(--indicator-color, inherit);
+ }
+
+ .label:hover {
+ background-color: var(--sl-color-primary-50);
+ border-color: var(--sl-color-primary-300);
+ }
+ `
+ ]
+ }
+
+ /**
+ * Name of the icon displayed when the switch is on
+ * @type {string}
+ */
+ @property() onIcon = "check";
+
+ /**
+ * Name of the icon displayed when the switch is off
+ * @type {string}
+ */
+ @property() offIcon = "x"
+
+ private get switch() : SlSwitch { return this.shadowRoot?.querySelector("sl-switch")};
+
+ private get input() { return this.switch.shadowRoot.querySelector("input");}
+
+ async getUpdateComplete()
+ {
+ const result = await super.getUpdateComplete();
+ await this.switch?.updateComplete;
+ return result;
+ }
+
+ set value(new_value : string | boolean)
+ {
+ this.switch.checked = !!new_value;
+ }
+
+ get value()
+ {
+ return this.switch?.checked;
+ }
+
+ labelTemplate()
+ {
+ return html`
+ ${this.label ? html`${this.label}
+ ` : nothing}
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ render()
+ {
+ return html`
+
+ ${this.labelTemplate()}
+
+ `;
+ }
+}
\ No newline at end of file
diff --git a/api/js/etemplate/Et2Switch/test/Et2SwitchIcon.test.ts b/api/js/etemplate/Et2Switch/test/Et2SwitchIcon.test.ts
new file mode 100644
index 0000000000..909a311192
--- /dev/null
+++ b/api/js/etemplate/Et2Switch/test/Et2SwitchIcon.test.ts
@@ -0,0 +1,71 @@
+/**
+ * Test file for Etemplate webComponent Et2Switch
+ */
+import {assert, expect, fixture, html} from '@open-wc/testing';
+import * as sinon from 'sinon';
+import {Et2SwitchIcon} from "../Et2SwitchIcon";
+
+describe("Switch icon widget", () =>
+{
+ // Reference to component under test
+ let element : Et2SwitchIcon;
+
+
+ // Setup run before each test
+ beforeEach(async() =>
+ {
+ // Create an element to test with, and wait until it's ready
+ element = await fixture(html`
+
+ `);
+
+ // Stub egw()
+ sinon.stub(element, "egw").returns({
+ tooltipUnbind: () => {},
+ // Image always give check mark. Use data URL to avoid having to serve an actual image
+ image: i => "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNS4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkViZW5lXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMzJweCIgaGVpZ2h0PSIzMnB4IiB2aWV3Qm94PSIwIDAgMzIgMzIiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDMyIDMyIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjNjk2OTY5IiBkPSJNNi45NDMsMjguNDUzDQoJYzAuOTA2LDAuNzY1LDIuMDk3LDEuMTI3LDMuMjg2LDEuMTA5YzAuNDMsMC4wMTQsMC44NTItMC4wNjgsMS4yNjUtMC4yMDdjMC42NzktMC4xOCwxLjMyOC0wLjQ1LDEuODY2LTAuOTAyTDI5LjQwMywxNC45DQoJYzEuNzcyLTEuNDk4LDEuNzcyLTMuOTI1LDAtNS40MjJjLTEuNzcyLTEuNDk3LTQuNjQ2LTEuNDk3LTYuNDE4LDBMMTAuMTE5LDIwLjM0OWwtMi4zODktMi40MjRjLTEuNDQtMS40NTctMy43NzItMS40NTctNS4yMTIsMA0KCWMtMS40MzgsMS40Ni0xLjQzOCwzLjgyNSwwLDUuMjgxQzIuNTE4LDIzLjIwNiw1LjQ3NCwyNi45NDcsNi45NDMsMjguNDUzeiIvPg0KPC9zdmc+DQo="
+ });
+ });
+
+ // Make sure it works
+ it('is defined', () =>
+ {
+ assert.instanceOf(element, Et2SwitchIcon);
+ });
+
+ it('has a label', () =>
+ {
+ element.set_label("Label set");
+
+ assert.equal(element.textContent.trim(), "Label set");
+ })
+
+ it("click happens", () =>
+ {
+ // Setup
+ let clickSpy = sinon.spy();
+ element.onclick = clickSpy;
+
+ // Click
+ element.click();
+
+ // Check for once & only once
+ assert(clickSpy.calledOnce, "Click only once");
+ });
+
+ it("shows 'on' icon", async() =>
+ {
+ element.onIcon = "plus";
+ await element.updateComplete;
+ const label = element.shadowRoot.querySelector(".label .on");
+ expect(label).to.be.displayed;
+ });
+ it("shows 'off' icon", async() =>
+ {
+ element.offIcon = "minus";
+ await element.updateComplete;
+ const label = element.shadowRoot.querySelector(".label .off");
+
+ expect(label).to.be.displayed;
+ });
+});
\ No newline at end of file
diff --git a/api/js/etemplate/etemplate2.ts b/api/js/etemplate/etemplate2.ts
index dfbdd4aed6..e06be19314 100644
--- a/api/js/etemplate/etemplate2.ts
+++ b/api/js/etemplate/etemplate2.ts
@@ -36,6 +36,7 @@ import './Et2Button/Et2ButtonCopy';
import './Et2Button/Et2ButtonIcon';
import './Et2Button/Et2ButtonScroll';
import './Et2Button/Et2ButtonTimestamper';
+import './Et2Button/Et2ButtonToggle';
import './Et2Checkbox/Et2Checkbox';
import './Et2Checkbox/Et2CheckboxReadonly';
import './Et2Date/Et2Date';
@@ -84,6 +85,7 @@ import './Et2Select/Tag/Et2EmailTag';
import './Et2Select/Tag/Et2ThumbnailTag';
import './Et2Spinner/Et2Spinner';
import './Et2Switch/Et2Switch';
+import './Et2Switch/Et2SwitchIcon';
import './Et2Textarea/Et2Textarea';
import './Et2Textarea/Et2TextareaReadonly';
import './Et2Textbox/Et2Textbox';