diff --git a/app/assets/javascripts/vue_shared/components/multi_step_form_template.stories.js b/app/assets/javascripts/vue_shared/components/multi_step_form_template.stories.js
new file mode 100644
index 0000000000000000000000000000000000000000..864850cefcdc3663fec7bdc83c8fdab196516d2c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/multi_step_form_template.stories.js
@@ -0,0 +1,57 @@
+import { GlButton } from '@gitlab/ui';
+import MultiStepFormTemplate from './multi_step_form_template.vue';
+
+export default {
+ component: MultiStepFormTemplate,
+ title: 'vue_shared/multi_step_form_template',
+ argTypes: {
+ title: {
+ control: 'text',
+ },
+ currentStep: {
+ control: 'number',
+ },
+ stepsTotal: {
+ control: 'number',
+ },
+ },
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { MultiStepFormTemplate, GlButton },
+ props: Object.keys(argTypes),
+ template: `
+
+
+ #form slot
+
+
+
+
+ #back slot for a back button
+
+
+
+
+ #next slot for a next button
+
+
+
+
+ #footer slot
+
+
+ `,
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ title: 'Create new project',
+ currentStep: 1,
+ stepsTotal: 2,
+};
+export const NumberOfStepsIsNotDefined = Template.bind({});
+NumberOfStepsIsNotDefined.args = {
+ title: 'Create new project',
+ currentStep: 1,
+};
diff --git a/app/assets/javascripts/vue_shared/components/multi_step_form_template.vue b/app/assets/javascripts/vue_shared/components/multi_step_form_template.vue
new file mode 100644
index 0000000000000000000000000000000000000000..c88978784150ac6f1cdb40b0687bc88270f3ada2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/multi_step_form_template.vue
@@ -0,0 +1,50 @@
+
+
+
+
diff --git a/app/assets/stylesheets/components/_index.scss b/app/assets/stylesheets/components/_index.scss
index 018b1fc49e932e3260411cac0dde9002c6c6a208..983e1236a39f17ef8d666aa33c54cce9c00283ba 100644
--- a/app/assets/stylesheets/components/_index.scss
+++ b/app/assets/stylesheets/components/_index.scss
@@ -3,6 +3,7 @@
@import './content_editor';
@import './deployment_instance';
@import './detail_page';
+@import './multi_step_form_template';
@import './multiple_choice_selector';
@import './ref_selector';
@import './related_items_list';
diff --git a/app/assets/stylesheets/components/multi_step_form_template.scss b/app/assets/stylesheets/components/multi_step_form_template.scss
new file mode 100644
index 0000000000000000000000000000000000000000..5af5618b4079400d7088a87cb6c98276c8a9441f
--- /dev/null
+++ b/app/assets/stylesheets/components/multi_step_form_template.scss
@@ -0,0 +1,3 @@
+.multi-step-form {
+ max-width: 38rem;
+}
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3f4f6eda46ff1ad27e8a26d51a61249df01ba4b6..7841051c6eab938ddc428da782368970e1793577 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -55228,9 +55228,15 @@ msgstr ""
msgid "StatusPage|your status page frontend."
msgstr ""
+msgid "Step %{currentStep}"
+msgstr ""
+
msgid "Step %{currentStep} of %{stepCount}"
msgstr ""
+msgid "Step %{currentStep} of %{stepsTotal}"
+msgstr ""
+
msgid "Step %{step}"
msgstr ""
diff --git a/spec/frontend/vue_shared/components/multi_step_form_template_spec.js b/spec/frontend/vue_shared/components/multi_step_form_template_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..86ed77d899252508e83de12e4f230c7005cb330c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/multi_step_form_template_spec.js
@@ -0,0 +1,88 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import MultiStepFormTemplate from '~/vue_shared/components/multi_step_form_template.vue';
+
+describe('MultiStepFormTemplate', () => {
+ let wrapper;
+ const defaultProps = {
+ title: 'Form title',
+ currentStep: 1,
+ };
+
+ const createComponent = (props = {}, slots) => {
+ wrapper = shallowMountExtended(MultiStepFormTemplate, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ slots,
+ });
+ };
+
+ const findTitle = () => wrapper.findByTestId('multi-step-form-title');
+ const findContent = () => wrapper.findByTestId('multi-step-form-content');
+ const findSteps = () => wrapper.findByTestId('multi-step-form-steps');
+ const findActions = () => wrapper.findByTestId('multi-step-form-action');
+ const findFooter = () => wrapper.findByTestId('multi-step-form-footer');
+
+ it('renders title', () => {
+ createComponent();
+
+ expect(findTitle().text()).toBe('Form title');
+ });
+
+ describe('step display', () => {
+ it('displays step X of N when stepsTotal is provided', () => {
+ createComponent({ stepsTotal: 2 });
+
+ expect(findSteps().text()).toBe('Step 1 of 2');
+ });
+
+ it('displays only step X when stepsTotal is not provided', () => {
+ createComponent();
+
+ expect(findSteps().text()).toBe('Step 1');
+ });
+ });
+
+ describe('slots', () => {
+ it('renders form slot content', () => {
+ createComponent({}, { form: '
Form Content
' });
+
+ expect(findContent().exists()).toBe(true);
+ expect(findContent().find('.test-form').exists()).toBe(true);
+ });
+
+ it('renders action buttons correctly when back and next slots are provided', () => {
+ createComponent(
+ {
+ currentStep: 3,
+ },
+ {
+ back: '',
+ next: '',
+ },
+ );
+
+ expect(findActions().find('button.back').exists()).toBe(true);
+ expect(findActions().find('button.next').exists()).toBe(true);
+ });
+
+ it('renders footer slot content when provided', () => {
+ createComponent(
+ {},
+ {
+ footer: '',
+ },
+ );
+
+ expect(findFooter().exists()).toBe(true);
+ expect(findFooter().find('.test-footer').exists()).toBe(true);
+ });
+
+ it('does not render footer section when no footer slot is provided', () => {
+ createComponent();
+
+ expect(findFooter().exists()).toBe(false);
+ });
+ });
+});