diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue
index 719e699910303363bc8ff09320e99e1cb4d4335c..12c03dc7a850f2c59c8a694f8ac9783b4259e4c4 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue
@@ -48,7 +48,7 @@ export default {
       'PipelineSchedules|Successfully scheduled a pipeline to run. Go to the %{linkStart}Pipelines page%{linkEnd} for details. ',
     ),
     planLimitReachedMsg: s__(
-      'PipelineSchedules|You have exceeded the maximum number of pipeline schedules for your plan. To create a new schedule, either increase your plan limit or delete an exisiting schedule.',
+      'PipelineSchedules|You have exceeded the maximum number of pipeline schedules for your plan. To create a new schedule, either increase your plan limit or delete an existing schedule.',
     ),
     planLimitReachedBtnText: s__('PipelineSchedules|Explore plan limits'),
   },
@@ -183,11 +183,23 @@ export default {
     nextPage() {
       return Number(this.schedules?.pageInfo?.hasNextPage);
     },
+    // if limit is null, then user does not have access to create schedule
+    hasNoAccess() {
+      return this.schedules?.planLimit === null;
+    },
+    // if limit is 0, then schedule creation is unlimited
+    hasUnlimitedSchedules() {
+      return this.schedules?.planLimit === 0;
+    },
+    // if limit is x, then schedule creation is limited
     hasReachedPlanLimit() {
       return this.schedules?.count >= this.schedules?.planLimit;
     },
-    hasPlanLimit() {
-      return this.schedules?.planLimit;
+    shouldShowLimitReachedAlert() {
+      return !this.hasUnlimitedSchedules && this.hasReachedPlanLimit && !this.hasNoAccess;
+    },
+    shouldDisableNewScheduleBtn() {
+      return (this.hasReachedPlanLimit || this.hasNoAccess) && !this.hasUnlimitedSchedules;
     },
   },
   watch: {
@@ -339,7 +351,7 @@ export default {
     </gl-alert>
 
     <gl-alert
-      v-if="hasReachedPlanLimit && hasPlanLimit"
+      v-if="shouldShowLimitReachedAlert"
       class="gl-my-3"
       variant="warning"
       :dismissible="false"
@@ -408,7 +420,7 @@ export default {
           :href="newSchedulePath"
           class="gl-ml-auto"
           variant="confirm"
-          :disabled="hasReachedPlanLimit"
+          :disabled="shouldDisableNewScheduleBtn"
           data-testid="new-schedule-button"
         >
           {{ $options.i18n.newSchedule }}
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 84296f0e2a4b11f20ed671b17ec4c2dfe6051267..2a8405b2f114cf1b245df0f61f79453d87c7462e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -36329,7 +36329,7 @@ msgstr ""
 msgid "PipelineSchedules|There was a problem taking ownership of the pipeline schedule."
 msgstr ""
 
-msgid "PipelineSchedules|You have exceeded the maximum number of pipeline schedules for your plan. To create a new schedule, either increase your plan limit or delete an exisiting schedule."
+msgid "PipelineSchedules|You have exceeded the maximum number of pipeline schedules for your plan. To create a new schedule, either increase your plan limit or delete an existing schedule."
 msgstr ""
 
 msgid "PipelineSource|API"
diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
index 1633c6a509e30a4827a8888009105db388020a74..5dd6dec66b5f920ee52c463186484cb9000674a8 100644
--- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
@@ -25,6 +25,7 @@ import {
   emptyPipelineSchedulesResponse,
   mockPipelineSchedulesResponseWithPagination,
   mockPipelineSchedulesResponsePlanLimitReached,
+  mockPipelineSchedulesResponseUnlimited,
   noPlanLimitResponse,
 } from '../mock_data';
 
@@ -46,6 +47,9 @@ describe('Pipeline schedules app', () => {
     .fn()
     .mockResolvedValue(mockPipelineSchedulesResponsePlanLimitReached);
   const noPlanLimitHandler = jest.fn().mockResolvedValue(noPlanLimitResponse);
+  const unlimitedSchedulesHandler = jest
+    .fn()
+    .mockResolvedValue(mockPipelineSchedulesResponseUnlimited);
   const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
 
   const deleteMutationHandlerSuccess = jest.fn().mockResolvedValue(deleteMutationResponse);
@@ -452,29 +456,20 @@ describe('Pipeline schedules app', () => {
     });
   });
 
-  describe('plan limit reached', () => {
-    beforeEach(async () => {
-      createComponent([[getPipelineSchedulesQuery, planLimitReachedHandler]]);
+  it.each`
+    description        | handler                      | buttonDisabled | alertExists
+    ${'limit reached'} | ${planLimitReachedHandler}   | ${true}        | ${true}
+    ${'no access'}     | ${noPlanLimitHandler}        | ${true}        | ${false}
+    ${'unlimited'}     | ${unlimitedSchedulesHandler} | ${false}       | ${false}
+  `(
+    'Alert should show: $alertExists and button should be disabled: $buttonDisabled when plan limit: $description',
+    async ({ handler, buttonDisabled, alertExists }) => {
+      createComponent([[getPipelineSchedulesQuery, handler]]);
 
       await waitForPromises();
-    });
 
-    it('shows disabled new schedule button with alert', () => {
-      expect(findNewButton().props('disabled')).toBe(true);
-      expect(findPlanLimitReachedAlert().exists()).toBe(true);
-    });
-  });
-
-  describe('no plan limit', () => {
-    beforeEach(async () => {
-      createComponent([[getPipelineSchedulesQuery, noPlanLimitHandler]]);
-
-      await waitForPromises();
-    });
-
-    it('shows disabled new schedule button', () => {
-      expect(findNewButton().props('disabled')).toBe(true);
-      expect(findPlanLimitReachedAlert().exists()).toBe(false);
-    });
-  });
+      expect(findNewButton().props('disabled')).toBe(buttonDisabled);
+      expect(findPlanLimitReachedAlert().exists()).toBe(alertExists);
+    },
+  );
 });
diff --git a/spec/frontend/ci/pipeline_schedules/mock_data.js b/spec/frontend/ci/pipeline_schedules/mock_data.js
index 7f640646616b3d8d34c21735d7385ea18165db3b..de580c86f19659c41df1952e3a67fe61c8546812 100644
--- a/spec/frontend/ci/pipeline_schedules/mock_data.js
+++ b/spec/frontend/ci/pipeline_schedules/mock_data.js
@@ -96,6 +96,30 @@ export const mockPipelineSchedulesResponsePlanLimitReached = {
   },
 };
 
+export const mockPipelineSchedulesResponseUnlimited = {
+  data: {
+    currentUser: mockGetPipelineSchedulesGraphQLResponse.data.currentUser,
+    project: {
+      id: mockGetPipelineSchedulesGraphQLResponse.data.project.id,
+      projectPlanLimits: {
+        ciPipelineSchedules: 0,
+        __typename: 'ProjectPlanLimits',
+      },
+      pipelineSchedules: {
+        count: 3,
+        nodes: mockGetPipelineSchedulesGraphQLResponse.data.project.pipelineSchedules.nodes,
+        pageInfo: {
+          hasNextPage: false,
+          hasPreviousPage: false,
+          startCursor: 'eyJpZCI6IjQ0In0',
+          endCursor: 'eyJpZCI6IjI4In0',
+          __typename: 'PageInfo',
+        },
+      },
+    },
+  },
+};
+
 export const emptyPipelineSchedulesResponse = {
   data: {
     currentUser: {