diff --git a/commerce_shipping.libraries.yml b/commerce_shipping.libraries.yml
new file mode 100644
index 0000000..1ef6494
--- /dev/null
+++ b/commerce_shipping.libraries.yml
@@ -0,0 +1,8 @@
+shipping_checkout:
+  version: VERSION
+  js:
+    js/shipping_checkout.js: {}
+  dependencies:
+    - core/drupalSettings
+    - core/jquery
+    - core/jquery.once
diff --git a/config/schema/commerce_shipping.schema.yml b/config/schema/commerce_shipping.schema.yml
index 44fa6cf..1d8b152 100644
--- a/config/schema/commerce_shipping.schema.yml
+++ b/config/schema/commerce_shipping.schema.yml
@@ -26,6 +26,9 @@ commerce_order.commerce_order_type.*.third_party.commerce_shipping:
 commerce_checkout.commerce_checkout_pane.shipping_information:
   type: commerce_checkout_pane_configuration
   mapping:
+    auto_recalculate:
+      type: boolean
+      label: 'Auto recalculate shipping costs when the shipping address changes'
     require_shipping_profile:
       type: boolean
       label: 'Hide shipping costs until an address is entered'
diff --git a/js/shipping_checkout.js b/js/shipping_checkout.js
new file mode 100644
index 0000000..9161151
--- /dev/null
+++ b/js/shipping_checkout.js
@@ -0,0 +1,51 @@
+/**
+ * @file
+ * Defines behaviors and callbacks for Commerce Shipping.
+ */
+(function ($, Drupal) {
+  'use strict';
+
+  /**
+   * Attaches the shipping recalculate behavior.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   */
+  Drupal.behaviors.commerceShippingRecalculate = {
+    attach: function (context, settings) {
+      if (settings.hasOwnProperty('commerceShipping')) {
+        var shippingSettings = settings.commerceShipping;
+        $(window).once('commerce-shipping').on('load', function () {
+          $.fn.commerceCheckShippingRecalculation(shippingSettings);
+        });
+        $.each(shippingSettings.recalculateFields, function (index, value) {
+          $(shippingSettings.wrapper + ' .field--type-address [name$="[' + value + ']"]').on('change', function() {
+            $.fn.commerceCheckShippingRecalculation(shippingSettings);
+          });
+        });
+      }
+    }
+  };
+
+  /**
+   * Checks to see if we can recalculate shipping rates.
+   */
+  $.fn.commerceCheckShippingRecalculation = function(settings) {
+    var recalculate = true;
+
+    $(settings.wrapper + ' .field--type-address :input.required').filter(':not(.chosen-container)').each(function() {
+      if (!$(this).val()) {
+        recalculate = false;
+      }
+    });
+
+    if (recalculate) {
+      return setTimeout(function() {
+        // Trigger the mousedown event on the shipping recalculation button.
+        $(settings.wrapper).find(settings.recalculateButtonSelector).trigger('mousedown');
+      }, 100);
+    }
+  };
+
+})(jQuery, Drupal);
diff --git a/src/Plugin/Commerce/CheckoutPane/ShippingInformation.php b/src/Plugin/Commerce/CheckoutPane/ShippingInformation.php
index 71f1880..8bd748c 100644
--- a/src/Plugin/Commerce/CheckoutPane/ShippingInformation.php
+++ b/src/Plugin/Commerce/CheckoutPane/ShippingInformation.php
@@ -87,6 +87,7 @@ class ShippingInformation extends CheckoutPaneBase implements ContainerFactoryPl
    */
   public function defaultConfiguration() {
     return [
+      'auto_recalculate' => TRUE,
       'require_shipping_profile' => TRUE,
     ] + parent::defaultConfiguration();
   }
@@ -110,6 +111,11 @@ class ShippingInformation extends CheckoutPaneBase implements ContainerFactoryPl
    */
   public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
     $form = parent::buildConfigurationForm($form, $form_state);
+    $form['auto_recalculate'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Auto recalculate shipping costs when the shipping address changes'),
+      '#default_value' => $this->configuration['auto_recalculate'],
+    ];
     $form['require_shipping_profile'] = [
       '#type' => 'checkbox',
       '#title' => $this->t('Hide shipping costs until an address is entered'),
@@ -127,6 +133,7 @@ class ShippingInformation extends CheckoutPaneBase implements ContainerFactoryPl
 
     if (!$form_state->getErrors()) {
       $values = $form_state->getValue($form['#parents']);
+      $this->configuration['auto_recalculate'] = !empty($values['auto_recalculate']);
       $this->configuration['require_shipping_profile'] = !empty($values['require_shipping_profile']);
     }
   }
