diff --git a/README.md b/README.md
index 1905881..acf071e 100644
--- a/README.md
+++ b/README.md
@@ -59,6 +59,10 @@ $schemes = [
 
       // 'endpoint' => 'https://api.example.com', // An alternative API endpoint
                                                   // for 3rd party S3 providers.
+
+      // 'cors' => TRUE,                          // Set to TRUE if CORS upload
+                                                  // support is enabled for the
+                                                  // bucket.
     ],
 
     'cache' => TRUE, // Creates a metadata cache to speed up lookups.
diff --git a/flysystem_s3.js b/flysystem_s3.js
new file mode 100644
index 0000000..f8073b4
--- /dev/null
+++ b/flysystem_s3.js
@@ -0,0 +1,214 @@
+/**
+ * @file
+ * Provides JavaScript additions to the S3 CORS upload managed file field type.
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  /**
+   * S3 File upload utility functions.
+   *
+   * @namespace
+   */
+  Drupal.flysystemS3 = Drupal.flysystemS3 || {
+
+    /**
+     * Submit file via CORS.
+     *
+     * @name Drupal.flysystemS3.submitCorsUpload
+     *
+     * @param {jQuery.Event} event
+     *   The event triggered, most likely a `change` event.
+     */
+    submitCorsUpload: function (event) {
+      var $fileElement = $(event.target);
+
+      if (typeof $fileElement[0].files !== 'undefined') {
+        // @todo This only supports the first file uploaded.
+        var file = $fileElement[0].files[0];
+        var $form = $(event.target).parents('form');
+        var field_id = '#' + $fileElement.attr('id').replace(/\-upload$/, '');
+        var form_build_id = $form.find('input[name="form_build_id"]').val();
+
+        // Prevent the submit button from actually submitting.
+        event.preventDefault();
+
+        // Add client side validation for the input[type=file].
+        // @todo Figure out why Drupal.file.validateExtension is not getting triggered.
+        // @todo Figure out what additional validation should be run.
+        if (typeof drupalSettings.file.elements[field_id] !== 'undefined') {
+          $('.file-upload-js-error').remove();
+          var extensionPattern = drupalSettings.file.elements[field_id].replace(/,\s*/g, '|');
+          if (extensionPattern.length > 1 && file.name.length > 0) {
+            var acceptableMatch = new RegExp('\\.(' + extensionPattern + ')$', 'gi');
+            if (!acceptableMatch.test(file.name)) {
+              var error = Drupal.t('The selected file %filename cannot be uploaded. Only files with the following extensions are allowed: %extensions.', {
+                '%filename': file.name,
+                '%extensions': extensionPattern.replace(/\|/g, ', ')
+              });
+              $(this).closest('div.js-form-managed-file').prepend('<div class="messages messages--error file-upload-js-error" aria-live="polite">' + error + '</div>');
+              // Cancel all other submit event handlers.
+              event.stopImmediatePropagation();
+              return;
+            }
+          }
+        }
+
+        // Add a progress bar.
+        var $progressBar = $(Drupal.theme.progressBar(field_id + '-progress'));
+        Drupal.flysystemS3.setCorsUploadProgress($progressBar, 0, Drupal.t('Signing @file for upload', {'@file': file.name}));
+        $fileElement.after($progressBar);
+
+        // Hide the upload field and the description.
+        $fileElement.hide();
+        $fileElement.siblings('.description').hide();
+
+        // Use the file object and ask Drupal to generate the appropriate signed
+        // request for us.
+        var signingPostData = {
+          filename: file.name,
+          filesize: file.size,
+          filemime: file.type,
+          acl: $fileElement.attr('data-s3-acl'),
+          destination: $fileElement.attr('data-s3-destination')
+        };
+
+        // POST to Drupal which will return the required parameters for signing
+        // a CORS request.
+        $.ajax({
+          url: drupalSettings.path.baseUrl + 'flysystem-s3/cors-upload-sign',
+          data: signingPostData,
+          type: 'POST',
+          error: function () {
+            Drupal.flysystemS3.setCorsUploadProgress($progressBar, 1, Drupal.t('Signing request failed. Trying secondary upload method...'));
+            // Trigger the submit button to let normal AJAX process the upload.
+            Drupal.file.triggerUploadButton(event);
+          },
+          success: function (signedFormData) {
+            Drupal.flysystemS3.setCorsUploadProgress($progressBar, 1, Drupal.t('Uploading @file', {'@file': file.name}));
+
+            // Take the signed data and construct a form out of it.
+            var uploadFormData = new FormData();
+            $.each(signedFormData.inputs, function(key, value) {
+              uploadFormData.append(key, value);
+            });
+
+            // Add the file to be uploaded.
+            uploadFormData.append('file', file);
+
+            $.ajax({
+              url: signedFormData.attributes.action,
+              data: uploadFormData,
+              type: signedFormData.attributes.method,
+              mimeType: signedFormData.attributes.enctype,
+              xhrFields: {
+                withCredentials: true
+              },
+              cache: false,
+              contentType: false,
+              processData: false,
+              xhr: function () {
+                var myXhr = $.ajaxSettings.xhr();
+                if (myXhr.upload) {
+                  myXhr.upload.addEventListener('progress', (function (event) {
+                    Drupal.flysystemS3.processCorsUploadProgress($progressBar, event);
+                  }), false);
+                }
+                return myXhr;
+              },
+              error: function (data) {
+                Drupal.flysystemS3.setCorsUploadProgress($progressBar, 1, Drupal.t('Upload failed. Trying secondary upload method...'));
+                // Trigger the submit button to let normal AJAX process the upload.
+                Drupal.file.triggerUploadButton(event);
+              },
+              complete: function (data) {
+                // Set progress bar to 100% in case the upload was so fast.
+                Drupal.flysystemS3.setCorsUploadProgress($progressBar, 100, Drupal.t('Processing upload'));
+                // Set the file upload to an empty value to prevent the file from being uploaded to Drupal.
+                $fileElement.val('');
+                // Set the fid element to our provided fid so that the AJAX response will render our file.
+                var $fidsElement = $fileElement.siblings('input[type="hidden"][name$="[fids]"]');
+                $fidsElement.val(signedFormData.fid);
+                // Trigger the submit button to let normal AJAX process the upload.
+                Drupal.file.triggerUploadButton(event);
+              }
+            });
+          }
+        });
+      }
+    },
+
+    /**
+     * Receives an XMLHttpRequestProgressEvent to display current progress.
+     *
+     * @name Drupal.flysystemS3.processCorsUploadProgress
+     *
+     * @param {jQuery} $progressBar
+     *   The progressbar element.
+     * @param event
+     *   And XMLHttpRequestProgressEvent object.
+     */
+    processCorsUploadProgress: function($progressBar, event) {
+      if (event.lengthComputable) {
+        // This is copied mostly from  Drupal.ProgressBar.setProgress
+        var percentage = Math.floor((event.loaded / event.total) * 100);
+        if (percentage >= 0 && percentage <= 100) {
+          Drupal.flysystemS3.setCorsUploadProgress($progressBar, percentage);
+        }
+        return true;
+      }
+    },
+
+    /**
+     * Update the CORS progress bar with a percent and an optional label.
+     *
+     * @name Drupal.flysystemS3.setCorsUploadProgress
+     *
+     * @param {jQuery} $progressBar
+     *   The progressbar element.
+     * @param {string} percentage
+     *   A percentage between or including 0 to 100.
+     * @param {string} [label]
+     *   An optional label
+     */
+    setCorsUploadProgress: function($progressBar, percentage, label) {
+      $progressBar.find('div.progress__bar').css('width', percentage + '%');
+      $progressBar.find('div.progress__percentage').html(percentage + '%');
+      if (label) {
+        $progressBar.find('div.progress__label').html(label);
+      }
+    }
+
+  };
+
+  /**
+   * Attach behaviors to submit uploads via CORS.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches triggers for the upload button.
+   * @prop {Drupal~behaviorDetach} detach
+   *   Detaches auto file upload trigger.
+   */
+  Drupal.behaviors.flySystemS3CorsUpload = {
+    attach: function (context) {
+      $(context).find('.js-form-managed-file.flysystem-s3-cors input[type="file"]')
+        // Add the CORS upload handler to the file input.
+        .once('auto-cors-upload')
+        .on('change.autoCorsFileUpload', Drupal.flysystemS3.submitCorsUpload)
+        // Disable the upload button trigger so that the CORS upload handler can run first.
+        .off('change.autoFileUpload', Drupal.file.triggerUploadButton);
+    },
+    detach: function (context, setting, trigger) {
+      if (trigger === 'unload') {
+        $(context).find('.js-form-managed-file.flysystem-s3-cors input[type="file"]')
+          .removeOnce('auto-cors-upload')
+          .off('change.autoCorsFileUpload', Drupal.flysystemS3.submitCorsUpload);
+      }
+    }
+  };
+
+})(jQuery, Drupal, drupalSettings);
diff --git a/flysystem_s3.libraries.yml b/flysystem_s3.libraries.yml
new file mode 100644
index 0000000..f236196
--- /dev/null
+++ b/flysystem_s3.libraries.yml
@@ -0,0 +1,7 @@
+drupal.s3_cors_upload:
+  version: VERSION
+  js:
+    flysystem_s3.js: {}
+  dependencies:
+    - file/drupal.file
+    - core/drupal.progress
diff --git a/flysystem_s3.module b/flysystem_s3.module
new file mode 100644
index 0000000..f29a769
--- /dev/null
+++ b/flysystem_s3.module
@@ -0,0 +1,10 @@
+<?php
+
+use Drupal\flysystem_s3\S3CorsManagedFileHelper;
+
+/**
+ * Implements hook_element_info_alter().
+ */
+function flysystem_s3_element_info_alter(array &$types) {
+  S3CorsManagedFileHelper::alterInfo($types);
+}
diff --git a/flysystem_s3.permissions.yml b/flysystem_s3.permissions.yml
new file mode 100644
index 0000000..b7c9702
--- /dev/null
+++ b/flysystem_s3.permissions.yml
@@ -0,0 +1,2 @@
+use S3 CORS upload:
+  title: 'Upload files directly to S3 using CORS'
diff --git a/flysystem_s3.routing.yml b/flysystem_s3.routing.yml
new file mode 100644
index 0000000..6bc7cb5
--- /dev/null
+++ b/flysystem_s3.routing.yml
@@ -0,0 +1,7 @@
+flysystem_s3.cors:
+  path: '/flysystem-s3/cors-upload-sign'
+  defaults:
+    _controller: '\Drupal\flysystem_s3\Controller\S3CorsUploadAjaxController::signRequest'
+  requirements:
+    _permission: 'use S3 CORS upload'
+    _method: 'POST'
diff --git a/src/Controller/S3CorsUploadAjaxController.php b/src/Controller/S3CorsUploadAjaxController.php
new file mode 100644
index 0000000..f8bb0fc
--- /dev/null
+++ b/src/Controller/S3CorsUploadAjaxController.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace Drupal\flysystem_s3\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\file\Entity\File;
+use Drupal\flysystem\FlysystemFactory;
+use Drupal\Core\File\FileSystemInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Aws\S3\PostObjectV4;
+
+/**
+ * Defines a controller to respond to S3 CORS upload AJAX requests.
+ */
+class S3CorsUploadAjaxController extends ControllerBase {
+
+  /**
+   * The form builder.
+   *
+   * @var \Drupal\flysystem\FlysystemFactory
+   */
+  protected $flysystemFactory;
+
+  /**
+   * The file system.
+   *
+   * @var \Drupal\Core\File\FileSystemInterface
+   */
+  protected $fileSystem;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('flysystem_factory'),
+      $container->get('file_system')
+    );
+  }
+
+  /**
+   * Constructs an S3CorsUploadAjaxController object.
+   *
+   * @param \Drupal\flysystem\FlysystemFactory $flysystem_factory
+   *   The Flysystem factory.
+   * @param \Drupal\Core\File\FileSystemInterface $file_system
+   *   The file system.
+   */
+  public function __construct(FlysystemFactory $flysystem_factory, FileSystemInterface $file_system) {
+    $this->flysystemFactory = $flysystem_factory;
+    $this->fileSystem = $file_system;
+  }
+
+  /**
+   * Returns the signed request.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   *
+   * @return \Symfony\Component\HttpFoundation\JsonResponse
+   *   A JsonResponse object.
+   */
+  public function signRequest(Request $request) {
+    $post = $request->request->all();
+
+    /** @var \Drupal\flysystem_s3\Flysystem\Adapter\S3Adapter $adapter */
+    $scheme = $this->fileSystem->uriScheme($post['destination']);
+    $adapter = $this->flysystemFactory->getPlugin($scheme)->getAdapter();
+
+    $client = $adapter->getClient();
+    $bucket = $adapter->getBucket();
+
+    $options = [
+      ['acl' => $post['acl']],
+      ['bucket' => $bucket],
+      ['starts-with', '$key', file_uri_target($post['destination']) . '/'],
+    ];
+
+    $uri = file_create_filename($post['filename'], $post['destination']);
+    $post['key'] = file_uri_target($uri);
+
+    // Create a temporary file to return with a file ID in the response.
+    $file = File::create([
+      'uri' => $uri,
+      'filesize' => $post['filesize'],
+      'filename' => $post['filename'],
+      'filemime' => $post['filemime'],
+      'uid' => \Drupal::currentUser()->getAccount()->id(),
+    ]);
+    $file->save();
+
+    // Remove values not necessary for the request to Amazon.
+    unset($post['destination']);
+    unset($post['filename']);
+    unset($post['filemime']);
+    unset($post['filesize']);
+
+    // @todo Make this interval configurable.
+    $expiration = '+5 hours';
+    $postObject = new PostObjectV4($client, $bucket, $post, $options, $expiration);
+
+    $data = [];
+    $data['attributes'] = $postObject->getFormAttributes();
+    $data['inputs'] = $postObject->getFormInputs();
+    $data['options'] = $options;
+    $data['fid'] = $file->id();
+
+    return new JsonResponse($data);
+  }
+
+}
diff --git a/src/S3CorsManagedFileHelper.php b/src/S3CorsManagedFileHelper.php
new file mode 100644
index 0000000..1f1e6a1
--- /dev/null
+++ b/src/S3CorsManagedFileHelper.php
@@ -0,0 +1,130 @@
+<?php
+
+namespace Drupal\flysystem_s3;
+
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Helper for altering and processing a managed_file element for CORS upload.
+ */
+class S3CorsManagedFileHelper {
+
+  public static function alterInfo(array &$types) {
+    array_unshift($types['managed_file']['#process'], [get_called_class(), 'preProcessCors']);
+    $types['managed_file']['#process'][] = [get_called_class(), 'postProcessCors'];
+  }
+
+  public static function preProcessCors(&$element) {
+    if (isset($element['#s3_cors']) && !$element['#s3_cors']) {
+      // S3 CORS support has been specifically disabled for this element.
+      return $element;
+    }
+
+    // Currently only single-value elements are supported.
+    if ($element['#multiple']) {
+      return $element;
+    }
+
+    // Default to off until the upload destination is confirmed to be an S3
+    // scheme, CORS support is enabled in the flysystem config, and the user
+    // has permission to upload files using CORS.
+    $element['#s3_cors'] = FALSE;
+
+    if (!empty($element['#upload_location']) && $scheme = file_uri_scheme($element['#upload_location'])) {
+      if (static::isCorsAvailable($scheme)) {
+        // @todo Verify account permission/role respected with cache tags.
+
+        // Disable the default progress indicator.
+        $element['#progress_indicator'] = 'none';
+
+        // Add a flag to the element to indicate that this is a CORS upload.
+        $element['#s3_cors'] = TRUE;
+
+        // Add a class to the element for the JS to select this element.
+        $element['#attributes']['class'][] = 'flysystem-s3-cors';
+
+        // Attach the JS library to the element conditionally.
+        $element['#attached']['library'][] = 'flysystem_s3/drupal.s3_cors_upload';
+
+        // Set the default S3 ACL if it is not already set.
+        if (!isset($element['#s3_acl'])) {
+          $element['#s3_acl'] = static::getAcl($scheme);
+        }
+      }
+    }
+
+    return $element;
+  }
+
+  public static function postProcessCors(&$element) {
+    if (!empty($element['#s3_cors'])) {
+      // Add data attributes that are used by flysystem_s3.js to submit the
+      // AJAX request to sign the upload.
+      $element['upload']['#attributes']['data-s3-acl'] = $element['#s3_acl'];
+      $element['upload']['#attributes']['data-s3-destination'] = $element['#upload_location'];
+    }
+
+    return $element;
+  }
+
+  /**
+   * Returns the settings for a Flysystem file scheme.
+   *
+   * @param string $scheme
+   *   A file scheme.
+   *
+   * @return array
+   *   The Flysystem file scheme's settings.
+   */
+  public static function getSchemeSettings($scheme) {
+    static $settings = [];
+
+    if (!isset($settings[$scheme])) {
+      /** @var \Drupal\flysystem\FlysystemFactory $factory */
+      $factory = \Drupal::service('flysystem_factory');
+      $settings[$scheme] = $factory->getSettings($scheme);
+    }
+
+    return $settings[$scheme];
+  }
+
+  /**
+   * Determines if CORS upload is available.
+   *
+   * @param string $scheme
+   *   A file scheme.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   A user account object.
+   *
+   * @return bool
+   *   TRUE if CORS upload support is available, or FALSE otherwise.
+   */
+  public static function isCorsAvailable($scheme, AccountInterface $account = NULL) {
+    if (!isset($account)) {
+      $account = \Drupal::currentUser()->getAccount();
+    }
+    $settings = static::getSchemeSettings($scheme);
+    return !empty($settings['driver']) && $settings['driver'] === 's3' && !empty($settings['config']['cors']) && $account->hasPermission('use S3 CORS upload');
+  }
+
+  /**
+   * Get the default S3 ACL setting for a file scheme.
+   *
+   * @param string $scheme
+   *   A file scheme.
+   *
+   * @return string
+   *   The S3 ACL upload setting. If not set in the scheme settings, it will
+   *   default to 'private'.
+   */
+  public static function getAcl($scheme) {
+    $settings = static::getSchemeSettings($scheme);
+    if (isset($settings['config']['options']['ACL'])) {
+      return $settings['config']['options']['ACL'];
+    }
+    else {
+      return 'private';
+    }
+  }
+
+}
