diff --git a/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml b/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml
index 19b4002efb2a71289961b139cf41517c0c78e8ef..c8842b5b5be3a31f206e4cd9d98e0938a5342c2c 100644
--- a/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml
+++ b/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml
@@ -10,15 +10,19 @@
 ?>
 <?php $swatchData = $block->getSwatchData(); ?>
 <div class="swatch-attribute swatch-layered <?php /* @escapeNotVerified */ echo $swatchData['attribute_code'] ?>"
-     attribute-code="<?php /* @escapeNotVerified */ echo $swatchData['attribute_code'] ?>" attribute-id="<?php /* @escapeNotVerified */ echo $swatchData['attribute_id'] ?>">
+     attribute-code="<?php /* @escapeNotVerified */ echo $swatchData['attribute_code'] ?>"
+     attribute-id="<?php /* @escapeNotVerified */ echo $swatchData['attribute_id'] ?>">
     <div class="swatch-attribute-options clearfix">
         <?php foreach ($swatchData['options'] as $option => $label): ?>
-            <a href="<?php /* @escapeNotVerified */ echo $label['link'] ?>" class="swatch-option-link-layered">
+            <a href="<?php /* @escapeNotVerified */ echo $label['link'] ?>"
+               aria-label="<?php /* @escapeNotVerified */ echo $label['label'] ?>"
+               class="swatch-option-link-layered">
                 <?php if (isset($swatchData['swatches'][$option]['type'])) { ?>
                     <?php switch ($swatchData['swatches'][$option]['type']) {
                         case '3':
                             ?>
                             <div class="swatch-option <?php /* @escapeNotVerified */ echo $label['custom_style'] ?>"
+                                 tabindex="-1"
                                  option-type="3"
                                  option-id="<?php /* @escapeNotVerified */ echo $option ?>"
                                  option-label="<?php /* @escapeNotVerified */ echo $label['label'] ?>"
@@ -33,6 +37,7 @@
                             <?php $swatchImagePath = $block->getSwatchPath('swatch_image',
                             $swatchData['swatches'][$option]['value']); ?>
                             <div class="swatch-option image <?php /* @escapeNotVerified */ echo $label['custom_style'] ?>"
+                                 tabindex="-1"
                                  option-type="2"
                                  option-id="<?php /* @escapeNotVerified */ echo $option ?>"
                                  option-label="<?php /* @escapeNotVerified */ echo $label['label'] ?>"
@@ -43,6 +48,7 @@
                         case '1':
                             ?>
                             <div class="swatch-option color <?php /* @escapeNotVerified */ echo $label['custom_style'] ?>"
+                                 tabindex="-1"
                                  option-type="1"
                                  option-id="<?php /* @escapeNotVerified */ echo $option ?>"
                                  option-label="<?php /* @escapeNotVerified */ echo $label['label'] ?>"
@@ -54,6 +60,7 @@
                         default:
                             ?>
                                 <div class="swatch-option text <?php /* @escapeNotVerified */ echo $label['custom_style'] ?>"
+                                     tabindex="-1"
                                      option-type="0"
                                      option-id="<?php /* @escapeNotVerified */ echo $option ?>"
                                      option-label="<?php /* @escapeNotVerified */ echo $label['label'] ?>"
diff --git a/app/code/Magento/Swatches/view/frontend/web/css/swatches.css b/app/code/Magento/Swatches/view/frontend/web/css/swatches.css
index 8204fc7fc8f630f64ace7caa37567550354f62cf..3899a663788b0f308ce5bf4c7b0f8da4389c4245 100644
--- a/app/code/Magento/Swatches/view/frontend/web/css/swatches.css
+++ b/app/code/Magento/Swatches/view/frontend/web/css/swatches.css
@@ -210,6 +210,10 @@
     padding: 0 !important;
 }
 
+.swatch-option-link-layered:focus > div {
+    box-shadow: 0 0 3px 1px #68a8e0;
+}
+
 .swatch-option-tooltip-layered {
     width: 140px;
     position: absolute;
@@ -276,3 +280,9 @@
 .swatch-option-loading {
     content: url("../images/loader-2.gif");
 }
+
+.swatch-input {
+    left: -1000px;
+    position: absolute;
+    visibility: hidden;
+}
diff --git a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js
index dfd4b2bb2da0ffcc19286dafd125be59d980f804..4e79c42dd6adbf4eb1f079f569ac3aeb77799241 100644
--- a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js
+++ b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js
@@ -6,11 +6,57 @@
 define([
     'jquery',
     'underscore',
+    'mage/smart-keyboard-handler',
     'jquery/ui',
-    'jquery/jquery.parsequery'
-], function ($, _) {
+    'jquery/jquery.parsequery',
+    'mage/validation/validation'
+], function ($, _, keyboardHandler) {
     'use strict';
 
+    /**
+     * Extend form validation to support swatch accessibility
+     */
+    $.widget('mage.validation', $.mage.validation, {
+        /**
+         * Handle form with swatches validation. Focus on first invalid swatch block.
+         *
+         * @param {jQuery.Event} event
+         * @param {Object} validation
+         */
+        listenFormValidateHandler: function (event, validation) {
+            var swatchWrapper, firstActive, swatches, swatch, successList, errorList, firstSwatch;
+
+            this._superApply(arguments);
+
+            swatchWrapper = '.swatch-attribute-options';
+            swatches = $(event.target).find(swatchWrapper);
+
+            if (!swatches.length) {
+                return;
+            }
+
+            swatch = '.swatch-attribute';
+            firstActive = $(validation.errorList[0].element || []);
+            successList = validation.successList;
+            errorList = validation.errorList;
+            firstSwatch = $(firstActive).parent(swatch).find(swatchWrapper);
+
+            keyboardHandler.focus(swatches);
+
+            $.each(successList, function (index, item) {
+                $(item).parent(swatch).find(swatchWrapper).attr('aria-invalid', false);
+            });
+
+            $.each(errorList, function (index, item) {
+                $(item.element).parent(swatch).find(swatchWrapper).attr('aria-invalid', true);
+            });
+
+            if (firstSwatch.length) {
+                $(firstSwatch).focus();
+            }
+        }
+    });
+
     /**
      * Render tooltips by attributes (only to up).
      * Required element attributes:
@@ -117,6 +163,7 @@ define([
                 $element.hide();
                 clearTimeout(timer);
             });
+
             $(document).on('tap', function () {
                 $element.hide();
                 clearTimeout(timer);
@@ -169,6 +216,9 @@ define([
             //selector of product images gallery wrapper
             mediaGallerySelector: '[data-gallery-role=gallery-placeholder]',
 
+            // selector of category product tile wrapper
+            selectorProductTile: '.product-item',
+
             // number of controls to show (false or zero = show all)
             numberToShow: false,
 
@@ -178,6 +228,9 @@ define([
             // enable label for control
             enableControlLabel: true,
 
+            // control label id
+            controlLabelId: '',
+
             // text for more button
             moreButtonText: 'More',
 
@@ -191,7 +244,10 @@ define([
             mediaGalleryInitial: [{}],
 
             //
-            onlyMainImg: false
+            onlyMainImg: false,
+
+            // whether swatches are rendered in product list or on product page
+            inProductList: false
         },
 
         /**
@@ -246,7 +302,9 @@ define([
                     'img': $main.find('.product-image-photo').attr('src')
                 }];
             }
-            this.productForm = this.element.parents(this.options.selectorProduct).find('form:first');
+
+            this.productForm = this.element.parents(this.options.selectorProductTile).find('form:first');
+            this.inProductList = this.productForm.length > 0;
         },
 
         /**
@@ -264,9 +322,11 @@ define([
 
             $.each(this.options.jsonConfig.attributes, function () {
                 var item = this,
-                    options = $widget._RenderSwatchOptions(item),
+                    controlLabelId = 'option-label-' + item.code + '-' + item.id,
+                    options = $widget._RenderSwatchOptions(item, controlLabelId),
                     select = $widget._RenderSwatchSelect(item, chooseText),
                     input = $widget._RenderFormInput(item),
+                    listLabel = '',
                     label = '';
 
                 // Show only swatch controls
@@ -276,22 +336,32 @@ define([
 
                 if ($widget.options.enableControlLabel) {
                     label +=
-                        '<span class="' + classes.attributeLabelClass + '">' + item.label + '</span>' +
+                        '<span id="' + controlLabelId + '" class="' + classes.attributeLabelClass + '">' +
+                            item.label +
+                        '</span>' +
                         '<span class="' + classes.attributeSelectedOptionLabelClass + '"></span>';
                 }
 
-                if ($widget.productForm) {
+                if ($widget.inProductList) {
                     $widget.productForm.append(input);
                     input = '';
+                    listLabel = 'aria-label="' + item.label + '"';
+                } else {
+                    listLabel = 'aria-labelledby="' + controlLabelId + '"';
                 }
 
                 // Create new control
                 container.append(
-                    '<div class="' + classes.attributeClass + ' ' + item.code +
-                        '" attribute-code="' + item.code +
-                        '" attribute-id="' + item.id + '">' +
-                            label +
-                        '<div class="' + classes.attributeOptionsWrapper + ' clearfix">' +
+                    '<div class="' + classes.attributeClass + ' ' + item.code + '" ' +
+                         'attribute-code="' + item.code + '" ' +
+                         'attribute-id="' + item.id + '">' +
+                        label +
+                        '<div aria-activedescendant="" ' +
+                             'tabindex="0" ' +
+                             'aria-invalid="false" ' +
+                             'aria-required="true" ' +
+                             'role="listbox" ' + listLabel +
+                             'class="' + classes.attributeOptionsWrapper + ' clearfix">' +
                             options + select +
                         '</div>' + input +
                     '</div>'
@@ -336,10 +406,11 @@ define([
          * Render swatch options by part of config
          *
          * @param {Object} config
+         * @param {String} controlId
          * @returns {String}
          * @private
          */
-        _RenderSwatchOptions: function (config) {
+        _RenderSwatchOptions: function (config, controlId) {
             var optionConfig = this.options.jsonSwatchConfig[config.id],
                 optionClass = this.options.classes.optionClass,
                 moreLimit = parseInt(this.options.numberToShow, 10),
@@ -375,11 +446,17 @@ define([
                 thumb = optionConfig[id].hasOwnProperty('thumb') ? optionConfig[id].thumb : '';
                 label = this.label ? this.label : '';
                 attr =
+                    ' id="' + controlId + '-item-' + id + '"' +
+                    ' aria-checked="false"' +
+                    ' aria-describedby="' + controlId + '"' +
+                    ' tabindex="0"' +
                     ' option-type="' + type + '"' +
                     ' option-id="' + id + '"' +
                     ' option-label="' + label + '"' +
+                    ' aria-label="' + label + '"' +
                     ' option-tooltip-thumb="' + thumb + '"' +
-                    ' option-tooltip-value="' + value + '"';
+                    ' option-tooltip-value="' + value + '"' +
+                    ' role="option"';
 
                 if (!this.hasOwnProperty('products') || this.products.length <= 0) {
                     attr += ' option-empty="true"';
@@ -392,19 +469,19 @@ define([
                 } else if (type === 1) {
                     // Color
                     html += '<div class="' + optionClass + ' color" ' + attr +
-                        '" style="background: ' + value +
+                        ' style="background: ' + value +
                         ' no-repeat center; background-size: initial;">' + '' +
                         '</div>';
                 } else if (type === 2) {
                     // Image
                     html += '<div class="' + optionClass + ' image" ' + attr +
-                        '" style="background: url(' + value + ') no-repeat center; background-size: initial;">' + '' +
+                        ' style="background: url(' + value + ') no-repeat center; background-size: initial;">' + '' +
                         '</div>';
                 } else if (type === 3) {
                     // Clear
                     html += '<div class="' + optionClass + '" ' + attr + '></div>';
                 } else {
-                    // Defaualt
+                    // Default
                     html += '<div class="' + optionClass + '" ' + attr + '>' + label + '</div>';
                 }
             });
@@ -460,10 +537,9 @@ define([
                 'type="text" ' +
                 'value="" ' +
                 'data-selector="super_attribute[' + config.id + ']" ' +
-                'data-validate="{required:true}" ' +
+                'data-validate="{required: true}" ' +
                 'aria-required="true" ' +
-                'aria-invalid="true" ' +
-                'style="visibility: hidden; position:absolute; left:-1000px">';
+                'aria-invalid="false">';
         },
 
         /**
@@ -472,22 +548,39 @@ define([
          * @private
          */
         _EventListener: function () {
+            var $widget = this,
+                options = this.options.classes,
+                target;
 
-            var $widget = this;
-
-            $widget.element.on('click', '.' + this.options.classes.optionClass, function () {
+            $widget.element.on('click', '.' + options.optionClass, function () {
                 return $widget._OnClick($(this), $widget);
             });
 
-            $widget.element.on('change', '.' + this.options.classes.selectClass, function () {
+            $widget.element.on('change', '.' + options.selectClass, function () {
                 return $widget._OnChange($(this), $widget);
             });
 
-            $widget.element.on('click', '.' + this.options.classes.moreButton, function (e) {
+            $widget.element.on('click', '.' + options.moreButton, function (e) {
                 e.preventDefault();
 
                 return $widget._OnMoreClick($(this));
             });
+
+            $widget.element.on('keydown', function (e) {
+                if (e.which === 13) {
+                    target = $(e.target);
+
+                    if (target.is('.' + options.optionClass)) {
+                        return $widget._OnClick(target, $widget);
+                    } else if (target.is('.' + options.selectClass)) {
+                        return $widget._OnChange(target, $widget);
+                    } else if (target.is('.' + options.moreButton)) {
+                        e.preventDefault();
+
+                        return $widget._OnMoreClick(target);
+                    }
+                }
+            });
         },
 
         /**
@@ -498,13 +591,17 @@ define([
          * @private
          */
         _OnClick: function ($this, $widget) {
-
             var $parent = $this.parents('.' + $widget.options.classes.attributeClass),
+                $wrapper = $this.parents('.' + $widget.options.classes.attributeOptionsWrapper),
                 $label = $parent.find('.' + $widget.options.classes.attributeSelectedOptionLabelClass),
                 attributeId = $parent.attr('attribute-id'),
+                $input = $parent.find('.' + $widget.options.classes.attributeInput);
+
+            if ($widget.inProductList) {
                 $input = $widget.productForm.find(
                     '.' + $widget.options.classes.attributeInput + '[name="super_attribute[' + attributeId + ']"]'
                 );
+            }
 
             if ($this.hasClass('disabled')) {
                 return;
@@ -514,11 +611,13 @@ define([
                 $parent.removeAttr('option-selected').find('.selected').removeClass('selected');
                 $input.val('');
                 $label.text('');
+                $this.attr('aria-checked', false);
             } else {
                 $parent.attr('option-selected', $this.attr('option-id')).find('.selected').removeClass('selected');
                 $label.text($this.attr('option-label'));
                 $input.val($this.attr('option-id'));
                 $this.addClass('selected');
+                $widget._toggleCheckedAttributes($this, $wrapper);
             }
 
             $widget._Rebuild();
@@ -533,6 +632,19 @@ define([
             $input.trigger('change');
         },
 
+        /**
+         * Toggle accessibility attributes
+         *
+         * @param {Object} $this
+         * @param {Object} $wrapper
+         * @private
+         */
+        _toggleCheckedAttributes: function ($this, $wrapper) {
+            $wrapper.attr('aria-activedescendant', $this.attr('id'))
+                    .find('.' + this.options.classes.optionClass).attr('aria-checked', false);
+            $this.attr('aria-checked', true);
+        },
+
         /**
          * Event for select
          *
@@ -543,9 +655,13 @@ define([
         _OnChange: function ($this, $widget) {
             var $parent = $this.parents('.' + $widget.options.classes.attributeClass),
                 attributeId = $parent.attr('attribute-id'),
+                $input = $parent.find('.' + $widget.options.classes.attributeInput);
+
+            if ($widget.productForm.length > 0) {
                 $input = $widget.productForm.find(
                     '.' + $widget.options.classes.attributeInput + '[name="super_attribute[' + attributeId + ']"]'
                 );
+            }
 
             if ($this.val() > 0) {
                 $parent.attr('option-selected', $this.val());
@@ -687,7 +803,6 @@ define([
                     'prices': $widget._getPrices(result, $productPrice.priceBox('option').prices)
                 }
             );
-
         },
 
         /**
diff --git a/lib/web/mage/smart-keyboard-handler.js b/lib/web/mage/smart-keyboard-handler.js
index 859ab69372e09515f967a10a42c421ea7b750341..b6a65f2ee5d43b43ae91722b43507c39febd1941 100644
--- a/lib/web/mage/smart-keyboard-handler.js
+++ b/lib/web/mage/smart-keyboard-handler.js
@@ -16,7 +16,8 @@ define([
             CODE_TAB = 9;
 
         return {
-            apply: smartKeyboardFocus
+            apply: smartKeyboardFocus,
+            focus: handleFocus
         };
 
         /**
@@ -49,22 +50,39 @@ define([
          * Handle logic, when onTabKeyPress fired at first.
          * Then it changes state.
          */
-        function onFocusInHandler () {
+        function onFocusInHandler() {
             focusState = true;
-            $('body').addClass(tabFocusClass)
+            body.addClass(tabFocusClass)
                 .off('focusin.keyboardHandler', onFocusInHandler);
         }
 
         /**
          * Handle logic to remove state after onTabKeyPress to normal.
-         * @param {Event} event
          */
-        function onClickHandler(event) {
-            focusState  = false;
-            $('body').removeClass(tabFocusClass)
+        function onClickHandler() {
+            focusState = false;
+            body.removeClass(tabFocusClass)
                 .off('click', onClickHandler);
         }
+
+        /**
+         * Attach smart focus on specific element.
+         * @param {jQuery} element
+         */
+        function handleFocus(element) {
+            element.on('focusin.emulateTabFocus', function () {
+                focusState = true;
+                body.addClass(tabFocusClass);
+                element.off();
+            });
+
+            element.on('focusout.emulateTabFocus', function () {
+                focusState = false;
+                body.removeClass(tabFocusClass);
+                element.off();
+            });
+        }
     }
 
     return new KeyboardHandler;
-});
\ No newline at end of file
+});
diff --git a/lib/web/mage/validation.js b/lib/web/mage/validation.js
index 6c8deba8164112a31939b11d1ed79b5c23dd7bc0..df2cd0288648e4a33b6e724c4141baf31d3a4d17 100644
--- a/lib/web/mage/validation.js
+++ b/lib/web/mage/validation.js
@@ -1543,6 +1543,7 @@
                 this.validate.resetForm();
             }
         },
+
         /**
          * Validation creation
          * @protected
@@ -1559,36 +1560,46 @@
 
             this._listenFormValidate();
         },
+
         /**
          * Validation listening
          * @protected
          */
         _listenFormValidate: function () {
-            $('form').on('invalid-form.validate', function (event, validation) {
-                var firstActive = $(validation.errorList[0].element || []),
-                    lastActive = $(validation.findLastActive() || validation.errorList.length && validation.errorList[0].element || []);
-
-                if (lastActive.is(':hidden')) {
-                    var parent = lastActive.parent();
-                    var windowHeight = $(window).height();
-                    $('html, body').animate({
-                        scrollTop: parent.offset().top - windowHeight / 2
-                    });
-                }
-
-                // ARIA (removing aria attributes if success)
-                var successList = validation.successList;
-                if (successList.length) {
-                    $.each(successList, function () {
-                        $(this)
-                            .removeAttr('aria-describedby')
-                            .removeAttr('aria-invalid');
-                    })
-                }
-                if (firstActive.length) {
-                    firstActive.focus();
-                }
-            });
+            $('form').on('invalid-form.validate', this.listenFormValidateHandler);
+        },
+
+        /**
+         * Handle form validation. Focus on first invalid form field.
+         *
+         * @param {jQuery.Event} event
+         * @param {Object} validation
+         */
+        listenFormValidateHandler:  function (event, validation) {
+            var firstActive = $(validation.errorList[0].element || []),
+                lastActive = $(validation.findLastActive() || validation.errorList.length && validation.errorList[0].element || []);
+
+            if (lastActive.is(':hidden')) {
+                var parent = lastActive.parent();
+                var windowHeight = $(window).height();
+                $('html, body').animate({
+                    scrollTop: parent.offset().top - windowHeight / 2
+                });
+            }
+
+            // ARIA (removing aria attributes if success)
+            var successList = validation.successList;
+            if (successList.length) {
+                $.each(successList, function () {
+                    $(this)
+                        .removeAttr('aria-describedby')
+                        .removeAttr('aria-invalid');
+                });
+            }
+
+            if (firstActive.length) {
+                firstActive.focus();
+            }
         }
     });