diff --git a/ui/app/components/wizard/index.hbs b/ui/app/components/wizard/index.hbs
new file mode 100644
index 0000000000..16420a1f85
--- /dev/null
+++ b/ui/app/components/wizard/index.hbs
@@ -0,0 +1,30 @@
+{{!
+ Copyright IBM Corp. 2016, 2025
+ SPDX-License-Identifier: BUSL-1.1
+}}
+
+{{#if @showWelcome}}
+
+ {{yield to="welcome"}}
+
+{{else}}
+
+ <:content>
+ {{yield to="quickstart"}}
+
+
+ <:submit>
+ {{#if (has-block "submit")}}
+ {{yield to="submit"}}
+ {{else}}
+
+ {{/if}}
+
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/app/components/wizard/quickstart.hbs b/ui/app/components/wizard/quickstart.hbs
new file mode 100644
index 0000000000..f984585b5a
--- /dev/null
+++ b/ui/app/components/wizard/quickstart.hbs
@@ -0,0 +1,40 @@
+{{!
+ Copyright IBM Corp. 2016, 2025
+ SPDX-License-Identifier: BUSL-1.1
+}}
+
+
+
+
+
+ {{yield to="content"}}
+
+
+
+ {{#if (gt @currentStep 0)}}
+
+ {{/if}}
+
+
+
+
+ {{#if this.isFinalStep}}
+ {{yield to="submit"}}
+ {{else}}
+
+ {{/if}}
+
+
+
+
\ No newline at end of file
diff --git a/ui/app/components/wizard/quickstart.ts b/ui/app/components/wizard/quickstart.ts
new file mode 100644
index 0000000000..f1e58e8e6b
--- /dev/null
+++ b/ui/app/components/wizard/quickstart.ts
@@ -0,0 +1,62 @@
+/**
+ * Copyright IBM Corp. 2016, 2025
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+
+/**
+ * @module QuickStart
+ * QuickStart component holds the wizard content pages and navigation controls.
+ *
+ * @example
+ *
+ */
+
+interface Args {
+ /**
+ * The active step
+ */
+ currentStep: number;
+ /**
+ * Define step information to be shown in the Stepper Nav
+ */
+ steps: { title: string; description?: string }[];
+ /**
+ * Callback to update viewing state when the wizard is exited.
+ */
+ onDismiss: CallableFunction;
+ /**
+ * Callback to update the current step when navigating backwards or
+ * forwards through the wizard
+ */
+ onStepChange: CallableFunction;
+ /**
+ * Helper arg to conditionally render a custom submit button upon
+ * completion of the wizard. Necessary to avoid a nested block error.
+ */
+ hasSubmitBlock: boolean;
+}
+
+export default class QuickStart extends Component {
+ constructor(owner: unknown, args: Args) {
+ super(owner, args);
+ }
+
+ get isFinalStep() {
+ return this.args.currentStep === this.args.steps.length - 1;
+ }
+
+ @action
+ onStepChange(change: number) {
+ const { currentStep, steps, onStepChange } = this.args;
+ const target = currentStep + change;
+
+ if (target < 0 || target > steps.length - 1) {
+ onStepChange(currentStep);
+ } else {
+ onStepChange(target);
+ }
+ }
+}
diff --git a/ui/app/styles/components/wizard.scss b/ui/app/styles/components/wizard.scss
new file mode 100644
index 0000000000..2cc5515465
--- /dev/null
+++ b/ui/app/styles/components/wizard.scss
@@ -0,0 +1,34 @@
+@use '../helper-classes/flexbox-and-grid';
+@use '../helper-classes/spacing';
+
+/**
+ * Copyright IBM Corp. 2016, 2025
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+.wizard {
+ @extend .is-flex-column;
+ @extend .is-flex-grow-1;
+ // a fixed height is needed for content to stretch properly
+ height: 1px;
+
+ .content {
+ // ensures button bar is always at the bottom even if content is not filled
+ @extend .is-flex-1;
+ overflow-y: auto;
+ }
+
+ .button-bar {
+ @extend .has-padding-m;
+ @extend .is-flex-between;
+
+ background: var(--token-color-surface-primary);
+ box-shadow:
+ 0 2px 3px 0 rgba(59, 61, 69, 0.25),
+ 0 12px 24px 0 rgba(59, 61, 69, 0.35);
+
+ .hds-button-set {
+ margin-left: auto;
+ }
+ }
+}
diff --git a/ui/app/styles/core.scss b/ui/app/styles/core.scss
index dd018ba939..e60a334ede 100644
--- a/ui/app/styles/core.scss
+++ b/ui/app/styles/core.scss
@@ -103,3 +103,4 @@
@use 'components/unseal-warning';
@use 'components/usage-page';
@use 'components/vault-loading';
+@use 'components/wizard';
diff --git a/ui/tests/helpers/general-selectors.ts b/ui/tests/helpers/general-selectors.ts
index d8713c86de..43fe55d815 100644
--- a/ui/tests/helpers/general-selectors.ts
+++ b/ui/tests/helpers/general-selectors.ts
@@ -34,6 +34,7 @@ export const GENERAL = {
confirmButton: '[data-test-confirm-button]', // used most often on modal or confirm popups
confirmTrigger: '[data-test-confirm-action-trigger]',
copyButton: '[data-test-copy-button]',
+ nextButton: '[data-test-next-button]',
revealButton: (label: string) => `[data-test-reveal="${label}"] button`, // intended for Hds::Reveal components
// there should only be one submit button per view (e.g. one per form) so this does not need to be dynamic
// this button should be used for any kind of "submit" on a form or "save" action.
diff --git a/ui/tests/integration/components/wizard-test.js b/ui/tests/integration/components/wizard-test.js
new file mode 100644
index 0000000000..ae1e453d47
--- /dev/null
+++ b/ui/tests/integration/components/wizard-test.js
@@ -0,0 +1,182 @@
+/**
+ * Copyright IBM Corp. 2016, 2025
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, click } from '@ember/test-helpers';
+import sinon from 'sinon';
+import hbs from 'htmlbars-inline-precompile';
+import { GENERAL } from 'vault/tests/helpers/general-selectors';
+
+const SELECTORS = {
+ welcome: '[data-test-welcome-content]',
+ quickstart: '[data-test-quickstart-content]',
+};
+
+module('Integration | Component | Wizard', function (hooks) {
+ setupRenderingTest(hooks);
+
+ hooks.beforeEach(function () {
+ this.steps = [
+ { title: 'First step' },
+ { title: 'Another stage' },
+ { title: 'Almost done' },
+ { title: 'Finale' },
+ ];
+ this.step = 0;
+ this.showWelcome = false;
+ this.onDismiss = sinon.spy();
+ this.onStepChange = sinon.spy();
+ });
+
+ test('it shows welcome content initially, then hides it when entering wizard', async function (assert) {
+ this.set('showWelcome', true);
+ await render(hbs`
+ <:welcome>
+ Some welcome content
+ {{!-- TODO: This will change once the welcome page structure is defined in a follow up PR --}}
+
+
+
+ <:quickstart>
+ Quickstart content
+
+ `);
+
+ // Assert welcome content is rendered and quickstart content is not
+ assert.dom(SELECTORS.welcome).exists('Welcome content is rendered initially');
+ assert.dom(SELECTORS.welcome).hasTextContaining('Some welcome content');
+ assert
+ .dom(SELECTORS.quickstart)
+ .doesNotExist('Quickstart content is not rendered when welcome is displayed');
+
+ await click('[data-test-enter-wizard-button]');
+
+ // Assert welcome content is no longer rendered and that quickstart content is rendered
+ assert.dom(SELECTORS.welcome).doesNotExist('Welcome content is hidden after entering wizard');
+ assert.dom(SELECTORS.quickstart).exists('Quickstart content is now rendered');
+ assert.dom(SELECTORS.quickstart).hasTextContaining('Quickstart content');
+ });
+
+ test('it shows custom submit block when provided', async function (assert) {
+ // Go to final step
+ this.set('step', 3);
+ this.onCustomSubmit = sinon.spy();
+
+ await render(hbs`
+ <:quickstart>
+ Quickstart content
+
+ <:submit>
+
+
+ `);
+
+ assert.dom('[data-test-custom-submit]').exists('Custom submit button is rendered');
+ assert.dom(GENERAL.submitButton).doesNotExist('Default submit button is not rendered');
+ await click('[data-test-custom-submit]');
+ assert.true(this.onCustomSubmit.calledOnce, 'Custom submit handler is called');
+ });
+
+ test('it shows default submit button when custom submit block is not provided', async function (assert) {
+ // Go to final step
+ this.set('step', 3);
+
+ await render(hbs`
+ <:quickstart>
+ Quickstart content
+
+ `);
+
+ assert
+ .dom(GENERAL.submitButton)
+ .exists('Default submit button is rendered when no custom submit provided');
+ });
+
+ test('it renders next button when not on final step', async function (assert) {
+ await render(hbs`
+ <:quickstart>
+ Quickstart content
+
+ `);
+
+ assert.dom(GENERAL.nextButton).exists('Next button is rendered when not on final step');
+ assert.dom(GENERAL.submitButton).doesNotExist('Submit button is not rendered when not on final step');
+ await click(GENERAL.nextButton);
+ assert.true(this.onStepChange.calledOnce, 'onStepChange is called');
+ // Go to final step
+ this.set('step', 3);
+ assert.dom(GENERAL.nextButton).doesNotExist('Next button is not rendered when on the final step');
+ assert.dom(GENERAL.submitButton).exists('Submit button is rendered on final step');
+ });
+
+ test('it renders back button when not on first step', async function (assert) {
+ await render(hbs`
+ <:quickstart>
+ Quickstart content
+
+ `);
+
+ assert.dom(GENERAL.backButton).doesNotExist('Back button is not rendered on the first step');
+ this.set('step', 2);
+ assert.dom(GENERAL.backButton).exists('Back button is shown when not on first step');
+ await click(GENERAL.backButton);
+ assert.true(this.onStepChange.calledOnce, 'onStepChange is called');
+ });
+
+ test('it dismisses wizard when exit button is clicked within quickstart', async function (assert) {
+ await render(hbs`
+ <:quickstart>
+ Quickstart content
+
+ `);
+
+ assert.dom(GENERAL.cancelButton).exists('Exit button is shown within quickstart');
+ await click(GENERAL.cancelButton);
+ assert.true(this.onDismiss.calledOnce, 'onDismiss is called when exit button is clicked');
+ });
+});