diff --git a/packages/core/index.js b/packages/core/index.js
index 599274df4..06781fe61 100644
--- a/packages/core/index.js
+++ b/packages/core/index.js
@@ -51,3 +51,4 @@ export { DelegateMixin } from './src/DelegateMixin.js';
export { DomHelpersMixin } from './src/DomHelpersMixin.js';
export { LionSingleton } from './src/LionSingleton.js';
export { SlotMixin } from './src/SlotMixin.js';
+export { DisabledMixin } from './src/DisabledMixin.js';
diff --git a/packages/core/src/DisabledMixin.js b/packages/core/src/DisabledMixin.js
new file mode 100644
index 000000000..b279695b3
--- /dev/null
+++ b/packages/core/src/DisabledMixin.js
@@ -0,0 +1,64 @@
+import { dedupeMixin } from './dedupeMixin.js';
+
+/**
+ * #DisabledMixin
+ *
+ * @polymerMixin
+ * @mixinFunction
+ */
+export const DisabledMixin = dedupeMixin(
+ superclass =>
+ // eslint-disable-next-line no-shadow
+ class DisabledMixin extends superclass {
+ static get properties() {
+ return {
+ disabled: {
+ type: Boolean,
+ reflect: true,
+ },
+ };
+ }
+
+ constructor() {
+ super();
+ this.__requestedToBeDisabled = false;
+ this.__isUserSettingDisabled = true;
+
+ this.__restoreDisabledTo = false;
+ this.disabled = false;
+ }
+
+ makeRequestToBeDisabled() {
+ if (this.__requestedToBeDisabled === false) {
+ this.__requestedToBeDisabled = true;
+ this.__restoreDisabledTo = this.disabled;
+ this.__internalSetDisabled(true);
+ }
+ }
+
+ retractRequestToBeDisabled() {
+ if (this.__requestedToBeDisabled === true) {
+ this.__requestedToBeDisabled = false;
+ this.__internalSetDisabled(this.__restoreDisabledTo);
+ }
+ }
+
+ __internalSetDisabled(value) {
+ this.__isUserSettingDisabled = false;
+ this.disabled = value;
+ this.__isUserSettingDisabled = true;
+ }
+
+ _requestUpdate(name, oldValue) {
+ super._requestUpdate(name, oldValue);
+ if (name === 'disabled') {
+ if (this.__isUserSettingDisabled) {
+ this.__restoreDisabledTo = this.disabled;
+ }
+ if (this.disabled === false && this.__requestedToBeDisabled === true) {
+ this.__internalSetDisabled(true);
+ }
+ }
+ }
+ },
+);
diff --git a/packages/core/test/DisabledMixin.test.js b/packages/core/test/DisabledMixin.test.js
new file mode 100644
index 000000000..cbd4389d5
--- /dev/null
+++ b/packages/core/test/DisabledMixin.test.js
@@ -0,0 +1,86 @@
+import { expect, fixture, html } from '@open-wc/testing';
+
+import { LitElement } from '../index.js';
+import { DisabledMixin } from '../src/DisabledMixin.js';
+
+describe('DisabledMixin', () => {
+ before(() => {
+ class CanBeDisabled extends DisabledMixin(LitElement) {}
+ customElements.define('can-be-disabled', CanBeDisabled);
+ });
+
+ it('reflects disabled to attribute', async () => {
+ const el = await fixture(html`
+
+ `);
+ expect(el.hasAttribute('disabled')).to.be.false;
+ el.disabled = true;
+ await el.updateComplete;
+ expect(el.hasAttribute('disabled')).to.be.true;
+ });
+
+ it('can be requested to be disabled', async () => {
+ const el = await fixture(html`
+
+ `);
+ el.makeRequestToBeDisabled();
+ expect(el.disabled).to.be.true;
+ await el.updateComplete;
+ expect(el.hasAttribute('disabled')).to.be.true;
+ });
+
+ it('will not allow to become enabled after makeRequestToBeDisabled()', async () => {
+ const el = await fixture(html`
+
+ `);
+ el.makeRequestToBeDisabled();
+ expect(el.disabled).to.be.true;
+
+ el.disabled = false;
+ expect(el.disabled).to.be.true;
+ });
+
+ it('will stay disabled after retractRequestToBeDisabled() if it was disabled before', async () => {
+ const el = await fixture(html`
+
+ `);
+ el.makeRequestToBeDisabled();
+ el.retractRequestToBeDisabled();
+ expect(el.disabled).to.be.true;
+ });
+
+ it('will become enabled after retractRequestToBeDisabled() if it was enabled before', async () => {
+ const el = await fixture(html`
+
+ `);
+ el.makeRequestToBeDisabled();
+ expect(el.disabled).to.be.true;
+ el.retractRequestToBeDisabled();
+ expect(el.disabled).to.be.false;
+ });
+
+ it('may allow multiple calls to makeRequestToBeDisabled()', async () => {
+ const el = await fixture(html`
+
+ `);
+ el.makeRequestToBeDisabled();
+ el.makeRequestToBeDisabled();
+ el.retractRequestToBeDisabled();
+ expect(el.disabled).to.be.false;
+ });
+
+ it('will restore last state after retractRequestToBeDisabled()', async () => {
+ const el = await fixture(html`
+
+ `);
+ el.makeRequestToBeDisabled();
+ el.disabled = true;
+ el.retractRequestToBeDisabled();
+ expect(el.disabled).to.be.true;
+
+ el.makeRequestToBeDisabled();
+ el.disabled = false;
+ el.retractRequestToBeDisabled();
+ expect(el.disabled).to.be.false;
+ });
+});