diff --git a/core/lib/Drupal/Core/Ajax/OpenDialogUrlCommand.php b/core/lib/Drupal/Core/Ajax/OpenDialogUrlCommand.php
new file mode 100644
index 0000000000..7e36a33dcd
--- /dev/null
+++ b/core/lib/Drupal/Core/Ajax/OpenDialogUrlCommand.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Drupal\Core\Ajax;
+
+use Drupal\Core\Url;
+
+class OpenDialogUrlCommand implements CommandInterface {
+
+  /**
+   * The URL to open in the modal dialog.
+   *
+   * @var string
+   */
+  protected $url;
+
+  /**
+   * Additional client-side options for the modal dialog.
+   *
+   * @var array
+   */
+  protected $dialogOptions = [
+    'width' => '70%',
+  ];
+
+  /**
+   * OpenDialogUrlCommand constructor.
+   *
+   * @param \Drupal\Core\Url|string $url
+   *   The URL to open in the modal dialog.
+   * @param array $dialog_options
+   *   (optional) Additional client-side options for the modal dialog.
+   */
+  public function __construct($url, array $dialog_options = []) {
+    $this->url = $url instanceof Url ? $url->toString() : $url;
+    if ($dialog_options) {
+      $this->dialogOptions = $dialog_options;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    return [
+      'command' => 'openDialogUrl',
+      'url' => $this->url,
+      'dialogOptions' => $this->dialogOptions,
+    ];
+  }
+
+}
diff --git a/core/misc/dialog/dialog.ajax.es6.js b/core/misc/dialog/dialog.ajax.es6.js
index 798673a1d8..6c8b44a809 100644
--- a/core/misc/dialog/dialog.ajax.es6.js
+++ b/core/misc/dialog/dialog.ajax.es6.js
@@ -95,6 +95,27 @@
   };
 
   /**
+   * Command to open an arbitrary URL in a modal dialog.
+   *
+   * @param {Drupal.Ajax} ajax
+   *   The Drupal Ajax object.
+   * @param {object} response
+   *   Object holding the server response.
+   * @param {number} [status]
+   *   The HTTP status code.
+   */
+  Drupal.AjaxCommands.prototype.openDialogUrl = function (ajax, response, status) {
+    new Drupal.Ajax(null, null, {
+      url: response.url,
+      dialogType: 'modal',
+      dialog: response.dialogOptions,
+      progress: {
+        type: 'throbber'
+      }
+    }).execute();
+  };
+
+  /**
    * Command to open a dialog.
    *
    * @param {Drupal.Ajax} ajax
diff --git a/core/misc/dialog/dialog.ajax.js b/core/misc/dialog/dialog.ajax.js
index 8277df3e51..15e11f39c4 100644
--- a/core/misc/dialog/dialog.ajax.js
+++ b/core/misc/dialog/dialog.ajax.js
@@ -59,6 +59,17 @@
     }
   };
 
+  Drupal.AjaxCommands.prototype.openDialogUrl = function (ajax, response, status) {
+    new Drupal.Ajax(null, null, {
+      url: response.url,
+      dialogType: 'modal',
+      dialog: response.dialogOptions,
+      progress: {
+        type: 'throbber'
+      }
+    }).execute();
+  };
+
   Drupal.AjaxCommands.prototype.openDialog = function (ajax, response, status) {
     if (!response.selector) {
       return false;
diff --git a/core/modules/media/config/install/core.entity_form_display.media.file.add_file.yml b/core/modules/media/config/install/core.entity_form_display.media.file.add_file.yml
new file mode 100644
index 0000000000..e2ed5cefbd
--- /dev/null
+++ b/core/modules/media/config/install/core.entity_form_display.media.file.add_file.yml
@@ -0,0 +1,34 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - core.entity_form_mode.media.add_file
+    - field.field.media.file.field_media_file
+    - media.type.file
+  module:
+    - file
+id: media.file.add_file
+targetEntityType: media
+bundle: file
+mode: add_file
+content:
+  field_media_file:
+    settings:
+      progress_indicator: throbber
+    third_party_settings: {  }
+    type: file_generic
+    weight: 1
+    region: content
+  name:
+    type: string_textfield
+    weight: 0
+    region: content
+    settings:
+      size: 60
+      placeholder: ''
+    third_party_settings: {  }
+hidden:
+  created: true
+  path: true
+  status: true
+  uid: true
diff --git a/core/modules/media/config/install/core.entity_form_display.media.image.add_file.yml b/core/modules/media/config/install/core.entity_form_display.media.image.add_file.yml
new file mode 100644
index 0000000000..75be602501
--- /dev/null
+++ b/core/modules/media/config/install/core.entity_form_display.media.image.add_file.yml
@@ -0,0 +1,36 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - core.entity_form_mode.media.add_file
+    - field.field.media.image.field_media_image
+    - image.style.thumbnail
+    - media.type.image
+  module:
+    - image
+id: media.image.add_file
+targetEntityType: media
+bundle: image
+mode: add_file
+content:
+  field_media_image:
+    settings:
+      progress_indicator: throbber
+      preview_image_style: thumbnail
+    third_party_settings: {  }
+    type: image_image
+    weight: 1
+    region: content
+  name:
+    type: string_textfield
+    weight: 0
+    region: content
+    settings:
+      size: 60
+      placeholder: ''
+    third_party_settings: {  }
+hidden:
+  created: true
+  path: true
+  status: true
+  uid: true
diff --git a/core/modules/media/config/install/core.entity_form_mode.media.add_file.yml b/core/modules/media/config/install/core.entity_form_mode.media.add_file.yml
new file mode 100644
index 0000000000..ff7eccdeb5
--- /dev/null
+++ b/core/modules/media/config/install/core.entity_form_mode.media.add_file.yml
@@ -0,0 +1,9 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - media
+id: media.add_file
+label: 'File Upload'
+targetEntityType: media
+cache: true
diff --git a/core/modules/media/config/schema/media.schema.yml b/core/modules/media/config/schema/media.schema.yml
index dad518985a..1176376da8 100644
--- a/core/modules/media/config/schema/media.schema.yml
+++ b/core/modules/media/config/schema/media.schema.yml
@@ -40,6 +40,14 @@ field.formatter.settings.media_thumbnail:
   type: field.formatter.settings.image
   label: 'Media thumbnail field display format settings'
 
+field.widget.settings.media_file:
+  type: mapping
+  label: 'File widget settings'
+  mapping:
+    open:
+      type: boolean
+      label: 'Open by default'
+
 media.source.*:
   type: mapping
   label: 'Media source settings'
diff --git a/core/modules/media/media.module b/core/modules/media/media.module
index dc2d898e3c..f89ae4a730 100644
--- a/core/modules/media/media.module
+++ b/core/modules/media/media.module
@@ -6,12 +6,15 @@
  */
 
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\field\FieldConfigInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Url;
+use Drupal\file\FileInterface;
+use Drupal\media\Form\AddFileForm;
 
 /**
  * Implements hook_help().
@@ -96,3 +99,39 @@ function template_preprocess_media(array &$variables) {
     $variables['content'][$key] = $variables['elements'][$key];
   }
 }
+
+/**
+ * Validate uploaded file for media field.
+ *
+ * @param \Drupal\file\FileInterface $file
+ *   File that should be validated.
+ *
+ * @return array
+ *   Result of validation.
+ */
+function media_validate_file_upload(FileInterface $file) {
+  $media_types = \Drupal::service('media.type_guesser.file')->getTypesForFile($file);
+
+  if (empty($media_types)) {
+    return [
+      t('%filename could not be matched to any media types.', [
+        '%filename' => $file->getFilename(),
+      ]),
+    ];
+  }
+  return [];
+}
+
+/**
+ * Implements hook_field_widget_WIDGET_TYPE_form_alter().
+ */
+function media_field_widget_file_generic_form_alter(array &$element, FormStateInterface $form_state, array $context) {
+  AddFileForm::alterFileWidget($element, $form_state);
+}
+
+/**
+ * Implements hook_field_widget_WIDGET_TYPE_form_alter().
+ */
+function media_field_widget_image_image_form_alter(array &$element, FormStateInterface $form_state, array $context) {
+  AddFileForm::alterFileWidget($element, $form_state);
+}
diff --git a/core/modules/media/media.routing.yml b/core/modules/media/media.routing.yml
index 9fbadeff29..fb7a582f4c 100644
--- a/core/modules/media/media.routing.yml
+++ b/core/modules/media/media.routing.yml
@@ -19,3 +19,16 @@ entity.media.revision:
   requirements:
     _access_media_revision: 'view'
     media: \d+
