diff --git a/changelog/23921.txt b/changelog/23921.txt
new file mode 100644
index 000000000000..cd03142227d0
--- /dev/null
+++ b/changelog/23921.txt
@@ -0,0 +1,3 @@
+```release-note:bug
+ui: show error from API when seal fails
+```
diff --git a/ui/app/components/seal-action.hbs b/ui/app/components/seal-action.hbs
new file mode 100644
index 000000000000..ae966e676f6f
--- /dev/null
+++ b/ui/app/components/seal-action.hbs
@@ -0,0 +1,34 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+~}}
+
+
+ {{#if this.error}}
+
+ Error
+
+ {{this.error}}
+
+
+ {{/if}}
+
+ Sealing a vault tells the Vault server to stop responding to any access operations until it is unsealed again. A sealed
+ vault throws away its root key to unlock the data, so it physically is blocked from responding to operations again until
+ the Vault is unsealed again with the "unseal" command or via the API.
+
+
+
+
+
+ Seal
+
+
\ No newline at end of file
diff --git a/ui/app/components/seal-action.js b/ui/app/components/seal-action.js
new file mode 100644
index 000000000000..4a2fd873ae47
--- /dev/null
+++ b/ui/app/components/seal-action.js
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { action } from '@ember/object';
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import errorMessage from 'vault/utils/error-message';
+
+export default class SealActionComponent extends Component {
+ @tracked error;
+
+ @action
+ async handleSeal() {
+ try {
+ await this.args.onSeal();
+ } catch (e) {
+ this.error = errorMessage(e, 'Seal attempt failed. Check Vault logs for details.');
+ }
+ }
+}
diff --git a/ui/app/templates/vault/cluster/settings/seal.hbs b/ui/app/templates/vault/cluster/settings/seal.hbs
index 972f9a72cf85..0b6d738e3f08 100644
--- a/ui/app/templates/vault/cluster/settings/seal.hbs
+++ b/ui/app/templates/vault/cluster/settings/seal.hbs
@@ -12,26 +12,7 @@
{{#if this.model.seal.canUpdate}}
-
-
- Sealing a vault tells the Vault server to stop responding to any access operations until it is unsealed again. A sealed
- vault throws away its root key to unlock the data, so it physically is blocked from responding to operations again
- until the Vault is unsealed again with the "unseal" command or via the API.
-
-
-
-
- Seal
-
-
+
{{else}}
{{/if}}
\ No newline at end of file
diff --git a/ui/tests/integration/components/seal-action-test.js b/ui/tests/integration/components/seal-action-test.js
new file mode 100644
index 000000000000..19bcf382048c
--- /dev/null
+++ b/ui/tests/integration/components/seal-action-test.js
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'vault/tests/helpers';
+import { click, render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+import sinon from 'sinon';
+
+const SEAL_WHEN_STANDBY_MSG = 'vault cannot seal when in standby mode; please restart instead';
+
+module('Integration | Component | seal-action', function (hooks) {
+ setupRenderingTest(hooks);
+
+ hooks.beforeEach(function () {
+ this.sealSuccess = sinon.spy(() => new Promise((resolve) => resolve({})));
+ this.sealError = sinon.stub().throws({ message: SEAL_WHEN_STANDBY_MSG });
+ });
+
+ test('it handles success', async function (assert) {
+ this.set('handleSeal', this.sealSuccess);
+ await render(hbs``);
+
+ // attempt seal
+ await click('[data-test-seal] button');
+ await click('[data-test-confirm-button]');
+
+ assert.ok(this.sealSuccess.calledOnce, 'called onSeal action');
+ assert.dom('[data-test-seal-error]').doesNotExist('Does not show error when successful');
+ });
+
+ test('it handles error', async function (assert) {
+ this.set('handleSeal', this.sealError);
+ await render(hbs``);
+
+ // attempt seal
+ await click('[data-test-seal] button');
+ await click('[data-test-confirm-button]');
+
+ assert.ok(this.sealError.calledOnce, 'called onSeal action');
+ assert.dom('[data-test-seal-error]').includesText(SEAL_WHEN_STANDBY_MSG, 'Shows error returned from API');
+ });
+});