diff --git a/src/diff/index.js b/src/diff/index.js
index 242bf84581..ff03e5f954 100644
--- a/src/diff/index.js
+++ b/src/diff/index.js
@@ -332,6 +332,8 @@ function diffElementNodes(
 	let oldProps = oldVNode.props;
 	let newProps = newVNode.props;
 	let nodeType = newVNode.type;
+	let isCustomElement = ('' + newVNode.type).indexOf('-') > -1;
+
 	let i = 0;
 
 	// Tracks entering and exiting SVG namespace when descending through the tree.
@@ -420,7 +422,7 @@ function diffElementNodes(
 			}
 		}
 
-		diffProps(dom, newProps, oldProps, isSvg, isHydrating);
+		diffProps(dom, newProps, oldProps, isSvg, isHydrating, isCustomElement);
 
 		// If the new vnode didn't have dangerouslySetInnerHTML, diff its children
 		if (newHtml) {
diff --git a/src/diff/props.js b/src/diff/props.js
index df5710925a..ed5b8cb75e 100644
--- a/src/diff/props.js
+++ b/src/diff/props.js
@@ -9,8 +9,16 @@ import options from '../options';
  * @param {object} oldProps The old props
  * @param {boolean} isSvg Whether or not this node is an SVG node
  * @param {boolean} hydrate Whether or not we are in hydration mode
+ * @param {boolean} isCustomElement Whether or not we are diffing a custom element.
  */
-export function diffProps(dom, newProps, oldProps, isSvg, hydrate) {
+export function diffProps(
+	dom,
+	newProps,
+	oldProps,
+	isSvg,
+	hydrate,
+	isCustomElement
+) {
 	let i;
 
 	for (i in oldProps) {
@@ -24,8 +32,7 @@ export function diffProps(dom, newProps, oldProps, isSvg, hydrate) {
 			(!hydrate || typeof newProps[i] == 'function') &&
 			i !== 'children' &&
 			i !== 'key' &&
-			i !== 'value' &&
-			i !== 'checked' &&
+			(isCustomElement || (i !== 'value' && i !== 'checked')) &&
 			oldProps[i] !== newProps[i]
 		) {
 			setProperty(dom, i, newProps[i], oldProps[i], isSvg);
diff --git a/test/browser/render.test.js b/test/browser/render.test.js
index bd084a40b6..d8f2f5a733 100644
--- a/test/browser/render.test.js
+++ b/test/browser/render.test.js
@@ -489,6 +489,59 @@ describe('render()', () => {
 		expect(scratch.innerHTML).to.equal('');
 	});
 
+	describe('Custom elements properties', () => {
+		// Properties set to null/undefined are cleared in the DOM through an empty string.
+		const clearedPropertyValue = '';
+
+		class TestComponent extends HTMLElement {
+			constructor() {
+				super();
+				this.value = {};
+				this.checked = undefined;
+			}
+		}
+		customElements.define('test-component', TestComponent);
+
+		it(`should set complex value property`, () => {
+			const value = { foo: 'bar' };
+			render(, scratch);
+
+			expect(scratch.querySelector('test-component').value).to.equal(value);
+		});
+
+		it(`should set checked property`, () => {
+			let initialValue = true;
+			render(, scratch);
+			expect(scratch.querySelector('test-component').checked).to.equal(true);
+		});
+
+		clearPropertyWith(null);
+		clearPropertyWith(undefined);
+
+		function clearPropertyWith(clearValue) {
+			it(`should clear existing value property with ${clearValue}`, () => {
+				render(, scratch);
+
+				render(, scratch);
+
+				expect(scratch.querySelector('test-component').value).to.equal(
+					clearedPropertyValue
+				);
+			});
+
+			it(`should clear existing checked property with ${clearValue}`, () => {
+				let initialValue = true;
+				render(, scratch);
+
+				render(, scratch);
+
+				expect(scratch.querySelector('test-component').checked).to.equal(
+					clearedPropertyValue
+				);
+			});
+		}
+	});
+
 	it('should mask value on password input elements', () => {
 		render(, scratch);
 		expect(scratch.innerHTML).to.equal('');