DEV Community

Cover image for Migrating from Elastic EUI to MUI, Chakra, or Ant Design — The Complete Playbook
Bassem Chagra
Bassem Chagra

Posted on • Originally published at frontfamily.com

Migrating from Elastic EUI to MUI, Chakra, or Ant Design — The Complete Playbook

If you're reading this, your team probably built something on Elastic EUI and now you're stuck. Maybe you're not an Elastic shop anymore. Maybe the 188kb bundle is killing your Lighthouse scores. Maybe you're tired of fighting the Elastic License.

Whatever the reason, you need to migrate — and the docs won't help you. This is what I learned mapping 45 EUI components to their MUI, Chakra, and Ant Design equivalents.

Why teams leave EUI

Bundle size. EUI ships approximately 188KB gzipped. MUI's tree-shakeable architecture lets you import only what you use, typically resulting in 40-60% smaller bundles.

Elastic ecosystem lock-in. EUI was designed for Kibana. Its design language, component behavior, and even its color palette reflect Elastic's brand. Teams building non-Elastic apps find themselves fighting the library's opinions.

License concerns. EUI uses the Elastic License 2.0 (ELv2), which is not OSI-approved. Some legal teams flag this as a compliance risk for SaaS products.

Community. Stack Overflow questions, blog posts, and community plugins are significantly fewer compared to MUI (93k+ GitHub stars) or Ant Design (91k+).

The hardest parts nobody warns you about

EuiProvider is a monolith

EUI wraps theme, i18n, toast notifications, and global component defaults in a single provider. You can't extract components one-by-one without either keeping EuiProvider alive (bloating the bundle) or breaking everything at once.

What to do: Map every component that depends on EuiProvider context before writing a single line of migration code. You'll be surprised how many "independent" components break without it.

EuiBasicTable pagination is invisible

EUI manages pagination state internally. MUI's DataGrid requires you to manage paginationModel state yourself — page index, page size, total count. Teams that relied on EUI's "just pass items and it works" discover they need to add 20+ lines of state management.

// EUI — pagination is automatic
<EuiBasicTable items={data} columns={columns} pagination={pagination} />

// MUI — you manage the state
const [paginationModel, setPaginationModel] = useState({
  page: 0,
  pageSize: 20,
});
<DataGrid
  rows={data}
  columns={columns}
  paginationModel={paginationModel}
  onPaginationModelChange={setPaginationModel}
  pageSizeOptions={[20, 50, 100]}
/>
Enter fullscreen mode Exit fullscreen mode

Color tokens are lossy

EUI uses semantic color names (subdued, accent, success) that map to its own palette. MUI uses Material Design colors (primary, secondary, error). There's no 1:1 mapping — subdued has no MUI equivalent. You'll end up building a custom theme layer just to bridge the gap.

The date math dependency

EUI depends on @elastic/datemath and moment. If your app uses EuiSuperDatePicker's relative date expressions ("now-15m"), you need to keep these dependencies or rewrite the date parsing logic. MUI doesn't have anything equivalent.

TypeScript migration patterns

This is where senior engineers spend most of their time.

Generic type mapping (Data Tables)

EUI uses generics to enforce type safety on row items:

// EUI
import type { EuiBasicTableColumn } from '@elastic/eui';

const columns: Array<EuiBasicTableColumn<User>> = [
  { field: 'name', name: 'Full Name', sortable: true },
  {
    field: 'status',
    name: 'Status',
    render: (status: User['status']) => (
      <EuiHealth color={status === 'active' ? 'success' : 'danger'}>
        {status}
      </EuiHealth>
    ),
  },
];
Enter fullscreen mode Exit fullscreen mode
// MUI equivalent
import type { GridColDef } from '@mui/x-data-grid';

const columns: GridColDef<User>[] = [
  { field: 'name', headerName: 'Full Name', sortable: true },
  {
    field: 'status',
    headerName: 'Status',
    renderCell: ({ row }) => (
      <Chip
        color={row.status === 'active' ? 'success' : 'error'}
        label={row.status}
        size="small"
      />
    ),
  },
];
Enter fullscreen mode Exit fullscreen mode

Key differences:

  1. EuiBasicTableColumn<T>GridColDef<T> — different generic wrapper
  2. nameheaderName — column display name
  3. render: (value)renderCell: ({ row }) — receives params object, not raw value
  4. MUI DataGrid requires @mui/x-data-grid as a separate package

Dealing with ExclusiveUnion

EUI uses a custom ExclusiveUnion utility to strictly prevent mixing button/anchor props:

// EUI — TypeScript Error! Can't have both onClick and href
<EuiButton href="/login" onClick={handleClick}>Login</EuiButton>
Enter fullscreen mode Exit fullscreen mode

MUI handles this with the component prop:

// MUI — explicit about the underlying element
<Button component="a" href="/login">Login</Button>
<Button component={Link} href="/login">Login</Button> // Next.js Link
Enter fullscreen mode Exit fullscreen mode

When migrating wrapper components, rewrite using MUI's typed component pattern:

// Before: EUI wrapper with ExclusiveUnion
type Props = EuiButtonProps; // includes ExclusiveUnion internally

// After: MUI wrapper with generic component type
type Props<C extends React.ElementType = 'button'> = ButtonProps<C>;
Enter fullscreen mode Exit fullscreen mode

The 10 components with no direct equivalent

These will cost you the most time:

EUI Component What to do
EuiSuperDatePicker Build custom with two MUI DatePickers + relative date presets
EuiGlobalToastList Use notistack or build a custom Snackbar queue manager
EuiHealth Trivial — just a colored dot with Box
EuiStat Build with Typography + Box
EuiColorPicker Use react-colorful
EuiMarkdownEditor Use @uiw/react-md-editor
EuiCodeBlock Use react-syntax-highlighter or prism-react-renderer
EuiComboBox (full) MUI Autocomplete covers 80% — async loading needs custom work
EuiContextMenu (panels) MUI Menu doesn't support panel-based navigation — build custom
EuiSelectable No direct equivalent — use custom interface with MUI Select

The migration strategy that works

After doing this across 45 components, here's the approach I'd use again:

Step 1: Audit. List every EUI component in your codebase. Count usage. Sort by frequency.

Step 2: Abstraction layer. Create wrapper components that currently re-export EUI:

// /components/ui/Button.tsx — Phase 1
export { EuiButton as Button } from '@elastic/eui';

// Phase 2 — swap the import, consumers don't change
export { Button } from '@mui/material';
Enter fullscreen mode Exit fullscreen mode

Step 3: Leaf components first. Migrate buttons, badges, text, avatars — the components that don't depend on others. This builds confidence and momentum.

Step 4: Complex components last. Tables, modals, forms — these touch the most code and have the most API differences.

Step 5: Kill EuiProvider. Once all components are migrated, remove EuiProvider and @elastic/eui from your dependencies. Verify nothing breaks.

Interactive component reference

I built a searchable tool that maps all 45 EUI components to their MUI equivalents — including props, import changes, and which ones have no direct equivalent.

Try the interactive prop search table →

You can also paste EUI code directly into the converter and get MUI/Chakra/Ant Design output instantly:

Open the converter →

Or eject a production-ready component into your project:

npx @frontfamily/cli eject data-table -f react-mui
Enter fullscreen mode Exit fullscreen mode

This guide is part of FrontFamily — a free tool for converting UI components between frameworks. 42 conversion paths, 219 verified component mappings, 18 ejectable patterns.

Top comments (0)