How to Use Collapse and Tabs in Element UI for Collapsible Content and Tabbed Navigation

Element UI provides ElCollapse and ElTabs components that use Vue's provide/inject pattern to manage panel states and tab navigation through v-model binding and event emissions.

This guide examines the implementation details of collapsible panels and tabbed interfaces in the ElemeFE/element repository. You will learn how to leverage the internal architecture—including the collapse and rootTabs provide/inject contexts—to build interactive layouts with accordion behavior, editable tabs, and custom slot-based headers.

Component Architecture and State Management

Both components follow a consistent Vue-style API pattern that separates container logic from child item rendering.

ElCollapse manages an array of active panel names (activeNames) and provides a collapse object to all child items via provide() { return { collapse: this } } in packages/collapse/src/collapse.vue. Child ElCollapseItem components inject this parent reference to determine their active state and dispatch item-click events upward.

ElTabs tracks the currently selected pane in currentName and provides rootTabs to its navigation components and panes through the same provide/inject mechanism defined in packages/tabs/src/tabs.vue. The root component handles tab clicks, removals, additions, and before-leave validation hooks.

Both containers emit input events for v-model synchronization and specific change events (change for collapse, tab-click/tab-remove for tabs) to notify parent components of state transitions.

Implementing Collapsible Panels with ElCollapse

Basic Multi-Panel Configuration

The standard collapse implementation allows multiple panels to remain open simultaneously. The container stores active names in its activeNames data property, which derives from the value prop and updates through setActiveNames() method calls.

<template>
  <el-collapse v-model="activeNames" @change="handleChange">
    <el-collapse-item title="Consistency" name="1">
      <p>Design stays consistent across the UI.</p>
    </el-collapse-item>
    <el-collapse-item title="Feedback" name="2">
      <p>Provide clear feedback on user actions.</p>
    </el-collapse-item>
  </el-collapse>
</template>

<script>
export default {
  data() {
    return { activeNames: ['1'] };
  },
  methods: {
    handleChange(val) {
      console.log('Active panels:', val);
    }
  }
};
</script>

Each ElCollapseItem computes its visibility by checking this.collapse.activeNames via the injected parent reference, then uses ElCollapseTransition for smooth height animation when opening or closing.

Accordion Mode for Single Panel Display

Set the accordion prop to restrict the active set to a single panel at a time. When enabled, clicking a new panel automatically collapses the previously open one by ensuring activeNames contains only the most recently clicked name.

<template>
  <el-collapse v-model="activeName" accordion>
    <el-collapse-item title="Network Settings" name="network">
      <p>Configure network adapters and proxies.</p>
    </el-collapse-item>
    <el-collapse-item title="Security Settings" name="security">
      <p>Manage firewall and encryption options.</p>
    </el-collapse-item>
  </el-collapse>
</template>

<script>
export default {
  data() {
    return { activeName: 'network' };
  }
};
</script>

In packages/collapse/src/collapse.vue, the handleItemClick method checks this flag before calling setActiveNames() to either replace the entire array (accordion) or toggle the specific name (standard mode).

Custom Headers with Named Slots