+
+entity.media.add_file:
+  path: '/media/add-file/{media_type}/{file}/{selector}'
+  defaults:
+    _entity_form: 'media.add_file'
+  options:
+    parameters:
+      media_type:
+        type: 'entity:media_type'
+      file:
+        type: 'entity:file'
+  requirements:
+    _entity_create_access: 'media'
diff --git a/core/modules/media/media.services.yml b/core/modules/media/media.services.yml
index f22f90a124..7896a10ce1 100644
--- a/core/modules/media/media.services.yml
+++ b/core/modules/media/media.services.yml
@@ -8,3 +8,8 @@ services:
     arguments: ['@entity_type.manager']
     tags:
       - { name: access_check, applies_to: _access_media_revision }
+
+  media.type_guesser.file:
+    class: Drupal\media\FileGuesser
+    # TODO: Add a cache backend.
+    arguments: ['@entity_type.manager']
diff --git a/core/modules/media/src/Entity/Media.php b/core/modules/media/src/Entity/Media.php
index 49f7283f7e..dea100180c 100644
--- a/core/modules/media/src/Entity/Media.php
+++ b/core/modules/media/src/Entity/Media.php
@@ -38,6 +38,7 @@
  *       "add" = "Drupal\media\MediaForm",
  *       "edit" = "Drupal\media\MediaForm",
  *       "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