@@ -181,7 +188,6 @@ class ShippingInformation extends CheckoutPaneBase implements ContainerFactoryPl
     $pane_form['#wrapper_id'] = 'shipping-information-wrapper';
     $pane_form['#prefix'] = '<div id="' . $pane_form['#wrapper_id'] . '">';
     $pane_form['#suffix'] = '</div>';
-
     $pane_form['shipping_profile'] = [
       '#type' => 'commerce_profile_select',
       '#default_value' => $shipping_profile,
@@ -201,6 +207,24 @@ class ShippingInformation extends CheckoutPaneBase implements ContainerFactoryPl
         array_merge($pane_form['#parents'], ['shipping_profile']),
       ],
     ];
+    // If the auto recalculation is enabled, hide the "Recalculate shipping"
+    // button.
+    if (!empty($this->configuration['auto_recalculate'])) {
+      $pane_form['#attached']['library'][] = 'commerce_shipping/shipping_checkout';
+      $pane_form['#attached']['drupalSettings']['commerceShipping'] = [
+        'wrapper' => '#' . $pane_form['#wrapper_id'],
+        'recalculateButtonSelector' => '[data-drupal-selector="edit-shipping-information-recalculate-shipping"]',
+        'recalculateFields' => [
+          'dependent_locality',
+          'locality',
+          'postal_code',
+          'administrative_area',
+        ],
+      ];
+      $pane_form['recalculate_shipping']['#attributes'] = [
+        'class' => ['js-hide'],
+      ];
+    }
     $pane_form['removed_shipments'] = [
       '#type' => 'value',
       '#value' => [],
diff --git a/src/Plugin/Field/FieldWidget/ShippingRateWidget.php b/src/Plugin/Field/FieldWidget/ShippingRateWidget.php
index b490184..fda7bbe 100644
--- a/src/Plugin/Field/FieldWidget/ShippingRateWidget.php
+++ b/src/Plugin/Field/FieldWidget/ShippingRateWidget.php
@@ -137,31 +137,33 @@ class ShippingRateWidget extends WidgetBase implements ContainerFactoryPluginInt
     $parents = array_merge($form['#parents'], [$field_name, 0]);
     $element = NestedArray::getValue($form, [$field_name, 'widget', 0]);
     $selected_value = NestedArray::getValue($form_state->getValues(), $parents, $key_exists);
-    if ($selected_value) {
+    if ($selected_value && isset($element[$selected_value])) {
       $shipping_method_id = $element[$selected_value]['#shipping_method_id'];
-      /** @var \Drupal\commerce_shipping\ShippingRate $shipping_rate */
-      $shipping_rate = $element[$selected_value]['#shipping_rate'];
-      /** @var \Drupal\commerce_shipping\Entity\ShipmentInterface $shipment */
-      $shipment = $items[0]->getEntity();
-      // @todo This should be done by selectRate() but the plugin doesn't
-      // have access to the parent entity ID yet.
-      $shipment->setShippingMethodId($shipping_method_id);
-
-      $shipping_method_storage = $this->entityTypeManager->getStorage('commerce_shipping_method');
-      /** @var \Drupal\commerce_shipping\Entity\ShippingMethodInterface $shipping_method */
-      $shipping_method = $shipping_method_storage->load($shipping_method_id);
-      $shipping_method_plugin = $shipping_method->getPlugin();
-      if (empty($shipment->getPackageType())) {
-        $shipment->setPackageType($shipping_method_plugin->getDefaultPackageType());
-      }
-      $shipping_method_plugin->selectRate($shipment, $shipping_rate);
-
-      // Put delta mapping in $form_state, so that flagErrors() can use it.
-      $field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
-      foreach ($items as $delta => $item) {
-        $field_state['original_deltas'][$delta] = $delta;
+      if ($shipping_method_id) {
+        /** @var \Drupal\commerce_shipping\ShippingRate $shipping_rate */
+        $shipping_rate = $element[$selected_value]['#shipping_rate'];
+        /** @var \Drupal\commerce_shipping\Entity\ShipmentInterface $shipment */
+        $shipment = $items[0]->getEntity();
+        // @todo This should be done by selectRate() but the plugin doesn't
+        // have access to the parent entity ID yet.
+        $shipment->setShippingMethodId($shipping_method_id);
+
+        $shipping_method_storage = $this->entityTypeManager->getStorage('commerce_shipping_method');
+        /** @var \Drupal\commerce_shipping\Entity\ShippingMethodInterface $shipping_method */
+        $shipping_method = $shipping_method_storage->load($shipping_method_id);
+        $shipping_method_plugin = $shipping_method->getPlugin();
+        if (empty($shipment->getPackageType())) {
+          $shipment->setPackageType($shipping_method_plugin->getDefaultPackageType());
+        }
+        $shipping_method_plugin->selectRate($shipment, $shipping_rate);
+
+        // Put delta mapping in $form_state, so that flagErrors() can use it.
+        $field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
+        foreach ($items as $delta => $item) {
+          $field_state['original_deltas'][$delta] = $delta;
+        }
+        static::setWidgetState($form['#parents'], $field_name, $form_state, $field_state);
       }
-      static::setWidgetState($form['#parents'], $field_name, $form_state, $field_state);
     }
   }
 
diff --git a/tests/src/FunctionalJavascript/CheckoutPaneTest.php b/tests/src/FunctionalJavascript/CheckoutPaneTest.php
index c90de70..298fa31 100644
--- a/tests/src/FunctionalJavascript/CheckoutPaneTest.php
+++ b/tests/src/FunctionalJavascript/CheckoutPaneTest.php
@@ -196,7 +196,7 @@ class CheckoutPaneTest extends CommerceBrowserTestBase {
   /**
    * Tests checkout with a single shipment.
    */
-  public function testSingleShipment() {
+  public function testSingleShipment($autocalculate = TRUE) {
     $this->drupalGet($this->firstProduct->toUrl()->toString());
     $this->submitForm([], 'Add to cart');
     $this->drupalGet($this->secondProduct->toUrl()->toString());
@@ -224,7 +224,13 @@ class CheckoutPaneTest extends CommerceBrowserTestBase {
     foreach ($address as $property => $value) {
       $page->fillField($address_prefix . '[' . $property . ']', $value);
     }
-    $page->findButton('Recalculate shipping')->click();
+    if ($autocalculate) {
+      // Wait for the recalculate button to be clicked.
+      $page->waitFor(0.3, function() {});
+    }
+    else {
+      $page->findButton('Recalculate shipping')->click();
+    }
     $this->waitForAjaxToFinish();
 
     $this->assertSession()->pageTextContains('Shipping method');
@@ -299,7 +305,8 @@ class CheckoutPaneTest extends CommerceBrowserTestBase {
     foreach ($address as $property => $value) {
       $page->fillField($address_prefix . '[' . $property . ']', $value);
     }
-    $page->findButton('Recalculate shipping')->click();
+    // Wait for the recalculate button to be clicked.
+    $page->waitFor(0.3, function() {});
     $this->waitForAjaxToFinish();
 
     foreach ([0, 1] as $shipment_index) {
@@ -376,6 +383,7 @@ class CheckoutPaneTest extends CommerceBrowserTestBase {
   public function testNoRequiredShippingProfile() {
     $checkout_flow = CheckoutFlow::load('shipping');
     $checkout_flow_configuration = $checkout_flow->get('configuration');
+    $checkout_flow_configuration['panes']['shipping_information']['auto_recalculate'] = FALSE;
     $checkout_flow_configuration['panes']['shipping_information']['require_shipping_profile'] = FALSE;
     $checkout_flow->set('configuration', $checkout_flow_configuration);
     $checkout_flow->save();
@@ -448,6 +456,18 @@ class CheckoutPaneTest extends CommerceBrowserTestBase {
     $this->assertEquals(new Price('26.97', 'USD'), $order->getTotalPrice());
   }
 
+  /**
+   * Tests checkout when the auto calculate is disabled.
+   */
+  public function testAutoCalculateDisabled() {
+    $checkout_flow = CheckoutFlow::load('shipping');
+    $checkout_flow_configuration = $checkout_flow->get('configuration');
+    $checkout_flow_configuration['panes']['shipping_information']['auto_recalculate'] = FALSE;
+    $checkout_flow->set('configuration', $checkout_flow_configuration);
+    $checkout_flow->save();
+    $this->testSingleShipment(FALSE);
+  }
+
   /**
    * Asserts that a select field has all of the provided options.
    *
