diff --git a/docs/_jade/customizer-nav.jade b/docs/_jade/customizer-nav.jade
index c4f6ddf105979ed39586a30cffda1db2d9ff4651..eba71ef6bf124cddcbd4eadeb2c0d7eb77bc2115 100644
--- a/docs/_jade/customizer-nav.jade
+++ b/docs/_jade/customizer-nav.jade
@@ -1,4 +1,6 @@
 // NOTE: DO NOT EDIT THE FOLLOWING SECTION DIRECTLY! It is autogenerated via the `build-customizer-html` Grunt task using the customizer-nav.jade template.
+li
+  a(href='#import') Import
 li
   a(href='#less') Less components
 li
diff --git a/docs/assets/css/src/docs.css b/docs/assets/css/src/docs.css
index 142519bd5fc0704ad5f73483e47dfdecd1671806..ea0739893d5b35c849ec315e5ea90702ce38203a 100644
--- a/docs/assets/css/src/docs.css
+++ b/docs/assets/css/src/docs.css
@@ -1378,6 +1378,30 @@ h1[id] {
           box-shadow: inset 0 2px 4px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1);
 }
 
+.bs-dropzone {
+  position: relative;
+  padding: 20px;
+  margin-bottom: 20px;
+  color: #777;
+  text-align: center;
+  border: 2px dashed #eee;
+  border-radius: 4px;
+}
+.bs-dropzone h2 {
+  margin-top: 0;
+  margin-bottom: 5px;
+}
+.bs-dropzone .lead {
+  margin-bottom: 10px;
+  font-weight: normal;
+  color: #333;
+}
+.bs-dropzone hr {
+  width: 100px;
+}
+.bs-dropzone p:last-child {
+  margin-bottom: 0;
+}
 
 /*
  * Brand guidelines
diff --git a/docs/assets/js/src/customizer.js b/docs/assets/js/src/customizer.js
index c3a212f2c7274db1df0e788509776982ecf2cd83..7340f1668fdc50ee5b41fab274de5a52c16a042d 100644
--- a/docs/assets/js/src/customizer.js
+++ b/docs/assets/js/src/customizer.js
@@ -16,6 +16,9 @@ window.onload = function () { // wait for load in a dumb way because B-0
            ' * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n' +
            ' */\n\n'
 
