diff --git a/build/fragments/source-references.js b/build/fragments/source-references.js
index ac7547da1..d1203ed88 100644
--- a/build/fragments/source-references.js
+++ b/build/fragments/source-references.js
@@ -20,6 +20,7 @@ knockoutDebugCallback([
     'src/virtualElements.js',
     'src/components/loaderRegistry.js',
     'src/components/defaultLoader.js',
+    'src/components/customElements.js',
     'src/binding/bindingProvider.js',
     'src/binding/bindingAttributeSyntax.js',
     'src/components/componentBinding.js',
diff --git a/spec/components/customElementBehaviors.js b/spec/components/customElementBehaviors.js
new file mode 100644
index 000000000..a03bae1d4
--- /dev/null
+++ b/spec/components/customElementBehaviors.js
@@ -0,0 +1,87 @@
+describe('Components: Custom elements', function() {
+    beforeEach(function() {
+        jasmine.prepareTestNode();
+        jasmine.Clock.useMock();
+    });
+
+    afterEach(function() {
+        jasmine.Clock.reset();
+        ko.components.unregister('test-component');
+    });
+
+    it('Inserts components into custom elements with matching names', function() {
+        ko.components.register('test-component', {
+            template: 'custom element <span data-bind="text: 123"></span>'
+        });
+        var initialMarkup = '<div>hello <test-component></test-component></div>';
+        testNode.innerHTML = initialMarkup;
+
+        // Since components are loaded asynchronously, it doesn't show up synchronously
+        ko.applyBindings(null, testNode);
+        expect(testNode).toContainHtml(initialMarkup);
+
+        // ... but when the component is loaded, it does show up
+        jasmine.Clock.tick(1);
+        expect(testNode).toContainHtml('<div>hello <test-component>custom element <span data-bind="text: 123">123</span></test-component></div>');
+    });
+
+    it('Is possible to override getComponentNameForNode to determine which component goes into which element', function() {
+        ko.components.register('test-component', { template: 'custom element'});
+        this.restoreAfter(ko.components, 'getComponentNameForNode');
+
+        // Set up a getComponentNameForNode function that maps "A" tags to test-component
+        testNode.innerHTML = '<div>hello <a></a> <b>ignored</b></div>';
+        ko.components.getComponentNameForNode = function(node) {
+            return node.tagName === 'A' ? 'test-component' : null;
+        }
+
+        // See the component show up
+        ko.applyBindings(null, testNode);
+        jasmine.Clock.tick(1);
+        expect(testNode).toContainHtml('<div>hello <a>custom element</a> <b>ignored</b></div>');
+    });
+
+    it('Is possible to have regular data-bind bindings on a custom element, as long as they don\'t attempt to control descendants', function() {
+        ko.components.register('test-component', { template: 'custom element'});
+        testNode.innerHTML = '<test-component data-bind="visible: shouldshow"></test-component>';
+
+        // Bind with a viewmodel that controls visibility
+        var viewModel = { shouldshow: ko.observable(true) };
+        ko.applyBindings(viewModel, testNode);
+        jasmine.Clock.tick(1);
+        expect(testNode).toContainHtml('<test-component data-bind="visible: shouldshow">custom element</test-component>');
+        expect(testNode.childNodes[0].style.display).not.toBe('none');
+
+        // See that the 'visible' binding still works
+        viewModel.shouldshow(false);
+        expect(testNode.childNodes[0].style.display).toBe('none');
+        expect(testNode.childNodes[0].innerHTML).toBe('custom element');
+    });
+
+    it('Is not possible to have regular data-bind bindings on a custom element if they also attempt to control descendants', function() {
+        ko.components.register('test-component', { template: 'custom element'});
+        testNode.innerHTML = '<test-component data-bind="if: true"></test-component>';
+
+        expect(function() { ko.applyBindings(null, testNode); })
+            .toThrowContaining('Multiple bindings (if and component) are trying to control descendant bindings of the same element.');
+    });
+
+    it('Is possible to call applyBindings directly on a custom element', function() {
+        ko.components.register('test-component', { template: 'custom element'});
+        testNode.innerHTML = '<test-component></test-component>';
+        var customElem = testNode.childNodes[0];
+        expect(customElem.tagName).toBe('TEST-COMPONENT');
+        
+        ko.applyBindings(null, customElem);
+        jasmine.Clock.tick(1);
+        expect(customElem.innerHTML).toBe('custom element');
+    });
+
+    it('Throws if you try to duplicate the \'component\' binding on a custom element that matches a component', function() {
+        ko.components.register('test-component', { template: 'custom element'});
+        testNode.innerHTML = '<test-component data-bind="component: {}"></test-component>';
+
+        expect(function() { ko.applyBindings(null, testNode); })
+            .toThrowContaining('Cannot use the "component" binding on a custom element matching a component');
+    });
+});
diff --git a/spec/runner.html b/spec/runner.html
index 6f585af09..8a41a8012 100755
--- a/spec/runner.html
+++ b/spec/runner.html
@@ -56,6 +56,7 @@
         <script type="text/javascript" src="components/loaderRegistryBehaviors.js"></script>
         <script type="text/javascript" src="components/defaultLoaderBehaviors.js"></script>
         <script type="text/javascript" src="components/componentBindingBehaviors.js"></script>
+        <script type="text/javascript" src="components/customElementBehaviors.js"></script>
 
         <!-- Default bindings -->
         <script type="text/javascript" src="defaultBindings/attrBehaviors.js"></script>
diff --git a/src/binding/bindingProvider.js b/src/binding/bindingProvider.js
index fe5f71714..537ba256f 100644
--- a/src/binding/bindingProvider.js
+++ b/src/binding/bindingProvider.js
@@ -8,20 +8,25 @@
     ko.utils.extend(ko.bindingProvider.prototype, {
         'nodeHasBindings': function(node) {
             switch (node.nodeType) {
-                case 1: return node.getAttribute(defaultBindingAttributeName) != null;   // Element
-                case 8: return ko.virtualElements.hasBindingValue(node); // Comment node
+                case 1: // Element
+                    return node.getAttribute(defaultBindingAttributeName) != null
+                        || ko.components['getComponentNameForNode'](node);
+                case 8: // Comment node
+                    return ko.virtualElements.hasBindingValue(node);
                 default: return false;
             }
         },
 
         'getBindings': function(node, bindingContext) {
-            var bindingsString = this['getBindingsString'](node, bindingContext);
-            return bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node) : null;
+            var bindingsString = this['getBindingsString'](node, bindingContext),
+                parsedBindings = bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node) : null;
+            return ko.components.addBindingsForCustomElement(parsedBindings, node, bindingContext, /* valueAccessors */ false);
         },
 
         'getBindingAccessors': function(node, bindingContext) {
-            var bindingsString = this['getBindingsString'](node, bindingContext);
-            return bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node, {'valueAccessors':true}) : null;
+            var bindingsString = this['getBindingsString'](node, bindingContext),
+                parsedBindings = bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node, { 'valueAccessors': true }) : null;
+            return ko.components.addBindingsForCustomElement(parsedBindings, node, bindingContext, /* valueAccessors */ true);
         },
 
         // The following function is only used internally by this default provider.
