diff --git a/core/modules/views_ui/css/views_ui.admin.css b/core/modules/views_ui/css/views_ui.admin.css
index 2507d306578a24111649bd9b903baf54ea416d11..aca14d88bf69a58b7663c993784d79ba351eeb7e 100644
--- a/core/modules/views_ui/css/views_ui.admin.css
+++ b/core/modules/views_ui/css/views_ui.admin.css
@@ -206,3 +206,30 @@ html.js span.js-only {
 .js .views-edit-view .dropbutton-wrapper {
   width: auto;
 }
+
+/* JS moves Views action buttons under a secondary tabs container, which causes
+a large layout shift. We mitigate this by using animations to temporarily hide
+the buttons, but they will appear after a set amount of time just in case the JS
+is loaded but does not properly run. */
+@media (scripting: enabled) {
+  .views-tabs__action-list-button:not(.views-tabs--secondary *) {
+    animation-name: appear;
+    animation-duration: 0.1s;
+    /* Buttons will be hidden for the amount of time in the animation-delay if
+    not moved. Note this is the approximate time to download the views
+    aggregate CSS with slow 3G. */
+    animation-delay: 5s;
+    animation-iteration-count: 1;
+    animation-fill-mode: backwards;
+  }
+}
+
+@keyframes appear {
+  from {
+    display: none;
+  }
+
+  to {
+    display: unset;
+  }
+}
diff --git a/core/themes/claro/css/components/views-ui.css b/core/themes/claro/css/components/views-ui.css
index ae0bb5a0afee215746c66ecb8dbc8e94d0845df9..3b4a505af00f9873b9e89929b37df2516dd220cd 100644
--- a/core/themes/claro/css/components/views-ui.css
+++ b/core/themes/claro/css/components/views-ui.css
@@ -406,6 +406,34 @@ details.fieldset-no-legend {
   font-weight: normal;
 }
 
+/* JS moves Views action buttons under a secondary tabs container, which causes
+a large layout shift. We mitigate this by using animations to temporarily hide
+the buttons, but they will appear after a set amount of time just in case the JS
+is loaded but does not properly run. */
+
+@media (scripting: enabled) {
+  .views-tabs__action-list-button:not(.views-tabs--secondary *) {
+    animation-name: appear;
+    animation-duration: 0.1s;
+    /* Buttons will be hidden for the amount of time in the animation-delay if
+    not moved. Note this is the approximate time to download the views
+    aggregate CSS with slow 3G. */
+    animation-delay: 5s;
+    animation-iteration-count: 1;
+    animation-fill-mode: backwards;
+  }
+}
+
+@keyframes appear {
+  from {
+    display: none;
+  }
+
+  to {
+    display: unset;
+  }
+}
+
 /* RTL required for precedence over core's styles. */
 
 [dir="rtl"] .views-tabs__action-list-button {
diff --git a/core/themes/claro/css/components/views-ui.pcss.css b/core/themes/claro/css/components/views-ui.pcss.css
index eec7ad688ac2975f9d6f2ce0b92542ddb4e7b036..1537a1f93f307fbd2300edda5da8bf5b23a84387 100644
--- a/core/themes/claro/css/components/views-ui.pcss.css
+++ b/core/themes/claro/css/components/views-ui.pcss.css
@@ -350,6 +350,34 @@ details.fieldset-no-legend {
   background: none repeat scroll 0 0 transparent;
   font-weight: normal;
 }
+
+/* JS moves Views action buttons under a secondary tabs container, which causes
+a large layout shift. We mitigate this by using animations to temporarily hide
+the buttons, but they will appear after a set amount of time just in case the JS
+is loaded but does not properly run. */
+@media (scripting: enabled) {
+  .views-tabs__action-list-button:not(.views-tabs--secondary *) {
+    animation-name: appear;
+    animation-duration: 0.1s;
+    /* Buttons will be hidden for the amount of time in the animation-delay if
+    not moved. Note this is the approximate time to download the views
+    aggregate CSS with slow 3G. */
+    animation-delay: 5s;
+    animation-iteration-count: 1;
+    animation-fill-mode: backwards;
+  }
+}
+
+@keyframes appear {
+  from {
+    display: none;
+  }
+
+  to {
+    display: unset;
+  }
+}
+
 /* RTL required for precedence over core's styles. */
 [dir="rtl"] .views-tabs__action-list-button {
   margin: 0;