+  var supportsFile = (window.File && window.FileReader && window.FileList && window.Blob)
+  var importDropTarget = $('#import-drop-target')
+
   function showError(msg, err) {
     $('<div id="bsCustomizerAlert" class="bs-customizer-alert">' +
         '<div class="container">' +
@@ -46,6 +49,11 @@ window.onload = function () { // wait for load in a dumb way because B-0
     }
   }
 
+  function showAlert(type, msg, insertAfter) {
+    $('<div class="alert alert-' + type + '">' + msg + '<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button></div>')
+      .insertAfter(insertAfter)
+  }
+
   function getQueryParam(key) {
     key = key.replace(/[*+?^$.\[\]{}()|\\\/]/g, '\\$&') // escape RegEx meta chars
     var match = location.search.match(new RegExp('[?&]' + key + '=([^&]+)(&|$)'))
@@ -106,6 +114,24 @@ window.onload = function () { // wait for load in a dumb way because B-0
     return data
   }
 
+  function updateCustomizerFromJson(data) {
+    if (data.js) {
+      $('#plugin-section input').each(function () {
+        $(this).prop('checked', ~$.inArray(this.value, data.js))
+      })
+    }
+    if (data.css) {
+      $('#less-section input').each(function () {
+        $(this).prop('checked', ~$.inArray(this.value, data.css))
+      })
+    }
+    if (data.vars) {
+      for (var i in data.vars) {
+        $('input[data-var="' + i + '"]').val(data.vars[i])
+      }
+    }
+  }
+
   function parseUrl() {
     var id = getQueryParam('id')
 
@@ -118,21 +144,7 @@ window.onload = function () { // wait for load in a dumb way because B-0
     })
     .success(function (result) {
       var data = JSON.parse(result.files['config.json'].content)
-      if (data.js) {
-        $('#plugin-section input').each(function () {
-          $(this).prop('checked', ~$.inArray(this.value, data.js))
-        })
-      }
-      if (data.css) {
-        $('#less-section input').each(function () {
-          $(this).prop('checked', ~$.inArray(this.value, data.css))
-        })
-      }
-      if (data.vars) {
-        for (var i in data.vars) {
-          $('input[data-var="' + i + '"]').val(data.vars[i])
-        }
-      }
+      updateCustomizerFromJson(data)
     })
     .error(function (err) {
       showError('Error fetching bootstrap config file', err)
@@ -324,6 +336,61 @@ window.onload = function () { // wait for load in a dumb way because B-0
     }
   }
 
+  function removeImportAlerts() {
+    importDropTarget.nextAll('.alert').remove()
+  }
+
+  function handleConfigFileSelect(e) {
+    e.stopPropagation()
+    e.preventDefault()
+
+    var file = (e.originalEvent.hasOwnProperty('dataTransfer')) ? e.originalEvent.dataTransfer.files[0] : e.originalEvent.target.files[0]
+
+    if (!file.type.match('application/json')) {
+      return showAlert('danger', '<strong>Ruh roh.</strong> We can only read <code>.json</code> files. Please try again.', importDropTarget)
+    }
+
+    var reader = new FileReader()
+
+    reader.onload = (function () {
+      return function (e) {
+        var text = e.target.result
+
+        try {
+          var json = JSON.parse(text)
+
+          if (typeof json != 'object') {
+            throw new Error('JSON data from config file is not an object.')
+          }
+
+          updateCustomizerFromJson(json)
+          showAlert('success', '<strong>Woohoo!</strong> Your configuration was successfully uploaded. Tweak your settings, then hit Download.', importDropTarget)
+        } catch (err) {
+          return showAlert('danger', '<strong>Shucks.</strong> We can only read valid <code>.json</code> files. Please try again.', importDropTarget)
+        }
+      }
+    })(file)
+
+    reader.readAsText(file)
+  }
+
+  function handleConfigDragOver(e) {
+    e.stopPropagation()
+    e.preventDefault()
+    e.originalEvent.dataTransfer.dropEffect = 'copy'
+
+    removeImportAlerts()
+  }
+
+  if (supportsFile) {
+    importDropTarget
+      .on('dragover', handleConfigDragOver)
+      .on('drop', handleConfigFileSelect)
+  }
+
+  $('#import-file-select').on('select', handleConfigFileSelect)
+  $('#import-manual-trigger').on('click', removeImportAlerts)
+
   var inputsComponent = $('#less-section input')
   var inputsPlugin    = $('#plugin-section input')
   var inputsVariables = $('#less-variables-section input')
@@ -410,7 +477,8 @@ window.onload = function () { // wait for load in a dumb way because B-0
       { type: 'image/svg+xml;charset=utf-8' }
     )
     var objectUrl = url.createObjectURL(svg);
-    if (/^blob:/.exec(objectUrl) === null) {
+
+    if (/^blob:/.exec(objectUrl) === null || !supportsFile) {
       // `URL.createObjectURL` created a URL that started with something other
       // than "blob:", which means it has been polyfilled and is not supported by
       // this browser.
diff --git a/docs/customize.html b/docs/customize.html
index 2e067b8f4828c7a57ca5a402bb72fedaa611b51d..2e918bd6eec8840e3e46cf0187fa5e750c09584e 100644
--- a/docs/customize.html
+++ b/docs/customize.html
@@ -12,6 +12,7 @@ lead: Customize Bootstrap's components, Less variables, and jQuery plugins to ge
 <!--[if lt IE 9]>
   <style>
     .bs-customizer,
+    .bs-customizer-import,
     .bs-docs-sidebar {
       display: none;
     }
@@ -23,6 +24,17 @@ lead: Customize Bootstrap's components, Less variables, and jQuery plugins to ge
 <![endif]-->
 
 <!-- Customizer form -->
+
+<div class="bs-docs-section bs-customizer-import">
+  <div id="import-drop-target" class="bs-dropzone">
+    <h2>✍</h2>
+    <p class="lead">Have an existing configuration? Upload your <code>config.json</code> to import it.</p>
+    <p>Drag and drop here, or <label id="import-manual-trigger" class="btn-link">manually upload<input type="file" id="import-file-select" class="hidden"></label>.</p>
+    <hr>
+    <p><strong>Don't have one?</strong> That's okay—just start customizing the fields below.</p>
+  </div>
+</div><!-- /import -->
+
 <form class="bs-customizer" role="form">
   <div class="bs-docs-section" id="less-section">
     <button class="btn btn-default toggle" type="button">Toggle all</button>
@@ -358,8 +370,6 @@ lead: Customize Bootstrap's components, Less variables, and jQuery plugins to ge
     {% include customizer-variables.html %}
   </div>
 
-
-
   <div class="bs-docs-section">
     <h1 id="download" class="page-header">Download</h1>