diff --git a/src/components/customElements.js b/src/components/customElements.js
new file mode 100644
index 000000000..2f88bebe7
--- /dev/null
+++ b/src/components/customElements.js
@@ -0,0 +1,32 @@
+(function (undefined) {
+    // Overridable API for determining which component name applies to a given node. By overriding this,
+    // you can for example map specific tagNames to components that are not preregistered.
+    ko.components['getComponentNameForNode'] = function(node) {
+        var tagNameLower = ko.utils.tagNameLower(node);
+        return ko.components.isRegistered(tagNameLower) && tagNameLower;
+    };
+
+    ko.components.addBindingsForCustomElement = function(allBindings, node, bindingContext, valueAccessors) {
+        // Determine if it's really a custom element matching a component
+        if (node.nodeType === 1) {
+            var componentName = ko.components['getComponentNameForNode'](node);
+            if (componentName) {
+                // It does represent a component, so add a component binding for it
+                allBindings = allBindings || {};
+
+                if (allBindings['component']) {
+                    // Avoid silently overwriting some other 'component' binding that may already be on the element
+                    throw new Error('Cannot use the "component" binding on a custom element matching a component');
+                }
+
+                var componentBindingValue = { 'name': componentName };
+
+                allBindings['component'] = valueAccessors
+                    ? function() { return componentBindingValue; }
+                    : componentBindingValue;
+            }
+        }
+
+        return allBindings;
+    }
+})();
\ No newline at end of file