ionic-team/ionic-framework · error · Error

IonTabs must contain an IonRouterOutlet or an IonTab.

Error message

IonTabs must contain an IonRouterOutlet or an IonTab.

What it means

Thrown from the render() of the Vue `<IonTabs>` component when the default slot contains neither an `IonRouterOutlet` nor an `IonTab` vnode. IonTabs requires exactly one of those two navigation models so it knows how to manage child views. Because this fires during render, Vue surfaces it as an uncaught runtime error.

Source

Thrown at packages/vue/src/components/IonTabs.ts:141

       * the history stack or URL updates associated
       * with the router.
       */
      routerOutlet = slottedContent.find((child: VNode) =>
        isRouterOutlet(child)
      );

      /**
       * Developers must pass at least one ion-tab
       * inside of ion-tabs if they want to use a
       * basic tab-based navigation without the
       * history stack or URL updates associated
       * with the router.
       */
      hasTab = slottedContent.some((child: VNode) => isTab(child));
    }

    if (!routerOutlet && !hasTab) {
      throw new Error("IonTabs must contain an IonRouterOutlet or an IonTab.");
    }
    if (routerOutlet && hasTab) {
      throw new Error(
        "IonTabs cannot contain an IonRouterOutlet and IonTab at the same time."
      );
    }

    if (hasTab) {
      return h(
        "ion-tabs",
        {
          ...props,
        },
        slottedContent
      );
    }

    /**

View on GitHub (pinned to 625f9c38ad)

Solutions

  1. Add an `<ion-router-outlet>` (via `<IonRouterOutlet>`) as a direct child of `<IonTabs>` for router-driven tabs.
  2. Or add at least one `<IonTab>` child for router-less tab navigation.
  3. Ensure the outlet/tab is not gated behind a `v-if` that evaluates false on the first render; use `v-show` or render a fallback.
  4. If the slot content comes from a dynamic list, guarantee the list is non-empty and contains a recognized tab/outlet element.

Example fix

<!-- before -->
<IonTabs>
  <IonTabBar slot="bottom">
    <IonTabButton tab="home"><IonLabel>Home</IonLabel></IonTabButton>
  </IonTabBar>
</IonTabs>
<!-- after -->
<IonTabs>
  <IonRouterOutlet>
    <Route path="/home" component="Home" />
  </IonRouterOutlet>
  <IonTabBar slot="bottom">
    <IonTabButton tab="home" href="/home"><IonLabel>Home</IonLabel></IonTabButton>
  </IonTabBar>
</IonTabs>
Defensive patterns

Strategy: type-guard

Validate before calling

// Before relying on IonTabs, assert the slot will contain a recognized child
import { h, Fragment, isVNode } from 'vue';
const isRouterOutlet = (n: any) => n?.type && ((n.type as any).name === 'IonRouterOutlet' || n.type === 'ion-router-outlet');
const isTab = (n: any) => {
  if (n?.type === Fragment && Array.isArray(n.children)) return n.children.some((c) => isVNode(c) && isTab(c));
  return n?.type && ((n.type as any).name === 'ion-tab' || n.type === IonTab);
};
function validateTabsChildren(slotted: any[] | undefined) {
  const hasOutlet = !!slotted?.some(isRouterOutlet);
  const hasTab = !!slotted?.some(isTab);
  if (!hasOutlet && !hasTab) throw new Error('IonTabs needs an IonRouterOutlet or IonTab child');
}

Type guard

function hasTabsNavModel(slotted: any[] | undefined): boolean {
  const outlet = slotted?.some((n) => n?.type && ((n.type as any).name === 'IonRouterOutlet' || n.type === 'ion-router-outlet'));
  const tab = slotted?.some((n) => n?.type && ((n.type as any).name === 'ion-tab' || n.type === 'IonTab'));
  return Boolean(outlet) || Boolean(tab);
}

Prevention

When it happens

Trigger: Authoring `<IonTabs>` whose children are only an `<IonTabBar>`, plain divs, or nothing at all — i.e. neither `<IonRouterOutlet>` nor `<IonTab>` is present in the slot.

Common situations: Migrating from an older tabs API that allowed a tab bar alone; forgetting to wrap routes in `<IonRouterOutlet>`; rendering tabs conditionally so the outlet is absent on first render; copy-pasting a tabs template that omitted the outlet.

Related errors


AI-assisted analysis of ionic-team/ionic-framework@625f9c38ad (2026-08-12). Data as JSON: /api/errors/56c2794fbcf711e3. Report an issue: GitHub.