Unlike a marketing site, enterprise dashboards are dense with el-table, el-form, and bespoke cell renderers. The Vue 3 work ( core checklist) and the library work interleave. Pinia and state migration often land in the same sprints as table pagination refactors because the same screens drive both.
This post stays specific to Element / Element Plus, so we are not re-covering the generic Bootstrap or Vuetify angles covered in other articles on this site.
1. Inventory: components that hurt
Before touching Vue core, we tag screens by the Element controls they use. In practice, the time sinks are: el-table with custom column slots, nested dialogs + drawer stacks, el-form with async rules, and global $message / $notify usage (imperative APIs you will trace through every file).
2. i18n and day-one locale parity
If you ship in multiple languages, your Element Plus locale config must be paired with app-level i18n so strings and date formats stay aligned. A partial migration (English Plus + legacy Vue 2 in another locale) is a support trap—treat i18n as a release gate, not a follow-up.
3. Rollout strategy
We prefer vertical slices: migrate a domain (billing, org admin, reporting) to Vue 3 with Element Plus end-to-end, with feature flags behind routes. If you federate or run multiple deployables, align package versions per shell so you do not ship two major Element lineages at once.
4. What “done” means for QA
- Print / PDF flows where
el-tablewidth math changed - Keyboard and focus order through nested overlays
- Row virtualization or infinite scroll, if you brought third-party add-ons in Vue 2
5. The component-by-component diff
Most Element UI components have a near-identical Element Plus counterpart, but the breaking changes cluster in a predictable set. Here is the cheat sheet we hand to teams on day one:
| Element UI (Vue 2) | Element Plus (Vue 3) | What breaks |
|---|---|---|
this.$message | ElMessage import | Imperative API moves from Vue prototype to named import; every call site changes. |
el-table scoped slot slot-scope="scope" | #default="scope" | Slot syntax change; scoped slots renamed. |
el-form prop path strings | Same syntax | Validation timing changed — async rules now resolve through Promises only. |
el-dialog with .sync on visible | v-model | No more .sync; converts cleanly with codemod. |
el-input v-model.trim | Same | Works, but composition events fire differently with IME (Japanese, Chinese). |
el-select remote with remote-method | Same prop names | Debounce default changed; check loading flicker. |
el-date-picker default value | el-date-picker | Default returns Date object; formerly returned a localized string in some configs. |
6. Migrating el-table without losing your mind
el-table is where most enterprise Element migrations stall. The breaking changes look small in the docs and large in production, especially around scoped slots and column rendering.
Before (Vue 2 / Element UI)
<el-table :data="rows">
<el-table-column label="Status">
<template slot-scope="scope">
<status-badge :value="scope.row.status" />
</template>
</el-table-column>
</el-table>After (Vue 3 / Element Plus)
<el-table :data="rows">
<el-table-column label="Status">
<template #default="scope">
<status-badge :value="scope.row.status" />
</template>
</el-table-column>
</el-table>The mechanical change is trivial. The traps are:
- Render functions in column definitions using
render-headerorscopedSlotsgenerators must be rewritten. They were usually copy-pasted from a senior dev's gist three years ago and nobody knows why. - Custom sort and filter functions receive arguments in slightly different shapes. Snapshot tests catch this; visual review does not.
- Fixed columns (
fixed="left") now use CSSposition: sticky. Older browsers and print styles need verification. - Virtualized tables in Element Plus use
el-table-v2with a different API; this is a port, not an upgrade.
7. Imperative APIs and the $message problem
Element UI patched the Vue prototype: this.$message, this.$notify, this.$confirm, this.$msgbox. Element Plus exports them as named imports. That is correct, but it means every call site changes.
// before
this.$message.success("Saved");
// after
import { ElMessage } from "element-plus";
ElMessage.success("Saved");Two pragmatic options:
- Codemod everything. A jscodeshift script handles the simple cases; the awkward cases (e.g.
this.$confirm(...).then(...)) need a hand-pass. - Wrap with a thin facade. Re-export the imperative APIs from
@/lib/notify.ts, then import from there everywhere. Future moves (away from Element entirely, for example) become a one-file change.
8. Theming, dark mode, and CSS variables
Element UI used SCSS variables compiled into a single theme. Element Plus uses CSS custom properties, which makes runtime theming and dark mode straightforward — but every legacy SCSS override file must be revisited.
- Replace
$--color-primaryoverrides with--el-color-primaryCSS variables. - Move dark-mode toggles to the
html.darkselector recommended by Element Plus. - If you use a corporate design token package, audit which tokens map to which Element Plus variables. This is the cleanest moment to consolidate; coordinate with any design system extraction work in progress.
9. Common pitfalls in enterprise dashboards
- Print stylesheets break silently. CSS variable cascading interacts with
@media printin ways that flat SCSS variables did not. QA your PDF and "print to PDF" flows. - Form validation timing. Async
rulesresolved synchronously in some Element UI corners. Element Plus is strict about Promises. Race conditions with debounced server-side checks surface immediately. - Locale defaults. Element UI fell back to English when no locale was registered. Element Plus warns and ships untranslated keys. Pair locale registration with vue-i18n bootstrap.
el-popoverteleport target. Default changed; popovers may render inside scrolled containers in unexpected places. Setteleported="false"selectively.- Auto-import + global styles double up. If you mix
unplugin-vue-componentsauto-import with a global Element Plus import, CSS bundles inflate. Pick one strategy.
10. Step-by-step migration plan
- Inventory. Grep every
el-*tag and$message / $notify / $confirmcall. Tag screens by complexity (table-heavy, form-heavy, simple). - Wrap imperative APIs. Introduce
@/lib/notify.tsin the Vue 2 codebase first, so the cutover is mechanical. - Pick a vertical slice. One domain, one route group, one feature flag. We typically start with an internal admin screen — production traffic, low blast radius.
- Run codemods on the slice.
slot-scope→#default,.sync→v-model, prototype methods → named imports. - Hand-pass
el-tablecolumns. This is the one place codemods reliably miss edge cases. - QA pass. Print/PDF, keyboard order, IME input, locale parity. Add Cypress component tests where coverage is thin — see our Cypress component testing post.
- Soak in production behind a flag for one release cycle. Compare error rates against the Vue 2 baseline.
- Move to the next slice. Track velocity per slice; estimates compound, and the second slice should be ~30% faster than the first.
11. Anti-patterns we see
- "Migrate Element first, then Vue." Element Plus does not run on Vue 2. Element UI does not run on Vue 3. They are coupled migrations.
- Forking Element UI to add Vue 3 support. Some teams attempt this. The maintenance cost dwarfs the migration cost within 6 months.
- Big-bang switch over a weekend. Enterprise dashboards have long-tail behaviors (saved filters, exported PDFs, scheduled jobs that hit the same UI). Slice migrations win.
- Skipping the wrapper layer for imperative APIs. Direct
ElMessageimports everywhere couples your code tighter to Element Plus than necessary. - Treating
@vue/compatas the bridge. Compat does not help with Element — both libraries must move together. Coordinate this with the broader strangler fig pattern.
FAQ
How long does a typical Element UI to Element Plus migration take?
For a 50-screen dashboard, 8–14 weeks of focused work for a team of 3–4 engineers, including the parallel Vue 3 core migration. Our cost estimate guide breaks down the variables.
Can we use Element Plus for new features while keeping Element UI for legacy screens?
Only across separate apps or micro-frontends. In one Vue runtime you must pick one. Loading both inflates your bundle and creates CSS conflicts.
Should we switch to a different UI kit during the migration?
If your product is Element-heavy, no — staying on Element Plus is the lowest-risk path. If you were already considering a switch, doing it post-migration (on a stable Vue 3 baseline) is cheaper than doing both at once.
What about Element Plus accessibility?
Better than Element UI in most components, but not perfect. Audit keyboard navigation in dialogs, tables, and select dropdowns. Some teams pair the migration with their first real WCAG sweep.
Element-heavy app and a hard deadline?
We have migrated data-dense UIs on Element to Vue 3 with testable slices and predictable calendars.
Discuss your dashboardConclusion
Element Plus is the right successor for most Element UI codebases, but the migration effort tracks how deeply your product relied on table and form composition—not how many el-button tags you have. Sequence the painful surfaces early and you will de-risk the rest of the Vue 3 work.
