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 eeb2ac168a..5f4dd0fb79 100644
--- a/core/modules/media/media.module
+++ b/core/modules/media/media.module
@@ -5,13 +5,18 @@
  * Provides media items.
  */
 
+use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Access\AccessResult;
 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\field\FieldConfigInterface;
+use Drupal\file\FileInterface;
+use Drupal\file\Plugin\Field\FieldType\FileItem;
+use Drupal\media\Entity\MediaType;
+use Drupal\media\MediaTypeInterface;
 
 /**
  * Implements hook_help().
@@ -92,6 +97,65 @@ function media_theme_suggestions_media(array $variables) {
   return $suggestions;
 }
 
+function media_get_bundle_by_extension($extension) {
+  static $media_types;
+
+  if (!isset($media_types)) {
+    $media_types = \Drupal::entityTypeManager()
+      ->getStorage('media_type')
+      ->getQuery()
+      ->execute();
+
+    $media_types = array_filter($media_types, [
+      \Drupal::entityTypeManager()->getAccessControlHandler('media'),
+      'createAccess',
+    ]);
+
+    $media_types = MediaType::loadMultiple($media_types);
+
+    $media_types = array_filter($media_types, function (MediaTypeInterface $media_type) {
+      $item_class = $media_type
+        ->getSource()
+        ->getSourceFieldDefinition($media_type)
+        ->getItemDefinition()
+        ->getClass();
+
+      return is_a($item_class, FileItem::class, TRUE);
+    });
+  }
+
+  $extension = Unicode::strtolower($extension);
+
+  /** @var \Drupal\media\MediaTypeInterface $media_type */
+  foreach ($media_types as $media_type) {
+    $extensions = $media_type
+      ->getSource()
+      ->getSourceFieldDefinition($media_type)
+      ->getSetting('file_extensions');
+
+    $extensions = Unicode::strtolower($extensions);
+    $extensions = preg_split('/\s+/', $extensions);
+
+    if (in_array($extension, $extensions)) {
+      return $media_type->id();
+    }
+  }
+  return FALSE;
+}
+
+function media_validate_file_upload(FileInterface $file) {
+  $extension = pathinfo($file->getFilename(), PATHINFO_EXTENSION);
+
+  $type = media_get_bundle_by_extension($extension);
+  if (empty($type)) {
+    return [
+      t('%filename could not be matched to any media types.', [
+        '%filename' => $file->getFilename(),
+      ]),
+    ];
+  }
+}
+
 /**
  * Prepares variables for media templates.
  *
diff --git a/core/modules/media/src/Entity/Media.php b/core/modules/media/src/Entity/Media.php
index 5d68bdd505..5a562a485e 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\MediaFileForm",
  *     },
  *     "translation" = "Drupal\content_translation\ContentTranslationHandler",
  *     "views_data" = "Drupal\media\MediaViewsData",
diff --git a/core/modules/media/src/Form/MediaFileForm.php b/core/modules/media/src/Form/MediaFileForm.php
new file mode 100644
index 0000000000..0f628d70ce
--- /dev/null
+++ b/core/modules/media/src/Form/MediaFileForm.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Drupal\media\Form;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\media\MediaForm;
+
+class MediaFileForm extends MediaForm {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    return parent::form($form, $form_state);
+  }
+
+}
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..38d5f30eba
--- /dev/null
+++ b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php
@@ -0,0 +1,141 @@
+<?php
+
+namespace Drupal\media\Plugin\Field\FieldWidget;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Ajax\OpenModalDialogCommand;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\WidgetBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\file\Element\ManagedFile;
+use Drupal\file\Entity\File;
+use Drupal\media\Entity\Media;
+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 {
+
+  /**
+   * {@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 $field_definition->getFieldStorageDefinition()->getSetting('target_type') == 'media';
+    }
+  }
+
+  /**
+   * {@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();
+
+    $element['file'] = [
+      '#type' => 'managed_file',
+      '#multiple' => $cardinality !== $field_storage_definition::CARDINALITY_UNLIMITED,
+      // Hide the file element if the field is at capacity.
+      '#access' => $items->isEmpty() || ($cardinality > 0 && count($items) < $cardinality),
+      '#upload_validators' => [
+        'media_validate_file_upload' => [],
+      ],
+    ];
+
+    $element_info = \Drupal::service('element_info')->getInfo('managed_file');
+    $element['file']['#process'] = array_merge($element_info['#process'], [
+      [static::class, 'processFileElement'],
+    ]);
+
+    return $element;
+  }
+
+  public static function processFileElement(array $element) {
+    $element['upload_button']['#ajax']['callback'][0] = static::class;
+    $element['upload_button']['#submit'][] = [static::class, 'onFileUpload'];
+    return $element;
+  }
+
+  public static function onFileUpload(array &$form, FormStateInterface $form_state) {
+    $upload_button = $form_state->getTriggeringElement();
+    $key = $upload_button['#array_parents'];
+    array_splice($key, -1, 1, ['#value', 'fids']);
+    $fids = NestedArray::getValue($form, $key);
+
+    $media_items = [];
+    /** @var \Drupal\file\FileInterface $file */
+    foreach (File::loadMultiple($fids) as $file) {
+      $extension = pathinfo($file->getFilename(), PATHINFO_EXTENSION);
+
+      $media_item = Media::create([
+        'bundle' => media_get_bundle_by_extension($extension),
+      ]);
+      $configuration = $media_item->getSource()->getConfiguration();
+      $media_item->set($configuration['source_field'], $file);
+      $media_items[] = $media_item;
+    }
+    if ($media_items) {
+      $form_state->set('media_items', $media_items);
+    }
+  }
+
+  public static function uploadAjaxCallback(array &$form, FormStateInterface $form_state, Request $request) {
+    $response = ManagedFile::uploadAjaxCallback($form, $form_state, $request);
+
+    if ($form_state->has('media_items')) {
+      $media_items = $form_state->get('media_items');
+      /** @var \Drupal\media\MediaInterface $media_item */
+      $media_item = array_shift($media_items);
+
+      $entity_form = \Drupal::service('entity.form_builder')->getForm($media_item, 'add', [
+        'media_items' => $media_items,
+      ]);
+
+      $title = (string) t('Adding @filename', [
+        '@filename' => $media_item->getSource()->getMetadata($media_item, 'default_name'),
+      ]);
+
+      $response->addCommand(new OpenModalDialogCommand($title, $entity_form));
+    }
+
+    return $response;
+  }
+
+}
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..4b7686f247
--- /dev/null
+++ b/core/modules/media/tests/src/FunctionalJavascript/MediaFileWidgetTest.php
@@ -0,0 +1,105 @@
+<?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 {
+
+  /**
+   * @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();
+  }
+
+  public function testWidget() {
+    $assert = $this->assertSession();
+
+    $account = $this->drupalCreateUser([
+      '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'));
+
+    $this->getSession()->getPage()->attachFileToField($file_element->getAttribute('name'), __DIR__ . '/../../fixtures/example_1.jpeg');
+    $this->waitUntilVisible('#drupal-modal', 10000);
+    $assert->elementExists('css', '#drupal-modal form');
+
+    // 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']));
+  }
+
+}
