vuetifyjs/vuetify · error · Error

[Vuetify] v-expansion-panel-text needs to be placed inside v

Error message

[Vuetify] v-expansion-panel-text needs to be placed inside v-expansion-panel

What it means

VExpansionPanelText injects VExpansionPanelSymbol, provided only by <v-expansion-panel>. It throws when <v-expansion-panel-text> is rendered without a <v-expansion-panel> ancestor, because there is no panel selection/state to bind to.

Source

Thrown at packages/vuetify/src/components/VExpansionPanel/VExpansionPanelText.tsx:26

// Utilities
import { inject } from 'vue'
import { genericComponent, propsFactory, useRender } from '@/util'

export const makeVExpansionPanelTextProps = propsFactory({
  ...makeComponentProps(),
  ...makeLazyProps(),
}, 'VExpansionPanelText')

export const VExpansionPanelText = genericComponent()({
  name: 'VExpansionPanelText',

  props: makeVExpansionPanelTextProps(),

  setup (props, { slots }) {
    const expansionPanel = inject(VExpansionPanelSymbol)

    if (!expansionPanel) throw new Error('[Vuetify] v-expansion-panel-text needs to be placed inside v-expansion-panel')

    const { hasContent, onAfterLeave } = useLazy(props, expansionPanel.isSelected)

    useRender(() => (
      <VExpandTransition onAfterLeave={ onAfterLeave }>
        <div
          class={[
            'v-expansion-panel-text',
            props.class,
          ]}
          style={ props.style }
          v-show={ expansionPanel.isSelected.value }
        >
          { slots.default && hasContent.value && (
            <div class="v-expansion-panel-text__wrapper">
              { slots.default?.() }
            </div>
          )}

View on GitHub (pinned to 8d153908df)

Solutions

  1. Nest <v-expansion-panel-text> inside <v-expansion-panel> (and that inside <v-expansion-panels>).
  2. Keep teleported content within the panel's provide tree.
  3. In tests, wrap the component in <v-expansion-panel>.

Example fix

// before
<v-expansion-panel-text>Content</v-expansion-panel-text>

// after
<v-expansion-panels>
  <v-expansion-panel>
    <v-expansion-panel-title>Title</v-expansion-panel-title>
    <v-expansion-panel-text>Content</v-expansion-panel-text>
  </v-expansion-panel>
</v-expansion-panels>
Defensive patterns

Strategy: type-guard

Validate before calling

import { inject } from 'vue'
import { VExpansionPanelSymbol } from 'vuetify'
const panel = inject(VExpansionPanelSymbol, null)
if (!panel) {
  // not inside a panel: render a plain div instead of v-expansion-panel-text
}

Type guard

import { inject } from 'vue'
import { VExpansionPanelSymbol } from 'vuetify'
export function isInsideExpansionPanel (): boolean {
  return inject(VExpansionPanelSymbol, null) != null
}

Prevention

When it happens

Trigger: Placing <v-expansion-panel-text> outside <v-expansion-panel>; rendering it via a teleport whose target is outside the panel tree; testing it in isolation.

Common situations: Copy-paste of panel text into a non-panel container; restructuring markup that drops the panel wrapper; custom builds that assemble panels dynamically and miss the wrapper.

Related errors


AI-assisted analysis of vuetifyjs/vuetify@8d153908df (2026-08-12). Data as JSON: /api/errors/e0694c00c5b298b0. Report an issue: GitHub.