Override the default title prop rendering by using the title slot (or #title in Vue 3 syntax). This allows integration of icons, badges, or complex markup in panel headers.

<el-collapse-item name="custom">
  <template #title>
    <span>System Status <i class="el-icon-info"></i></span>
    <el-tag size="mini">Updated</el-tag>
  </template>
  <p>Last backup completed successfully.</p>
</el-collapse-item>

The slot content renders inside the header trigger element in packages/collapse/src/collapse-item.vue, which dispatches the item-click event to the parent when clicked.

Building Tabbed Navigation with ElTabs

Standard Tab Implementation

The ElTabs component manages tab selection through currentName and renders navigation headers via the internal TabNav component. Bind the v-model to synchronize the active tab state.

<template>
  <el-tabs v-model="activeTab" @tab-click="handleTabClick">
    <el-tab-pane label="User Management" name="users">User content</el-tab-pane>
    <el-tab-pane label="System Logs" name="logs">Log content</el-tab-pane>
  </el-tabs>
</template>

<script>
export default {
  data() {
    return { activeTab: 'users' };
  },
  methods: {
    handleTabClick(tab) {
      console.log('Activated:', tab.name);
    }
  }
};
</script>

The tab-click event emits when the handleTabClick method in packages/tabs/src/tabs.vue successfully updates currentName, allowing parents to react to navigation changes.

Editable Tabs with Dynamic Addition and Removal

Enable the editable prop to display add and remove controls. The component emits an edit event with parameters (targetName, action) where action is either 'add' or 'remove'.

<template>
  <el-tabs v-model="editableTab" type="card" editable @edit="handleEdit">
    <el-tab-pane
      v-for="pane in panes"
      :key="pane.name"
      :label="pane.title"
      :name="pane.name">
      {{ pane.content }}
    </el-tab-pane>
  </el-tabs>
</template>

<script>
export default {
  data() {
    return {
      editableTab: '2',
      panes: [
        { title: 'Tab 1', name: '1', content: 'Content 1' },
        { title: 'Tab 2', name: '2', content: 'Content 2' }
      ],
      tabIndex: 2
    };
  },
  methods: {
    handleEdit(target, action) {
      if (action === 'add') {
        const name = String(++this.tabIndex);
        this.panes.push({ 
          title: `Tab ${name}`, 
          name, 
          content: `Content ${name}` 
        });
        this.editableTab = name;
      } else if (action === 'remove') {
        this.panes = this.panes.filter(p => p.name !== target);
        if (this.editableTab === target) {
          this.editableTab = this.panes[0]?.name;
        }
      }
    }
  }
};
</script>

The underlying implementation in packages/tabs/src/tabs.vue provides handleTabAdd and handleTabRemove methods that trigger these events while managing the internal tab bar state.

Custom Labels and Before-Leave Hooks

Use the label slot to customize tab headers with HTML content, and implement the before-leave prop to prevent navigation until async validation completes.

<template>
  <el-tabs 
    v-model="activeTab" 
    type="border-card"
    :before-leave="beforeLeave">
    <el-tab-pane name="settings">
      <span slot="label"><i class="el-icon-setting"></i> Settings</span>
      Configuration panel
    </el-tab-pane>
    <el-tab-pane name="danger">
      <span slot="label"><i class="el-icon-warning"></i> Danger Zone</span>
      Destructive actions
    </el-tab-pane>
  </el-tabs>
</template>

<script>
export default {
  data() {
    return { activeTab: 'settings' };
  },
  methods: {
    beforeLeave(activeName, oldActiveName) {
      if (oldActiveName === 'danger' && this.hasUnsavedChanges) {
        return new Promise((resolve, reject) => {
          this.$confirm('Leave without saving?')
            .then(() => resolve())
            .catch(() => reject());
        });
      }
      return true;
    }
  }
};
</script>

The before-leave function receives the target and current tab names. If it returns false or a rejected Promise, setCurrentName in packages/tabs/src/tabs.vue aborts the navigation and maintains the current selection.

Key Implementation Files

Understanding these source files helps debug complex interactions and extend component behavior:

Summary

  • ElCollapse uses provide/inject to share activeNames state with child items, supporting both multi-panel and accordion modes through the accordion boolean prop.
  • ElTabs manages currentName and provides rootTabs to children, emitting tab-click, tab-remove, and tab-add events for external state synchronization.
  • Both components support v-model binding via value props and input emissions, enabling two-way data binding with parent components.
  • Custom content insertion is available through named slots: title on collapse items and label on tab panes.
  • The before-leave prop on tabs accepts functions or Promises to implement navigation guards and validation logic.

Frequently Asked Questions

How does Element UI determine which collapse panel is active?

Each ElCollapseItem component injects the parent collapse object and computes its active state by checking if its name exists in the parent's activeNames array. When a user clicks the panel header, the item dispatches an item-click event to the parent, which executes handleItemClick and updates activeNames via setActiveNames(), triggering a reactive re-render.

Can I prevent a tab from switching until confirmation is received?

Yes. Pass a function or Promise-returning method to the before-leave prop on ElTabs. The function receives (newName, oldName) as parameters. Return false or a rejected Promise to cancel the navigation. This is implemented in packages/tabs/src/tabs.vue within the setCurrentName method, which awaits the hook's resolution before updating currentName.

What is the difference between the change event on Collapse and the tab-click event on Tabs?

The change event on ElCollapse emits whenever the activeNames array updates, providing the new array of active panel names. The tab-click event on ElTabs fires when a user clicks a tab label, passing the clicked tab instance and event object. Unlike change, tab-click fires before before-leave validation and can be used to trigger side effects regardless of whether the tab actually switches.

How do I add icons or custom HTML to tab headers?

Use the label slot on ElTabPane instead of the label prop. The slot content replaces the default text header in the tab navigation bar rendered by tab-nav.vue, allowing complex markup including icons, badges, or dynamic status indicators while maintaining the component's built-in click handling and active state styling.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →