diff --git a/core/modules/views/config/schema/views.style.schema.yml b/core/modules/views/config/schema/views.style.schema.yml
index f31f67ac437bfa8d561962d6a298ab1328be0d2b..b2d7fdbe00e4e4580ccdd182a3f948ca7e3b644b 100644
--- a/core/modules/views/config/schema/views.style.schema.yml
+++ b/core/modules/views/config/schema/views.style.schema.yml
@@ -48,6 +48,23 @@ views.style.grid:
       type: boolean
       label: 'Default views column classes'
 
+views.style.grid_responsive:
+  type: views_style
+  label: 'Grid - Responsive'
+  mapping:
+    columns:
+      type:  integer
+      label: 'Maximum number of columns'
+    cell_min_width:
+      type:  integer
+      label: 'Minimum cell width'
+    grid_gutter:
+      type:  integer
+      label: 'Grid gutter'
+    alignment:
+      type: string
+      label: 'Alignment'
+
 views.style.table:
   type: views_style
   label: 'Table'
diff --git a/core/modules/views/css/views-responsive-grid.css b/core/modules/views/css/views-responsive-grid.css
new file mode 100644
index 0000000000000000000000000000000000000000..352837182441d9a6e3a7ae42fedd0e09cff6302d
--- /dev/null
+++ b/core/modules/views/css/views-responsive-grid.css
@@ -0,0 +1,35 @@
+/**
+ * CSS for Views responsive grid style.
+ */
+
+.views-view-responsive-grid {
+  --views-responsive-grid--layout-gap: 10px; /* Will be overridden by an inline style. */
+  --views-responsive-grid--column-count: 4; /* Will be overridden by an inline style. */
+  --views-responsive-grid--cell-min-width: 100px; /* Will be overridden by an inline style. */
+}
+
+.views-view-responsive-grid--horizontal {
+  /**
+   * Calculated values.
+   */
+  --views-responsive-grid--gap-count: calc(var(--views-responsive-grid--column-count) - 1);
+  --views-responsive-grid--total-gap-width: calc(var(--views-responsive-grid--gap-count) * var(--views-responsive-grid--layout-gap));
+  --views-responsive-grid-item--max-width: calc((100% - var(--views-responsive-grid--total-gap-width)) / var(--views-responsive-grid--column-count));
+
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(max(var(--views-responsive-grid--cell-min-width), var(--views-responsive-grid-item--max-width)), 1fr));
+  gap: var(--views-responsive-grid--layout-gap);
+}
+
+.views-view-responsive-grid--vertical {
+  margin-bottom: calc(var(--views-responsive-grid--layout-gap) * -1); /* Offset the bottom row's padding. */
+  column-width: var(--views-responsive-grid--cell-min-width);
+  column-count: var(--views-responsive-grid--column-count);
+  column-gap: var(--views-responsive-grid--layout-gap);
+}
+
+.views-view-responsive-grid--vertical .views-view-responsive-grid__item > * {
+  padding-bottom: var(--views-responsive-grid--layout-gap);
+  page-break-inside: avoid;
+  break-inside: avoid;
+}
diff --git a/core/modules/views/src/Plugin/views/style/GridResponsive.php b/core/modules/views/src/Plugin/views/style/GridResponsive.php
new file mode 100644
index 0000000000000000000000000000000000000000..f9b9b820c54b58d5d84ed9714a74df6dd8d86fb6
--- /dev/null
+++ b/core/modules/views/src/Plugin/views/style/GridResponsive.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace Drupal\views\Plugin\views\style;
+
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Style plugin to render each item in a responsive grid cell.
+ *
+ * @ingroup views_style_plugins
+ *
+ * @ViewsStyle(
+ *   id = "grid_responsive",
+ *   title = @Translation("Responsive Grid"),
+ *   help = @Translation("Displays rows in a responsive grid."),
+ *   theme = "views_view_grid_responsive",
+ *   display_types = {"normal"}
+ * )
+ */
+class GridResponsive extends StylePluginBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $usesRowPlugin = TRUE;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function defineOptions() {
+    $options = parent::defineOptions();
+    $options['columns'] = ['default' => '4'];
+    $options['cell_min_width'] = ['default' => '100'];
+    $options['grid_gutter'] = ['default' => '10'];
+    $options['alignment'] = ['default' => 'horizontal'];
+    return $options;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+    parent::buildOptionsForm($form, $form_state);
+    $form['columns'] = [
+      '#type' => 'number',
+      '#title' => $this->t('Maximum number of columns'),
+      '#attributes' => ['style' => 'width: 6em;'],
+      '#description' => $this->t('The maximum number of columns that will be displayed within the grid.'),
+      '#default_value' => $this->options['columns'],
+      '#required' => TRUE,
+      '#min' => 1,
+    ];
+    $form['cell_min_width'] = [
+      '#type' => 'number',
+      '#title' => $this->t('Minimum grid cell width'),
+      '#field_suffix' => 'px',
+      '#attributes' => ['style' => 'width: 6em;'],
+      '#description' => $this->t('The minimum width of the grid cells. If the grid container becomes narrow, the grid cells will reflow onto the next row as needed.'),
+      '#default_value' => $this->options['cell_min_width'],
+      '#required' => TRUE,
+      '#min' => 1,
+    ];
+    $form['grid_gutter'] = [
+      '#type' => 'number',
+      '#title' => $this->t('Grid gutter spacing'),
+      '#field_suffix' => 'px',
+      '#attributes' => ['style' => 'width: 6em;'],
+      '#description' => $this->t('The spacing between the grid cells.'),
+      '#default_value' => $this->options['grid_gutter'],
+      '#required' => TRUE,
+      '#min' => 0,
+    ];
+    $form['alignment'] = [
+      '#type' => 'radios',
+      '#title' => $this->t('Alignment'),
+      '#options' => ['horizontal' => $this->t('Horizontal'), 'vertical' => $this->t('Vertical')],
+      '#default_value' => $this->options['alignment'],
+      '#description' => $this->t('Horizontal alignment will place items starting in the upper left and moving right. Vertical alignment will place items starting in the upper left and moving down.'),
+    ];
+  }
+
+}
diff --git a/core/modules/views/templates/views-view-grid-responsive.html.twig b/core/modules/views/templates/views-view-grid-responsive.html.twig
new file mode 100644
index 0000000000000000000000000000000000000000..d36a1bf722897d1e9ae49d0434f88162b79f4998
--- /dev/null
+++ b/core/modules/views/templates/views-view-grid-responsive.html.twig
@@ -0,0 +1,55 @@
+{#
+/**
+ * @file
+ * Default theme implementation for views to display rows in a responsive grid.
+ *
+ * Available variables:
+ * - attributes: HTML attributes for the wrapping element.
+ * - title: The title of this group of rows.
+ * - view: The view object.
+ * - rows: The rows contained in this view.
+ * - options: The view plugin style options.
+ *   - alignment: a string set to either 'horizontal' or 'vertical'.
+ *   - columns: A number representing the max number of columns.
+ *   - cell_min_width: A number representing the minimum width of the grid cell.
+ *   - grid_gutter: A number representing the space between the grid cells.
+ * - items: A list of grid items.
+ *   - attributes: HTML attributes for each row or column.
+ *   - content: A list of columns or rows. Each row or column contains:
+ *     - attributes: HTML attributes for each row or column.
+ *     - content: The row or column contents.
+ *
+ * @see template_preprocess_views_view_grid_responsive()
+ *
+ * @ingroup themeable
+ */
+#}
+
+{{ attach_library('views/views.responsive-grid') }}
+
+{%
+  set classes = [
+    'views-view-responsive-grid',
+    'views-view-responsive-grid--' ~ options.alignment,
+  ]
+%}
+
+{% set responsive_grid_styles = [
+    '--views-responsive-grid--column-count:'  ~ options.columns ~ ';',
+    '--views-responsive-grid--cell-min-width:'  ~ options.cell_min_width ~ 'px;',
+    '--views-responsive-grid--layout-gap:'  ~ options.grid_gutter ~ 'px;',
+  ]
+%}
+
+{% if title %}
+  <h3>{{ title }}</h3>
+{% endif %}
+<div{{ attributes.addClass(classes).setAttribute('style', responsive_grid_styles|join()) }}>
+  {% for item in items %}
+    <div class="views-view-responsive-grid__item">
+      <div class="views-view-responsive-grid__item-inner">
+        {{- item.content -}}
+      </div>
+    </div>
+  {% endfor %}
+</div>
diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_grid_responsive.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_grid_responsive.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3a1b395e55a3409829b97f345546898fbd9c68df
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_grid_responsive.yml
@@ -0,0 +1,65 @@
+langcode: en
+status: true
+dependencies: {  }
+id: test_grid_responsive
+label: ''
+module: views
+description: ''
+tag: ''
+base_table: views_test_data
+base_field: nid
+display:
+  default:
+    display_options:
+      defaults:
+        fields: false
+        pager: false
+        sorts: false
+      fields:
+        age:
+          field: age
+          id: age
+          relationship: none
+          table: views_test_data
+          plugin_id: numeric
+        id:
+          field: id
+          id: id
+          relationship: none
+          table: views_test_data
+          plugin_id: numeric
+        name:
+          field: name
+          id: name
+          relationship: none
+          table: views_test_data
+          plugin_id: string
+      pager:
+        options:
+          offset: 0
+        type: none
+      sorts:
+        id:
+          field: id
+          id: id
+          order: ASC
+          relationship: none
+          table: views_test_data
+          plugin_id: numeric
+      style:
+        type: grid_responsive
+        options:
+          grouping: {  }
+      row:
+        type: fields
+    display_plugin: default
+    display_title: Default
+    id: default
+    position: 0
+  page_1:
+    display_options:
+      path: test-grid
+    display_plugin: page
+    display_title: 'Page display'
+    id: page_1
+    position: 1
diff --git a/core/modules/views/tests/src/Kernel/Plugin/StyleGridResponsiveTest.php b/core/modules/views/tests/src/Kernel/Plugin/StyleGridResponsiveTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..61d1d225adb8f6a7ecc0ecc5a49d13ec7c14305e
--- /dev/null
+++ b/core/modules/views/tests/src/Kernel/Plugin/StyleGridResponsiveTest.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace Drupal\Tests\views\Kernel\Plugin;
+
+use Drupal\views\Views;
+
+/**
+ * Tests the grid_responsive style plugin.
+ *
+ * @group views
+ * @see \Drupal\views\Plugin\views\style\GridResponsive
+ */
+class StyleGridResponsiveTest extends PluginKernelTestBase {
+
+  /**
+   * Views used by this test.
+   *
+   * @var array
+   */
+  public static $testViews = ['test_grid_responsive'];
+
+  /**
+   * Generates a grid_responsive and asserts that it is displaying correctly.
+   *
+   * @param array $options
+   *   Options for the style plugin.
+   * @param array $expected
+   *   Expected values sued for assertions.
+   *
+   * @dataProvider providerTestResponsiveGrid
+   */
+  public function testResponsiveGrid(array $options, array $expected): void {
+    // Create and preview a View with the provided options.
+    $view = Views::getView('test_grid_responsive');
+    $view->setDisplay('default');
+    $view->initStyle();
+    $view->initHandlers();
+    $view->initQuery();
+    $view->style_plugin->options = $options + $view->style_plugin->options;
+    $this->executeView($view);
+    $output = $view->preview();
+    $output = \Drupal::service('renderer')->renderRoot($output);
+    $this->setRawContent($output);
+
+    // Confirm that the alignment class is added.
+    $result = $this->xpath('//div[contains(@class, "views-view-responsive-grid") and contains(@class, :alignment)]', [':alignment' => 'views-view-responsive-grid--' . $expected['alignment']]);
+    $this->assertGreaterThan(0, count($result), "Alignment CSS variable value is detected and correct.");
+
+    // Check for CSS variables in style attribute.
+    $result = $this->xpath('//div[contains(@class, "views-view-responsive-grid") and contains(@style, :columns)]', [':columns' => '--views-responsive-grid--column-count:' . $expected['columns']]);
+    $this->assertGreaterThan(0, count($result), "Max-columns CSS variable value is detected and correct.");
+    $result = $this->xpath('//div[contains(@class, "views-view-responsive-grid") and contains(@style, :min-width)]', [':min-width' => '--views-responsive-grid--cell-min-width:' . $expected['cell_min_width'] . 'px']);
+    $this->assertGreaterThan(0, count($result), "Min-width CSS variable value is detected and correct.");
+    $result = $this->xpath('//div[contains(@class, "views-view-responsive-grid") and contains(@style, :gutter)]', [':gutter' => '--views-responsive-grid--layout-gap:' . $expected['grid_gutter'] . 'px']);
+    $this->assertGreaterThan(0, count($result), "Gutter CSS variable value is detected and correct.");
+
+    // Assert that the correct number of elements have been rendered and that
+    // markup structure is correct.
+    $result = $this->xpath('//div[contains(@class, "views-view-responsive-grid")]/div[contains(@class, "views-view-responsive-grid__item")]/div[contains(@class, "views-view-responsive-grid__item-inner")]');
+    // There are five results for this test view. See ViewTestData::dataSet().
+    $expected_count = 5;
+    $this->assertSame($expected_count, count($result), "The expected number of items are rendered in the correct structure.");
+  }
+
+  /**
+   * Data provider for testing various configurations.
+   *
+   * @return array
+   *   Array containing options for the style plugin and expected values.
+   */
+  public function providerTestResponsiveGrid() {
+    return [
+      'horizontal' => [
+        'settings' => [
+          'columns' => 7,
+          'cell_min_width' => 123,
+          'grid_gutter' => 13,
+          'alignment' => 'horizontal',
+        ],
+        'expected' => [
+          'columns' => 7,
+          'cell_min_width' => 123,
+          'grid_gutter' => 13,
+          'alignment' => 'horizontal',
+        ],
+      ],
+      'vertical' => [
+        'settings' => [
+          'columns' => 8,
+          'cell_min_width' => 50,
+          'grid_gutter' => 44,
+          'alignment' => 'vertical',
+        ],
+        'expected' => [
+          'columns' => 8,
+          'cell_min_width' => 50,
+          'grid_gutter' => 44,
+          'alignment' => 'vertical',
+        ],
+      ],
+      'default options' => [
+        'settings' => [],
+        'expected' => [
+          'columns' => 4,
+          'cell_min_width' => 100,
+          'grid_gutter' => 10,
+          'alignment' => 'horizontal',
+        ],
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/views/views.libraries.yml b/core/modules/views/views.libraries.yml
index 23910062948e9c9a49b7ac4268cc8d78c7e09d1d..08ff2d1640798b7f9e862b32074392f2f392d7ae 100644
--- a/core/modules/views/views.libraries.yml
+++ b/core/modules/views/views.libraries.yml
@@ -16,3 +16,9 @@ views.ajax:
     - core/once
     - core/internal.jquery.form
     - core/drupal.ajax
+
+views.responsive-grid:
+  version: VERSION
+  css:
+    layout:
+      css/views-responsive-grid.css: {}
diff --git a/core/modules/views/views.theme.inc b/core/modules/views/views.theme.inc
index d2cb65e402e4d031410fd1bb0aed12754ee048fd..b332d08f43451b421b4ea25d023014bfce715b17 100644
--- a/core/modules/views/views.theme.inc
+++ b/core/modules/views/views.theme.inc
@@ -794,6 +794,37 @@ function template_preprocess_views_view_grid(&$variables) {
   $variables['items'] = $items;
 }
 
+/**
+ * Prepares variables for views grid - responsive style templates.
+ *
+ * Default template: views-view-grid-responsive.html.twig.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - view: The view object.
+ *   - rows: An array of row items. Each row is an array of content.
+ */
+function template_preprocess_views_view_grid_responsive(&$variables) {
+  $variables['options'] = $variables['view']->style_plugin->options;
+  $view = $variables['view'];
+
+  $items = [];
+
+  foreach ($variables['rows'] as $id => $item) {
+
+    $attribute = new Attribute();
+    if ($row_class = $view->style_plugin->getRowClass($id)) {
+      $attribute->addClass($row_class);
+    }
+    $items[$id] = [
+      'content' => $item,
+      'attributes' => $attribute,
+    ];
+  }
+
+  $variables['items'] = $items;
+}
+
 /**
  * Prepares variables for views unformatted rows templates.
  *
diff --git a/core/tests/Drupal/KernelTests/Core/Theme/Stable9LibraryOverrideTest.php b/core/tests/Drupal/KernelTests/Core/Theme/Stable9LibraryOverrideTest.php
index 4df70246c169b5b7ade1da6ff5c43c583173fe8c..ccccf3dac28ce9a0f9eacad7c047b74b10d58ac9 100644
--- a/core/tests/Drupal/KernelTests/Core/Theme/Stable9LibraryOverrideTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Theme/Stable9LibraryOverrideTest.php
@@ -9,6 +9,15 @@
  */
 class Stable9LibraryOverrideTest extends StableLibraryOverrideTestBase {
 
+  /**
+   * A list of libraries to skip checking, in the format extension/library_name.
+   *
+   * @var string[]
+   */
+  protected $librariesToSkip = [
+    'views/views.responsive-grid',
+  ];
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/tests/Drupal/KernelTests/Core/Theme/Stable9TemplateOverrideTest.php b/core/tests/Drupal/KernelTests/Core/Theme/Stable9TemplateOverrideTest.php
index b4deeefd6e1bdc9377cf5c580a5018f9d617b8e1..486fe12e646e55a9ee1f2b4363bf63b184a9288d 100644
--- a/core/tests/Drupal/KernelTests/Core/Theme/Stable9TemplateOverrideTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Theme/Stable9TemplateOverrideTest.php
@@ -27,6 +27,7 @@ class Stable9TemplateOverrideTest extends KernelTestBase {
     // Registered as a template in the views_theme() function in views.module
     // but an actual template does not exist.
     'views-form-views-form',
+    'views-view-grid-responsive',
   ];
 
   /**
diff --git a/core/tests/Drupal/KernelTests/Core/Theme/StableLibraryOverrideTest.php b/core/tests/Drupal/KernelTests/Core/Theme/StableLibraryOverrideTest.php
index c06ce24755daef14030e66df15ec0016acf77d3f..167b9eb2877ec2b088c4ebc50c2f186c3bd3aa2d 100644
--- a/core/tests/Drupal/KernelTests/Core/Theme/StableLibraryOverrideTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Theme/StableLibraryOverrideTest.php
@@ -42,7 +42,9 @@ class StableLibraryOverrideTest extends StableLibraryOverrideTestBase {
    *
    * @var string[]
    */
-  protected $librariesToSkip = [];
+  protected $librariesToSkip = [
+    'views/views.responsive-grid',
+  ];
 
   /**
    * {@inheritdoc}
diff --git a/core/tests/Drupal/KernelTests/Core/Theme/StableTemplateOverrideTest.php b/core/tests/Drupal/KernelTests/Core/Theme/StableTemplateOverrideTest.php
index cb01c58a1205d5765b09bfda94f6285fcdef0fbe..1dd98306f2cbcd304d6b5988026d62bec6d81473 100644
--- a/core/tests/Drupal/KernelTests/Core/Theme/StableTemplateOverrideTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Theme/StableTemplateOverrideTest.php
@@ -25,6 +25,7 @@ class StableTemplateOverrideTest extends KernelTestBase {
    */
   protected $templatesToSkip = [
     'views-form-views-form',
+    'views-view-grid-responsive',
   ];
 
   /**