+ *       "add_file" = "Drupal\media\Form\AddFileForm",
  *     },
  *     "translation" = "Drupal\content_translation\ContentTranslationHandler",
  *     "views_data" = "Drupal\media\MediaViewsData",
diff --git a/core/modules/media/src/FileGuesser.php b/core/modules/media/src/FileGuesser.php
new file mode 100644
index 0000000000..1f69ee7f24
--- /dev/null
+++ b/core/modules/media/src/FileGuesser.php
@@ -0,0 +1,152 @@
+<?php
+
+namespace Drupal\media;
+
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Cache\UseCacheBackendTrait;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\file\FileInterface;
+use Drupal\file\Plugin\Field\FieldType\FileItem;
+
+/**
+ * Discovers media types which support file uploads.
+ */
+class FileGuesser implements FileGuesserInterface {
+
+  use UseCacheBackendTrait;
+
+  /**
+   * Entity type manager service.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * All media types that can be created by the current user.
+   *
+   * @var \Drupal\media\MediaTypeInterface[]
+   */
+  protected $allowedTypes;
+
+  /**
+   * Constructs a new FileGuesser.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   Entity type manager.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   (optional) A cache backend.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, CacheBackendInterface $cache_backend = NULL) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->cacheBackend = $cache_backend;
+    $this->useCaches = isset($cache_backend);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function guessForFile($file, array $limit = []) {
+    $media_types = $this->getTypesForFile($file, $limit);
+    return key($media_types);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function guessForExtension($extension, array $limit = []) {
+    $media_types = $this->getTypesByExtension($extension, $limit);
+    return key($media_types);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTypesForFile($file, array $limit = []) {
+    if (is_numeric($file)) {
+      $file = $this->entityTypeManager->getStorage('file')->load($file);
+    }
+
+    if ($file instanceof FileInterface) {
+      $extension = pathinfo($file->getFilename(), PATHINFO_EXTENSION);
+
+      return $this->getTypesByExtension($extension, $limit);
+    }
+    else {
+      throw new \InvalidArgumentException('Expected file ID or entity');
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTypesByExtension($extension, array $limit = []) {
+    $cache = $this->cacheGet($extension);
+    if ($cache) {
+      return $cache->data;
+    }
+
+    $media_types = $this->getAllowedMediaTypes();
+    if ($limit) {
+      $media_types = array_intersect_key($media_types, array_flip($limit));
+    }
+
+    $filter = function (MediaTypeInterface $media_type) {
+      $item_class = $media_type
+        ->getSource()
+        ->getSourceFieldDefinition($media_type)
+        ->getItemDefinition()
+        ->getClass();
+
+      return is_a($item_class, FileItem::class, TRUE);
+    };
+    $media_types = array_filter($media_types, $filter);
+
+    $extension = Unicode::strtolower($extension);
+
+    $matches = [];
+    /** @var \Drupal\media\MediaTypeInterface $media_type */
+    foreach ($media_types as $id => $media_type) {
+      $extensions = $media_type
+        ->getSource()
+        ->getSourceFieldDefinition($media_type)
+        ->getSetting('file_extensions');
+
+      $allowed_extensions = preg_split('/\s+/', Unicode::strtolower($extensions));
+
+      if (in_array($extension, $allowed_extensions)) {
+        $matches[$id] = $media_type->label();
+      }
+    }
+    $this->cacheSet($extension, $matches);
+
+    return $matches;
+  }
+
+  /**
+   * Returns all media types that can be created by the current user.
+   *
+   * @return \Drupal\media\MediaTypeInterface[]
+   *   The media types.
+   */
+  protected function getAllowedMediaTypes() {
+    if (isset($this->allowedTypes)) {
+      return $this->allowedTypes;
+    }
+
+    $storage = $this->entityTypeManager->getStorage('media_type');
+
+    $media_types = array_filter(
+      $storage->getQuery()->execute(),
+      [
+        $this->entityTypeManager->getAccessControlHandler('media'),
+        'createAccess',
+      ]
+    );
+    $this->allowedTypes = $storage->loadMultiple($media_types);
+
+    return $this->allowedTypes;
+  }
+
+}
diff --git a/core/modules/media/src/FileGuesserInterface.php b/core/modules/media/src/FileGuesserInterface.php
new file mode 100644
index 0000000000..fa97260506
--- /dev/null
+++ b/core/modules/media/src/FileGuesserInterface.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Drupal\media;
+
+/**
+ * Discovers media types which support file uploads.
+ */
+interface FileGuesserInterface {
+
+  /**
+   * Returns the first media type ID that matches a file.
+   *
+   * @param \Drupal\file\FileInterface|int $file
+   *   A file entity or ID.
+   * @param string[] $limit
+   *   (optional) The media type IDs to choose from. If omitted, all media types
+   *   will be considered.
+   *
+   * @return string|bool
+   *   A media type ID, or FALSE if no media types matched.
+   */
+  public function guessForFile($file, array $limit = []);
+
+  /**
+   * Returns the first media type ID that matches a file extension.
+   *
+   * @param string $extension
+   *   A file extension, without the leading period.
+   * @param string[] $limit
+   *   (optional) The media type IDs to choose from. If omitted, all media types
+   *   will be considered.
+   *
+   * @return string|bool
+   *   A media type ID, or FALSE if no media types matched.
+   */
+  public function guessForExtension($extension, array $limit = []);
+
+  /**
+   * Returns all media types that match a file.
+   *
+   * @param \Drupal\file\FileInterface|int $file
+   *   A file entity or ID.
+   * @param string[] $limit
+   *   (optional) The media type IDs to choose from. If omitted, all media types
+   *   will be considered.
+   *
+   * @throws \InvalidArgumentException
+   *   If $file is not a file entity, or the ID of one.
+   *
+   * @return \Drupal\media\MediaTypeInterface[]
+   *   All matching media types.
+   */
+  public function getTypesForFile($file, array $limit = []);
+
+  /**
+   * Returns all media types that match a file extension.
+   *
+   * @param string $extension
+   *   A file extension, without the leading period.
+   * @param string[] $limit
+   *   (optional) The media type IDs to choose from. If omitted, all media types
+   *   will be considered.
+   *
+   * @return \Drupal\media\MediaTypeInterface[]
+   *   All matching media types.
+   */
+  public function getTypesByExtension($extension, array $limit = []);
+
+}
diff --git a/core/modules/media/src/Form/AddFileForm.php b/core/modules/media/src/Form/AddFileForm.php
new file mode 100644
index 0000000000..465866fa28
--- /dev/null
+++ b/core/modules/media/src/Form/AddFileForm.php
@@ -0,0 +1,317 @@
+<?php
+
+namespace Drupal\media\Form;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\BeforeCommand;
+use Drupal\Core\Ajax\CloseModalDialogCommand;
+use Drupal\Core\Ajax\InvokeCommand;
+use Drupal\Core\Ajax\OpenDialogUrlCommand;
+use Drupal\Component\Datetime\TimeInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\media\Entity\MediaType;
+use Drupal\media\MediaForm;
+use Drupal\media\FileGuesserInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+
+/**
+ * Provides 'add_file' form handler for the Media entity.
+ */
+class AddFileForm extends MediaForm {
+
+  /**
+   * The file guesser service.
+   *
+   * @var \Drupal\media\FileGuesserInterface
+   */
+  protected $fileGuesser;
+
+  /**
+   * Constructs a ContentEntityForm object.
+   *
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   *   The entity manager.
+   * @param \Drupal\media\FileGuesserInterface $file_guesser
+   *   The file guesser service.
+   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
+   *   (optional) The entity type bundle service.
+   * @param \Drupal\Component\Datetime\TimeInterface $time
+   *   (optional) The time service.
+   */
+  public function __construct(EntityManagerInterface $entity_manager, FileGuesserInterface $file_guesser, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL) {
+    parent::__construct($entity_manager, $entity_type_bundle_info, $time);
+    $this->fileGuesser = $file_guesser;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity.manager'),
+      $container->get('media.type_guesser.file'),
+      $container->get('entity_type.bundle.info'),
+      $container->get('datetime.time')
+    );
+  }
+
+  /**
+   * AJAX callback associated with the 'media type' input selector.
+   *
+   * @param array $form
+   *   The form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   *
+   * @return array
+   *   The forms 'fields' array.
+   */
+  public static function onTypeSelect(array &$form, FormStateInterface $form_state) {
+    return $form['fields'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function init(FormStateInterface $form_state) {
+    if ($form_state->hasValue('bundle')) {
+      $entity = $this->entityTypeManager
+        ->getStorage('media')
+        ->create([
+          'bundle' => $form_state->getValue('bundle'),
+        ]);
+
+      $this->setEntity($entity);
+    }
+    return parent::init($form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareEntity() {
+    parent::prepareEntity();
+
+    /** @var \Drupal\media\MediaInterface $entity */
+    $entity = $this->getEntity();
+
+    $source_field = $entity->getSource()
+      ->getSourceFieldDefinition($entity->bundle->entity)
+      ->getName();
+
+    /** @var \Drupal\file\FileInterface $file */
+    $file = $this->getRouteMatch()->getParameter('file');
+    $entity->set($source_field, $file);
+
+    // Move the file to the location specified by the source field.
+    /** @var \Drupal\file\Plugin\Field\FieldType\FileItem $item */
+    $item = $entity->get($source_field)->first();
+    $destination = $item->getUploadLocation();
+    file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+    $destination .= '/' . $file->getFilename();
+    if (!file_exists($destination)) {
+      // TODO: Use an injected file_system service for this.
+      $destination = file_unmanaged_move($file->getFileUri(), $destination);
+    }
+    $file->setFileUri($destination);
+    // TODO: The file is saved on every bundle change. We might not want that.
+    $file->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $entity = parent::validateForm($form, $form_state);
+
+    // If the bundle has changed, reinitialize the entity form. This will call
+    // init(), which will replace the current entity with a new one of the
+    // selected type.
+    $chosen_bundle = $form_state->getValue('bundle');
+    if ($chosen_bundle && $chosen_bundle !== $this->getEntity()->bundle()) {
+      $storage = &$form_state->getStorage();
+      unset($storage['entity_form_initialized']);
+    }
+
+    return $entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form['fields'] = parent::form($form, $form_state);
+    $form['fields']['#prefix'] = '<div id="entity-fields">';
+    $form['fields']['#suffix'] = '</div>';
+
+    /** @var \Drupal\media\MediaInterface $entity */
+    $entity = $this->getEntity();
+    $source_field = $entity->getSource()
+      ->getSourceFieldDefinition($entity->bundle->entity)
+      ->getName();
+    /** @var \Drupal\file\FileInterface $file */
+    $file = $entity->get($source_field)->entity;
+    $form['message'] = [
+      '#type' => 'markup',
+      '#markup' => $this->t('Choose a media type for %filename', ['%filename' => $file->getFilename()]),
+      '#weight' => -110,
+    ];
+
+    $form['bundle'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Media Type'),
+      '#required' => TRUE,
+      '#default_value' => $this->getEntity()->bundle(),
+      '#weight' => -100,
+      '#ajax' => [
+        'wrapper' => 'entity-fields',
+        'callback' => [static::class, 'onTypeSelect'],
+      ],
+      '#limit_validation_errors' => [
+        ['bundle'],
+      ],
+      '#submit' => [],
+    ];
+
+    // TODO: Currently the types are fetched based on the field settings, but
+    // since every type can have it's own upload validators, we need to account
+    // for that as well. One media type might support PDF files, while another
+    // media type doesn't.
+    $query = $this->getRequest()->query;
+    $entity_type = $query->get('entity_type');
+    $bundle = $query->get('bundle');
+    $field_name = $query->get('field_name');
+    if ($entity_type && $bundle && $field_name) {
+      $field_definitions = $this->entityManager->getFieldDefinitions($entity_type, $bundle);
+
+      if (!isset($field_definitions[$field_name])) {
+        return $form;
+      }
+
+      $allowed_types = MediaType::loadMultiple($field_definitions[$field_name]->getSetting('handler_settings')['target_bundles']);
+      foreach ($allowed_types as $media_type) {
+        /** @var \Drupal\media\Entity\MediaType $media_type */
+        $source = $media_type->getSource();
+        if ($source->getPluginId() === 'file') {
+          $form['bundle']['#options'][$media_type->id()] = $media_type->label();
+        }
+      }
+    }
+
+    // Hide revision log message field.
+    $form['fields']['revision_log_message']['#access'] = FALSE;
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function actions(array $form, FormStateInterface $form_state) {
+    $actions = parent::actions($form, $form_state);
+
+    $actions['submit']['#ajax'] = [
+      'callback' => '::onSave',
+    ];
+
+    return $actions;
+  }
+
+  /**
+   * The 'onSave' action callback.
+   *
+   * @param array $form
+   *   The form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   The AJAX response.
+   */
+  public function onSave(array &$form, FormStateInterface $form_state) {
+    $response = new AjaxResponse();
+
+    if ($form_state->getErrors()) {
+      return $response->addCommand(
+        new BeforeCommand('#entity-fields', ['#type' => 'status_messages'])
+      );
+    }
+
+    $route_match = $this->getRouteMatch();
+    $parameters = $route_match->getRawParameters()->all();
+    $query = $this->getRequest()->query;
+
+    $media_items = $query->get('media', []);
+    array_push($media_items, $this->getEntity()->id());
+
+    if ($query->has('files')) {
+      $query = [
+        'files' => $query->get('files'),
+        'media' => $media_items,
+      ];
+      $next_file_id = array_shift($query['files']);
+      $parameters['file'] = $next_file_id;
+      $parameters['media_type'] = $this->fileGuesser->guessForFile($next_file_id);
+
+      $url = Url::fromRoute($route_match->getRouteName(), $parameters, [
+        'query' => $query,
+      ]);
+      $response->addCommand(new OpenDialogUrlCommand($url));
+    }
+    else {
+      $response
+        ->addCommand(new InvokeCommand(
+          'input[data-drupal-selector="' . $parameters['selector'] . '"]',
+          'val',
+          (array) implode(',', array_map('intval', $media_items))
+        ))
+        ->addCommand(new CloseModalDialogCommand());
+    }
+    return $response;
+  }
+
+  /**
+   * Alters the field widget by hiding the remove button.
+   *
+   * @param array $element
+   *   The elements.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   */
+  public static function alterFileWidget(array &$element, FormStateInterface $form_state) {
+    $self = static::class;
+
+    if ($form_state->getFormObject() instanceof $self) {
+      $element['#process'][] = [$self, 'hideRemoveButton'];
+    }
+  }
+
+  /**
+   * Hides the remove button.
+   *
+   * @param array $element
+   *   An associative array containing the 'remove_button' render element.
+   *
+   * @return array
+   *   An associated array containing the updated render array.
+   */
+  public static function hideRemoveButton(array $element) {
+    $element['remove_button']['#access'] = FALSE;
+    return $element;
+  }
+
+  /**
+   * Checks whether the revision form fields should be added to the form.
+   *
+   * @return bool
+   *   TRUE if the form field should be added, FALSE otherwise.
+   */
+  protected function showRevisionUi() {
+    return FALSE;
+  }
+
+}
diff --git a/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php
new file mode 100644
index 0000000000..33a779d09a
--- /dev/null
+++ b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php
@@ -0,0 +1,269 @@
+<?php
+
+namespace Drupal\media\Plugin\Field\FieldWidget;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Ajax\OpenDialogUrlCommand;
+use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\WidgetBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Url;
+use Drupal\file\Element\ManagedFile;
+use Drupal\file\Plugin\Field\FieldType\FileItem;
+use Drupal\media\Entity\MediaType;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Plugin implementation of the 'media_file' widget.
+ *
+ * @FieldWidget(
+ *   id = "media_file",
+ *   label = @Translation("File"),
+ *   field_types = {
+ *     "entity_reference",
+ *   },
+ *   multiple_values = TRUE,
+ * )
+ */
+class MediaFileWidget extends WidgetBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * An associative array describing the 'managed_file' element type.
+   *
+   * As defined by the plugin.manager.element_info service.
+   *
+   * @var array
+   */
+  protected $managedFileInfo;
+
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * Constructs a WidgetBase object.
+   *
+   * @param string $plugin_id
+   *   The plugin_id for the widget.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The definition of the field to which the widget is associated.
+   * @param array $settings
+   *   The widget settings.
+   * @param array $third_party_settings
+   *   Any third party settings.
+   * @param array $managed_file_info
+   *   An associative array describing the 'managed_file' element.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   */
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, array $managed_file_info, RendererInterface $renderer) {
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
+    $this->managedFileInfo = $managed_file_info;
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $plugin_id,
+      $plugin_definition,
+      $configuration['field_definition'],
+      $configuration['settings'],
+      $configuration['third_party_settings'],
+      $container->get('element_info')->getInfo('managed_file'),
+      $container->get('renderer')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultSettings() {
+    $settings = [
+      'open' => TRUE,
+    ];
+    return $settings + parent::defaultSettings();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function isApplicable(FieldDefinitionInterface $field_definition) {
+    if ($field_definition->getType() !== 'entity_reference') {
+      return FALSE;
+    }
+
+    if ($field_definition->getFieldStorageDefinition()->getSetting('target_type') !== 'media') {
+      return FALSE;
+    }
+
+    $allowed_types = MediaType::loadMultiple($field_definition->getSetting('handler_settings')['target_bundles']);
+    foreach ($allowed_types as $media_type) {
+      /** @var \Drupal\media\Entity\MediaType $media_type */
+      $source = $media_type->getSource();
+      if ($source->getPluginId() === 'file') {
+        return TRUE;
+      }
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+    $field_definition = $items->getFieldDefinition();
+    $field_storage_definition = $field_definition->getFieldStorageDefinition();
+
+    $element = [
+      '#type' => 'details',
+      '#title' => $field_definition->getLabel(),
+      '#attributes' => [
+        'title' => $field_definition->getLabel(),
+      ],
+      '#attached' => [
+        'library' => [
+          'core/drupal.dialog.ajax',
+        ],
+      ],
+      '#open' => $this->getSetting('open'),
+    ];
+
+    $cardinality = $field_storage_definition->getCardinality();
+
+    $upload_validators = [[]];
+    $allowed_types = MediaType::loadMultiple($field_definition->getSetting('handler_settings')['target_bundles']);
+    foreach ($allowed_types as $media_type) {
+      /** @var \Drupal\media\Entity\MediaType $media_type */
+      $source = $media_type->getSource();
+      if ($source->getPluginId() === 'file') {
+        $source_data_definition = FieldItemDataDefinition::create($source->getSourceFieldDefinition($media_type));
+        $file_item = new FileItem($source_data_definition);
+        $upload_validators[] = $file_item->getUploadValidators();
+      }
+    }
+    $upload_validators = array_merge(...$upload_validators);
+
+    $file_upload_help = [
+      '#theme' => 'file_upload_help',
+      '#upload_validators' => $upload_validators,
+      '#cardinality' => $cardinality,
+    ];
+
+    $element['file'] = [
+      '#type' => 'managed_file',
+      '#multiple' => $cardinality !== 1 ? TRUE : FALSE,
+      // Hide the file element if the field is at capacity.
+      '#access' => $items->isEmpty() || ($cardinality > 0 && count($items) < $cardinality),
+      '#upload_validators' => $upload_validators + [
+        'media_validate_file_upload' => [],
+      ],
+      '#description' => $this->renderer->renderPlain($file_upload_help),
+      '#entity_type' => $field_definition->getTargetEntityTypeId(),
+      '#bundle' => $field_definition->getTargetBundle(),
+      '#field_name' => $field_definition->getName(),
+    ];
+
+    // Attach a AJAX callback using '#process' information extracted from the
+    // 'managed_file' element type.
+    $element['file']['#process'] = $this->managedFileInfo['#process'];
+    $element['file']['#process'][] = [static::class, 'processFileElement'];
+
+    $element['media_items']['#type'] = 'hidden';
+
+    return $element;
+  }
+
+  /**
+   * Adds this widget to the AJAX callback of the upload_button.
+   *
+   * @param array $element
+   *   A render array containing the upload button.
+   *
+   * @return array
+   *   An updated render array.
+   */
+  public static function processFileElement(array $element) {
+    $element['upload_button']['#ajax']['callback'][0] = static::class;
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
+    if (empty($values['media_items'])) {
+      return [];
+    }
+
+    return array_map(
+      function ($media_id) {
+        return [
+          'target_id' => (int) $media_id,
+        ];
+      },
+      explode(',', $values['media_items'])
+    );
+  }
+
+  /**
+   * Ajax callback for media_file upload forms.
+   *
+   * Augments the ManagedFile::uploadAjaxCallback() with a OpenDialogUrlCommand
+   * and its associated data.
+   *
+   * @param array $form
+   *   The form render array.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   An updated AjaxResponse.
+   */
+  public static function uploadAjaxCallback(array &$form, FormStateInterface $form_state, Request $request) {
+    $response = ManagedFile::uploadAjaxCallback($form, $form_state, $request);
+
+    $trigger = $form_state->getTriggeringElement();
+    $complete_form = $form_state->getCompleteForm();
+    $widget = NestedArray::getValue($complete_form, array_slice($trigger['#array_parents'], 0, -2));
+
+    $files = $widget['file']['#value']['fids'];
+
+    if (!$files) {
+      return $response;
+    }
+
+    $parameters = [
+      'file' => array_shift($files),
+      'selector' => $widget['media_items']['#attributes']['data-drupal-selector'],
+    ];
+    $parameters['media_type'] = \Drupal::service('media.type_guesser.file')->guessForFile($parameters['file']);
+    $options = [
+      'query' => [
+        'files' => $files,
+        'entity_type' => $widget['file']['#entity_type'],
+        'bundle' => $widget['file']['#bundle'],
+        'field_name' => $widget['file']['#field_name'],
+      ],
+    ];
+    $url = Url::fromRoute('entity.media.add_file', $parameters, $options);
+
+    return $response->addCommand(new OpenDialogUrlCommand($url));
+  }
+
+}
diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaFileWidgetTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaFileWidgetTest.php
new file mode 100644
index 0000000000..04c8a9a80a
--- /dev/null
+++ b/core/modules/media/tests/src/FunctionalJavascript/MediaFileWidgetTest.php
@@ -0,0 +1,137 @@
+<?php
+
+namespace Drupal\Tests\media\FunctionalJavascript;
+
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\media\Entity\Media;
+
+/**
+ * Tests the media_file field widget.
+ *
+ * @group media
+ */
+class MediaFileWidgetTest extends MediaJavascriptTestBase {
+
+  /**
+   * A node content type.
+   *
+   * @var \Drupal\node\NodeTypeInterface
+   */
+  protected $nodeType;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->nodeType = $this->drupalCreateContentType();
+
+    $field_storage = FieldStorageConfig::create([
+      'field_name' => 'field_media',
+      'entity_type' => 'node',
+      'type' => 'entity_reference',
+      'settings' => [
+        'target_type' => 'media',
+      ],
+      'cardinality' => 3,
+    ]);
+    $field_storage->save();
+
+    FieldConfig::create([
+      'label' => 'Media',
+      'bundle' => $this->nodeType->id(),
+      'field_storage' => $field_storage,
+      'settings' => [
+        'handler' => 'default:media',
+        'handler_settings' => [
+          'target_bundles' => NULL,
+        ],
+      ],
+    ])->save();
+
+    entity_get_form_display('node', $this->nodeType->id(), 'default')
+      ->setComponent('field_media', [
+        'type' => 'media_file',
+        'region' => 'content',
+      ])
+      ->save();
+  }
+
+  /**
+   * Tests various feature of the MediaFileWidget through the UI.
+   *
+   * By attaching it to a node and uploading a file and inspected various
+   * stages.
+   */
+  public function testWidget() {
+    $assert = $this->assertSession();
+
+    $account = $this->drupalCreateUser([
+      'create media',
+      'create ' . $this->nodeType->id() . ' content',
+      'edit own ' . $this->nodeType->id() . ' content',
+    ]);
+    $this->drupalLogin($account);
+
+    $this->drupalGet('/node/add/' . $this->nodeType->id());
+    $wrapper = $assert->elementExists('css', 'details[open][title = "Media"]');
+    // Assert that multiple-cardinality media reference fields produce only
+    // a single multiple-value file upload element.
+    /** @var \Behat\Mink\Element\NodeElement[] $file_elements */
+    $file_elements = $wrapper->findAll('css', 'input[type = "file"]');
+    $this->assertCount(1, $file_elements);
+    $file_element = reset($file_elements);
+    $this->assertSame('files[field_media_file][]', $file_element->getAttribute('name'));
+    $this->assertTrue($file_element->hasAttribute('multiple'));
+
+    // Upload a file and assert that the media form shows up as expected.
+    $this->getSession()->getPage()->attachFileToField($file_element->getAttribute('name'), __DIR__ . '/../../fixtures/example_1.jpeg');
+    $this->waitUntilVisible('#drupal-modal', 30000);
+    $form_element = $assert->elementExists('css', '#drupal-modal form');
+    $assert->fieldValueEquals('Media type', 'image', $form_element);
+    $assert->fieldExists('Name', $form_element);
+    $fid = $assert->hiddenFieldExists('inner_form[field_media_image][0][fids]', $form_element)->getValue();
+    $this->assertNotEmpty($fid);
+    $this->assertTrue(is_numeric($fid));
+    $assert->buttonExists('Save', $form_element);
+
+    $buttons = $assert->elementExists('css', '#drupal-modal + .ui-dialog-buttonpane');
+    $save_button = $assert->buttonExists('Save', $buttons);
+    $save_button->press();
+
+    $errors_selector = '#drupal-modal form .messages--error';
+    $this->waitUntilVisible($errors_selector, 30000);
+    $assert->elementExists('named', ['content', 'Name field is required.'], $form_element);
+    $assert->elementExists('named', ['content', 'Alternative text field is required.'], $form_element);
+
+    $this->getSession()->executeScript('document.querySelector("' . $errors_selector . '").remove()');
+    $form_element->fillField('Name', 'Pastafazoul');
+    $save_button->press();
+    $this->waitUntilVisible($errors_selector, 30000);
+    $assert->elementNotExists('named', ['content', 'Name field is required.'], $form_element);
+    $assert->elementExists('named', ['content', 'Alternative text field is required.'], $form_element);
+
+    // Assert that the file element is hidden if the field is at capacity.
+    $node = $this->drupalCreateNode([
+      'type' => $this->nodeType->id(),
+      'uid' => $account->id(),
+    ]);
+    $media_type = $this->createMediaType()->id();
+    for ($i = 0; $i < 3; $i++) {
+      $media = Media::create([
+        'bundle' => $media_type,
+        'field_media_test' => $this->randomString(),
+      ]);
+      $media->save();
+      $node->field_media[$i] = $media;
+    }
+    $node->save();
+
+    $this->drupalGet('/node/' . $node->id() . '/edit');
+    $wrapper = $assert->elementExists('css', 'details[open][title = "Media"]');
+    $assert->elementNotExists('css', 'input[type = "file"]', $wrapper);
+  }
+
+}
diff --git a/core/modules/media/tests/src/Kernel/MediaFileWidgetTest.php b/core/modules/media/tests/src/Kernel/MediaFileWidgetTest.php
new file mode 100644
index 0000000000..559b3628b1
--- /dev/null
+++ b/core/modules/media/tests/src/Kernel/MediaFileWidgetTest.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\Tests\media\Kernel;
+
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\media\Plugin\Field\FieldWidget\MediaFileWidget;
+
+/**
+ * @coversDefaultClass \Drupal\media\Plugin\Field\FieldWidget\MediaFileWidget
+ *
+ * @group media
+ */
+class MediaFileWidgetTest extends MediaKernelTestBase {
+
+  /**
+   * @covers ::isApplicable
+   */
+  public function testIsApplicable() {
+    $field_storage = FieldStorageConfig::create([
+      'entity_type' => 'user',
+      'field_name' => 'field_media',
+      'type' => 'entity_reference',
+      'settings' => [
+        'target_type' => 'media',
+      ],
+    ]);
+    $field_storage->save();
+
+    $field = FieldConfig::create([
+      'bundle' => 'user',
+      'field_storage' => $field_storage,
+      'settings' => [
+        'handler' => 'default:media',
+        'handler_settings' => [
+          'target_bundles' => NULL,
+        ],
+      ],
+    ]);
+    $field->save();
+
+    $this->assertTrue(MediaFileWidget::isApplicable($field));
+
+    // Assert that the widget cannot be used on a string field, or an entity
+    // reference field that does not reference media items.
+    $fields = $this->container
+      ->get('entity_field.manager')
+      ->getBaseFieldDefinitions('user');
+
+    $this->assertFalse(MediaFileWidget::isApplicable($fields['name']));
+    $this->assertFalse(MediaFileWidget::isApplicable($fields['roles']));
+  }
+
+}
