diff --git a/build/vnu-jar.js b/build/vnu-jar.js
index 5fd214a9d07fcc2c156edf1d792daa589fb5d4de..f186d6bd7b8e7dd714f34156ce857938d83d849f 100644
--- a/build/vnu-jar.js
+++ b/build/vnu-jar.js
@@ -21,6 +21,7 @@ childProcess.exec('java -version', (error, stdout, stderr) => {
   // vnu-jar accepts multiple ignores joined with a `|`.
   // Also note that the ignores are regular expressions.
   const ignores = [
+    'A document must not include more than one “autofocus” attribute.*',
     // "autocomplete" is included in <button> and checkboxes and radio <input>s due to
     // Firefox's non-standard autocomplete behavior - see https://bugzilla.mozilla.org/show_bug.cgi?id=654072
     'Attribute “autocomplete” is only allowed when the input type is.*',
diff --git a/js/src/modal.js b/js/src/modal.js
index 0004fe8bbefc38d596f2a09ec38b2259bdb58088..994c5bf817dc1f27669f1924644c506b4f95dc46 100644
--- a/js/src/modal.js
+++ b/js/src/modal.js
@@ -21,17 +21,23 @@ const EVENT_KEY          = `.${DATA_KEY}`
 const DATA_API_KEY       = '.data-api'
 const JQUERY_NO_CONFLICT = $.fn[NAME]
 const ESCAPE_KEYCODE     = 27 // KeyboardEvent.which value for Escape (Esc) key
+const LEFT_KEYCODE       = 37
+const RIGHT_KEYCODE      = 39
 
 const Default = {
+  autofocus: 'notTouch',      // true|false|notTouch
   backdrop : true,
   keyboard : true,
+  keyboardBtnNav: true,  // ability to use arrows to nav button focus
   focus    : true,
   show     : true
 }
 
 const DefaultType = {
+  autofocus: '(boolean|string)',
   backdrop : '(boolean|string)',
   keyboard : 'boolean',
+  keyboardBtnNav: 'boolean',
   focus    : 'boolean',
   show     : 'boolean'
 }
@@ -45,6 +51,7 @@ const Event = {
   RESIZE            : `resize${EVENT_KEY}`,
   CLICK_DISMISS     : `click.dismiss${EVENT_KEY}`,
   KEYDOWN_DISMISS   : `keydown.dismiss${EVENT_KEY}`,
+  KEYDOWN_NAV       : `keydown.nav${EVENT_KEY}`,
   MOUSEUP_DISMISS   : `mouseup.dismiss${EVENT_KEY}`,
   MOUSEDOWN_DISMISS : `mousedown.dismiss${EVENT_KEY}`,
   CLICK_DATA_API    : `click${EVENT_KEY}${DATA_API_KEY}`
@@ -130,6 +137,7 @@ class Modal {
     $(document.body).addClass(ClassName.OPEN)
 
     this._setEscapeEvent()
+    this._setKeyNavEvent()
     this._setResizeEvent()
 
     $(this._element).on(
@@ -174,6 +182,7 @@ class Modal {
     }
 
     this._setEscapeEvent()
+    this._setKeyNavEvent()
     this._setResizeEvent()
 
     $(document).off(Event.FOCUSIN)
@@ -234,6 +243,11 @@ class Modal {
     return config
   }
 
+  // Util worthy?
+  _isTouchDevice() {
+    return 'ontouchstart' in window || window.DocumentTouch && document instanceof window.DocumentTouch
+  }
+
   _showElement(relatedTarget) {
     const transition = $(this._element).hasClass(ClassName.FADE)
 
@@ -265,6 +279,9 @@ class Modal {
       if (this._config.focus) {
         this._element.focus()
       }
+      if (this._config.autofocus === true || this._config.autofocus === 'notTouch' && !this._isTouchDevice()) {
+        this._autofocus()
+      }
       this._isTransitioning = false
       $(this._element).trigger(shownEvent)
     }
@@ -280,6 +297,10 @@ class Modal {
     }
   }
 
+  _autofocus() {
+    $(this._element).find(':input[autofocus]:not(:hidden)').eq(0).trigger('focus')
+  }
+
   _enforceFocus() {
     $(document)
       .off(Event.FOCUSIN) // Guard against infinite focus loop
@@ -305,6 +326,20 @@ class Modal {
     }
   }
 
+  _setKeyNavEvent() {
+    if (this._isShown && this._config.keyboardBtnNav) {
+      $(this._element).on(Event.KEYDOWN_NAV, (event) => {
+        if (event.which === LEFT_KEYCODE) {
+          this._keyboardBtnNav('prev')
+        } else if (event.which === RIGHT_KEYCODE) {
+          this._keyboardBtnNav('next')
+        }
+      })
+    } else if (!this._isShown) {
+      $(this._element).off(Event.KEYDOWN_NAV)
+    }
+  }
+
   _setResizeEvent() {
     if (this._isShown) {
       $(window).on(Event.RESIZE, (event) => this.handleUpdate(event))
@@ -325,6 +360,29 @@ class Modal {
     })
   }
 
+  _keyboardBtnNav(prevNext) {
+    const $focusable = $(this._element).find('.btn')
+    let curFocusIdx = $focusable.index(document.activeElement)
+    if ($(document.activeElement).is(':input:not(:button)')) {
+      // we're currently focused on an input, stay put
+      return
+    }
+    if (curFocusIdx < 0) {
+      // nothing currently focused
+      // "next" will focus first $focusable, "prev" will focus last $focusable
+      curFocusIdx = prevNext === 'next' ? -1 : 0
+    }
+    if (prevNext === 'prev') {
+      // eq() accepts negative index
+      $focusable.eq(curFocusIdx - 1).trigger('focus')
+    } else if (curFocusIdx === $focusable.length - 1) {
+      // last btn is focused, wrap back to first
+      $focusable.eq(0).trigger('focus')
+    } else {
+      $focusable.eq(curFocusIdx + 1).trigger('focus')
+    }
+  }
+
   _removeBackdrop() {
     if (this._backdrop) {
       $(this._backdrop).remove()
diff --git a/js/tests/unit/modal.js b/js/tests/unit/modal.js
index 1156ce0c7009b13698d2b292fe1063fcb7230096..a3e07b2e0b73fc50851a0ff5c13adae44713e5c4 100644
--- a/js/tests/unit/modal.js
+++ b/js/tests/unit/modal.js
@@ -733,6 +733,218 @@ $(function () {
 
       $.fn.off.restore()
       done()
-    }).bootstrapModal('show')
+    })
+      .bootstrapModal('show')
+  })
+
+  QUnit.test('right arrow should select first button when none focused', function (assert) {
+    assert.expect(1)
+    var done = assert.async()
+    var $div = $('<div id="modal-test"><div class="contents">' +
+      '<button class="btn">button 1</button>' +
+      '<button class="btn">button 2</button>' +
+      '</div>' +
+      '</div>')
+    $div.on('shown.bs.modal', function () {
+      $div.trigger($.Event('keydown', {
+        which: 39
+      }))
+      setTimeout(function () {
+        assert.ok($(document.activeElement).is($div.find('.btn').first()), 'first button selected')
+        done()
+      }, 0)
+    })
+      .bootstrapModal('show')
+  })
+
+  QUnit.test('right arrow should select next button', function (assert) {
+    assert.expect(1)
+    var done = assert.async()
+    var $div = $('<div id="modal-test"><div class="contents">' +
+      '<button class="btn">button 1</button>' +
+      '<button class="btn">button 2</button>' +
+      '</div>' +
+      '</div>')
+    $div.on('shown.bs.modal', function () {
+      $div.find('.btn').first().trigger('focus')
+      setTimeout(function () {
+        $div.trigger($.Event('keydown', {
+          which: 39
+        }))
+      }, 0)
+      setTimeout(function () {
+        assert.ok($(document.activeElement).is($div.find('.btn').last()), 'last button selected')
+        done()
+      }, 0)
+    })
+      .bootstrapModal('show')
+  })
+
+  QUnit.test('right arrow should select first button when last focused', function (assert) {
+    assert.expect(1)
+    var done = assert.async()
+    var $div = $('<div id="modal-test"><div class="contents">' +
+      '<button class="btn">button 1</button>' +
+      '<button class="btn">button 2</button>' +
+      '</div>' +
+      '</div>')
+    $div.on('shown.bs.modal', function () {
+      $div.find('.btn').last().trigger('focus')
+      setTimeout(function () {
+        $div.trigger($.Event('keydown', {
+          which: 39
+        }))
+      }, 0)
+      setTimeout(function () {
+        assert.ok($(document.activeElement).is($div.find('.btn').first()), 'right arrow wrapped')
+        done()
+      }, 0)
+    })
+      .bootstrapModal('show')
+  })
+
+  QUnit.test('right arrow should not take focus from input', function (assert) {
+    assert.expect(1)
+    var done = assert.async()
+    var $div = $('<div id="modal-test"><div class="contents">' +
+      '<input type="text" />' +
+      '<button class="btn">button 1</button>' +
+      '<button class="btn">button 2</button>' +
+      '</div>' +
+      '</div>')
+    $div.on('shown.bs.modal', function () {
+      $div.find('input').trigger('focus')
+      setTimeout(function () {
+        $div.trigger($.Event('keydown', {
+          which: 39
+        }))
+      }, 0)
+      setTimeout(function () {
+        assert.ok($(document.activeElement).is('input'), 'input still focused')
+        done()
+      }, 0)
+    })
+      .bootstrapModal('show')
+  })
+
+  QUnit.test('left arrow should last button when none focused', function (assert) {
+    assert.expect(1)
+    var done = assert.async()
+    var $div = $('<div id="modal-test"><div class="contents">' +
+      '<button class="btn">button 1</button>' +
+      '<button class="btn">button 2</button>' +
+      '</div>' +
+      '</div>')
+    $div.on('shown.bs.modal', function () {
+      $div.trigger($.Event('keydown', {
+        which: 37
+      }))
+      setTimeout(function () {
+        assert.ok($(document.activeElement).is($div.find('.btn').last()), 'last button selected')
+        done()
+      }, 0)
+    })
+      .bootstrapModal('show')
+  })
+
+  QUnit.test('left arrow should select prev button', function (assert) {
+    assert.expect(1)
+    var done = assert.async()
+    var $div = $('<div id="modal-test"><div class="contents">' +
+      '<button class="btn">button 1</button>' +
+      '<button class="btn">button 2</button>' +
+      '</div>' +
+      '</div>')
+    $div.on('shown.bs.modal', function () {
+      $div.find('.btn').last().trigger('focus')
+      setTimeout(function () {
+        $div.trigger($.Event('keydown', {
+          which: 37
+        }))
+      }, 0)
+      setTimeout(function () {
+        assert.ok($(document.activeElement).is($div.find('.btn').first()), 'first button selected')
+        done()
+      }, 0)
+    })
+      .bootstrapModal('show')
+  })
+
+  QUnit.test('left arrow should select last button when first focused', function (assert) {
+    assert.expect(1)
+    var done = assert.async()
+    var $div = $('<div id="modal-test"><div class="contents">' +
+      '<button class="btn">button 1</button>' +
+      '<button class="btn">button 2</button>' +
+      '</div>' +
+      '</div>')
+    $div.on('shown.bs.modal', function () {
+      $div.find('.btn').first().trigger('focus')
+      setTimeout(function () {
+        $div.trigger($.Event('keydown', {
+          which: 37
+        }))
+      }, 0)
+      setTimeout(function () {
+        assert.ok($(document.activeElement).is($div.find('.btn').last()), 'last button selected')
+        done()
+      }, 0)
+    })
+      .bootstrapModal('show')
+  })
+
+  QUnit.test('right arrow should not take focus from input', function (assert) {
+    assert.expect(1)
+    var done = assert.async()
+    var $div = $('<div id="modal-test"><div class="contents">' +
+      '<input type="text" />' +
+      '<button class="btn">button 1</button>' +
+      '<button class="btn">button 2</button>' +
+      '</div>' +
+      '</div>')
+    $div.on('shown.bs.modal', function () {
+      $div.find('input').trigger('focus')
+      setTimeout(function () {
+        $div.trigger($.Event('keydown', {
+          which: 37
+        }))
+      }, 0)
+      setTimeout(function () {
+        assert.ok($(document.activeElement).is('input'), 'input still focused')
+        done()
+      }, 0)
+    })
+      .bootstrapModal('show')
+  })
+
+  QUnit.test(':input[autofocus] should get focus', function (assert) {
+    assert.expect(2)
+    var done = assert.async()
+    $('body').append('<div id="modal-test" class="modal" data-autofocus="true"><div class="contents">' +
+      '<input id="my-input" type="text" autofocus />' +
+      '</div>' +
+      '</div>')
+    assert.notOk($(document.activeElement).is('#my-input'), 'input focused')
+    $('#modal-test').on('shown.bs.modal', function () {
+      assert.ok($(document.activeElement).is('#my-input'), 'input focused')
+      done()
+    })
+      .bootstrapModal('show')
+  })
+
+  QUnit.test(':input[autofocus] should not get focus (default)', function (assert) {
+    assert.expect(1)
+    var done = assert.async()
+    $('body').append(
+      '<div id="modal-test" class="modal" data-autofocus="false"><div class="contents">' +
+      '<input id="my-input" type="text" autofocus />' +
+      '</div>' +
+      '</div>')
+    $('#modal-test')
+      .on('shown.bs.modal', function () {
+        assert.notOk($(document.activeElement).is('#my-input'), 'input does not have focus')
+        done()
+      })
+      .bootstrapModal('show')
   })
 })
diff --git a/package.json b/package.json
index 29665aa4ae19294d6f3e644f678bf82f21502193..5ed27d2c869024c62daa48e265d1e9507513c4a0 100644
--- a/package.json
+++ b/package.json
@@ -190,7 +190,7 @@
     },
     {
       "path": "./dist/js/bootstrap.js",
-      "maxSize": "22 kB"
+      "maxSize": "23 kB"
     },
     {
       "path": "./dist/js/bootstrap.min.js",
diff --git a/site/docs/4.1/components/modal.md b/site/docs/4.1/components/modal.md
index 86996ecda4314469fdc6961f4badb7ffbfcb284b..09df5f7f670fc317028a0a812f833823cb2c490d 100644
--- a/site/docs/4.1/components/modal.md
+++ b/site/docs/4.1/components/modal.md
@@ -15,13 +15,6 @@ Before getting started with Bootstrap's modal component, be sure to read the fol
 - Bootstrap only supports one modal window at a time. Nested modals aren't supported as we believe them to be poor user experiences.
 - Modals use `position: fixed`, which can sometimes be a bit particular about its rendering. Whenever possible, place your modal HTML in a top-level position to avoid potential interference from other elements. You'll likely run into issues when nesting a `.modal` within another fixed element.
 - Once again, due to `position: fixed`, there are some caveats with using modals on mobile devices. [See our browser support docs]({{ site.baseurl }}/docs/{{ site.docs_version }}/getting-started/browsers-devices/#modals-and-dropdowns-on-mobile) for details.
-- Due to how HTML5 defines its semantics, [the `autofocus` HTML attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-autofocus) has no effect in Bootstrap modals. To achieve the same effect, use some custom JavaScript:
-
-{% highlight js %}
-$('#myModal').on('shown.bs.modal', function () {
-  $('#myInput').trigger('focus')
-})
-{% endhighlight %}
 
 {% include callout-info-prefersreducedmotion.md %}
 
@@ -70,7 +63,7 @@ Below is a _static_ modal example (meaning its `position` and `display` have bee
       </div>
       <div class="modal-footer">
         <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
-        <button type="button" class="btn btn-primary">Save changes</button>
+        <button type="button" class="btn btn-primary" autofocus>Save changes</button>
       </div>
     </div>
   </div>
@@ -95,7 +88,7 @@ Toggle a working modal demo by clicking the button below. It will slide down and
       </div>
       <div class="modal-footer">
         <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
-        <button type="button" class="btn btn-primary">Save changes</button>
+        <button type="button" class="btn btn-primary" autofocus>Save changes</button>
       </div>
     </div>
   </div>
@@ -128,7 +121,7 @@ Toggle a working modal demo by clicking the button below. It will slide down and
       </div>
       <div class="modal-footer">
         <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
-        <button type="button" class="btn btn-primary">Save changes</button>
+        <button type="button" class="btn btn-primary" autofocus>Save changes</button>
       </div>
     </div>
   </div>
@@ -228,7 +221,7 @@ Add `.modal-dialog-centered` to `.modal-dialog` to vertically center the modal.
       </div>
       <div class="modal-footer">
         <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
-        <button type="button" class="btn btn-primary">Save changes</button>
+        <button type="button" class="btn btn-primary" autofocus>Save changes</button>
       </div>
     </div>
   </div>
@@ -261,7 +254,7 @@ Add `.modal-dialog-centered` to `.modal-dialog` to vertically center the modal.
       </div>
       <div class="modal-footer">
         <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
-        <button type="button" class="btn btn-primary">Save changes</button>
+        <button type="button" class="btn btn-primary" autofocus>Save changes</button>
       </div>
     </div>
   </div>
@@ -289,7 +282,7 @@ Add `.modal-dialog-centered` to `.modal-dialog` to vertically center the modal.
         <p><a href="#" class="tooltip-test" title="Tooltip" data-container="#exampleModalPopovers">This link</a> and <a href="#" class="tooltip-test" title="Tooltip" data-container="#exampleModalPopovers">that link</a> have tooltips on hover.</p>
       </div>
       <div class="modal-footer">
-        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
+        <button type="button" class="btn btn-secondary" data-dismiss="modal" autofocus>Close</button>
         <button type="button" class="btn btn-primary">Save changes</button>
       </div>
     </div>
@@ -352,7 +345,7 @@ Utilize the Bootstrap grid system within a modal by nesting `.container-fluid` w
         </div>
       </div>
       <div class="modal-footer">
-        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
+        <button type="button" class="btn btn-secondary" data-dismiss="modal" autofocus>Close</button>
         <button type="button" class="btn btn-primary">Save changes</button>
       </div>
     </div>
@@ -420,7 +413,7 @@ Below is a live demo followed by example HTML and JavaScript. For more informati
         <form>
           <div class="form-group">
             <label for="recipient-name" class="col-form-label">Recipient:</label>
-            <input type="text" class="form-control" id="recipient-name">
+            <input type="text" class="form-control" id="recipient-name" autofocus>
           </div>
           <div class="form-group">
             <label for="message-text" class="col-form-label">Message:</label>
@@ -474,6 +467,10 @@ If the height of a modal changes while it is open, you should call `$('#myModal'
 
 Be sure to add `role="dialog"` and `aria-labelledby="..."`, referencing the modal title, to `.modal`, and `role="document"` to the `.modal-dialog` itself. Additionally, you may give a description of your modal dialog with `aria-describedby` on `.modal`.
 
+The `autofocus` attribute may be given to inputs and buttons in modals.  By default, when not on a touch-device, focus will be given to the autofocus input/button when the modal is shown.
+
+By default, keyboard left & right arrow-keys can be used to focus buttons within the modal (ie change between "close" and "save").
+
 ### Embedding YouTube videos
 
 Embedding YouTube videos in modals requires additional JavaScript not in Bootstrap to automatically stop playback and more. [See this helpful Stack Overflow post](https://stackoverflow.com/questions/18622508/bootstrap-3-and-youtube-in-modal) for more information.
@@ -639,6 +636,18 @@ Options can be passed via data attributes or JavaScript. For data attributes, ap
     </tr>
   </thead>
   <tbody>
+    <tr>
+      <td>autofocus</td>
+      <td>boolean or the string <code>'notTouch'</code></td>
+      <td>'notTouch'</td>
+      <td>Whether input with `autofocus` attribute should be given focus when modal is shown<br />
+          <ul class="list-unstyled">
+            <li><code>notTouch</code> will give focus when not a touch device</li>
+            <li><code>true</code> will give focus regardless</li>
+            <li><code>false</code> no autofocus</li>
+          </ul>
+      </td>
+    </tr>
     <tr>
       <td>backdrop</td>
       <td>boolean or the string <code>'static'</code></td>
@@ -651,6 +660,14 @@ Options can be passed via data attributes or JavaScript. For data attributes, ap
       <td>true</td>
       <td>Closes the modal when escape key is pressed</td>
     </tr>
+    <tr>
+      <td>keyboardBtnNav</td>
+      <td>boolean</td>
+      <td>true</td>
+      <td>Whether keyboard's left and right arrow keys should move focus to/between `.btn` elements.
+        <span class="text-muted">(focus will not be taken from input elements)</span>
+      </td>
+    </tr>
     <tr>
       <td>focus</td>
       <td>boolean</td>