diff --git a/.changeset/7143-data-error-state-migration.md b/.changeset/7143-data-error-state-migration.md
new file mode 100644
index 000000000..1d490b3e2
--- /dev/null
+++ b/.changeset/7143-data-error-state-migration.md
@@ -0,0 +1,58 @@
+---
+'@object-ui/components': minor
+'@object-ui/plugin-list': minor
+---
+
+`DataErrorState` accepts the icon props `DataEmptyState` already had, and `ListView`'s
+load-failure panel is now rendered by the error state instead of the empty state
+(objectui#7143; maintainer ruling 2026-09-01, director decision batch #27).
+
+`ListView` rendered its load FAILURE through `DataEmptyState` — the component named for
+the *empty* case — passing it a destructive icon, error copy and a retry action, while
+`DataErrorState`, in the same file and with the same layout, had no consumer anywhere in
+the repo. objectui#7132 closed the accessibility half of that collision (the panel now
+declares `role="alert"` over the empty state's `role="status"` default) and deliberately
+left the structural half alone: `DataErrorState` hardcoded its icon, so the swap was a
+props-surface question plus a visual change rather than a rename.
+
+**`@object-ui/components` — three additive optional props on `DataErrorState`**, mirrored
+from `DataEmptyState` in the same file rather than spelled a second way:
+
+- `icon?: React.ReactNode` — rendered above the title; falls back to the `AlertCircle`
+ glyph the component has always drawn.
+- `showIcon?: boolean` (default `true`) — `false` omits the icon container entirely.
+- `iconWrapperClassName?: string` — REPLACES the wrapper's default class rather than
+ merging with it, so `""` renders the icon raw. `DataEmptyState` resolves it with `??`
+ against its own default and this does the same, against
+ `flex size-10 items-center justify-center rounded-lg bg-destructive/10` — the destructive
+ square `DataErrorState` already drew.
+
+Same names, same types, same default semantics as the empty state's; nothing existing on
+`DataErrorState` changed, and a call site that passes none of the three renders exactly
+what it rendered before. `illustration` and `action` were deliberately NOT mirrored — the
+ruling pins three props, and this component's retry affordance is already spelled
+`onRetry` / `retryLabel` (plus `children` for a call site that needs its own control).
+
+One non-prop addition rides along, called out rather than folded in: the icon wrapper now
+carries `data-slot="data-error-state-icon"`, mirroring the empty state's
+`data-empty-state-icon`. Without it the wrapper `iconWrapperClassName` governs has no
+name — untestable and unstylable — and migrating a call site off `DataEmptyState` would
+DROP that identifier rather than rename it.
+
+**`@object-ui/plugin-list` — the panel changes component identity, not pixels.** The call
+site passes the same custom icon through the new `icon` prop, the same
+`iconWrapperClassName="mb-3"`, the same title, and the same copy through `message` (the
+error state's spelling of `description`); its retry `
)}
- />
+
) : loading && data.length === 0 ? (
Promise) {
+ const ds = {
+ find: vi.fn().mockImplementation(find),
+ findOne: vi.fn(),
+ create: vi.fn(),
+ update: vi.fn(),
+ delete: vi.fn(),
+ };
+ return render(
+
+
+ ,
+ );
+}
+
+const failing = (error: unknown) => renderWith(() => Promise.reject(error));
+const forbidden = () => Object.assign(new Error('Forbidden'), { httpStatus: 403 });
+
+async function panel(container: HTMLElement, testId: string): Promise {
+ await waitFor(() => {
+ expect(container.querySelector(`[data-testid="${testId}"]`)).not.toBeNull();
+ });
+ return container.querySelector(`[data-testid="${testId}"]`) as HTMLElement;
+}
+
+describe('ListView — the load failure is a DataErrorState (#7143)', () => {
+ it('IDENTITY: the panel is data-slot="data-error-state"', async () => {
+ const { container } = failing(forbidden());
+ const box = await panel(container, 'list-error-state');
+ expect(box.getAttribute('data-slot')).toBe('data-error-state');
+ // Named in both directions: the component it used to borrow must be gone
+ // from this node, not merely joined by a second slot value.
+ expect(box.getAttribute('data-slot')).not.toBe('data-empty-state');
+ });
+
+ it('UNCHANGED: role, test id and error kind survive the swap', async () => {
+ const { container } = failing(forbidden());
+ const box = await panel(container, 'list-error-state');
+ expect(box.getAttribute('role')).toBe('alert');
+ expect(box.getAttribute('data-error-kind')).toBe('forbidden');
+ expect(box.textContent).toMatch(/permission|access/i);
+ });
+
+ it('UNCHANGED: the per-kind glyph still reaches the panel, in a stripped wrapper', async () => {
+ // The reason `DataErrorState` needed props at all: this panel draws an
+ // outage differently from a denial, and it removes the primitive's box.
+ const { container } = failing(new TypeError('Failed to fetch'));
+ const box = await panel(container, 'list-error-state');
+ const icon = box.querySelector('[data-slot="data-error-state-icon"]');
+ expect(icon).not.toBeNull();
+ expect(icon!.querySelector('svg')).not.toBeNull();
+ // `iconWrapperClassName` REPLACES rather than merges, so the destructive
+ // square the primitive draws by default must not have survived.
+ expect(icon!.className).toBe('mb-3');
+ });
+
+ it('UNCHANGED: the retry button is still rendered inside the panel', async () => {
+ const { container } = failing(forbidden());
+ const box = await panel(container, 'list-error-state');
+ const retry = box.querySelector('[data-testid="list-error-retry"]');
+ expect(retry).not.toBeNull();
+ expect(retry!.textContent).toMatch(/retry/i);
+ });
+
+ it('UNCHANGED: an enable-block denial still offers no retry', async () => {
+ const err = Object.assign(new Error('Object API is disabled'), {
+ httpStatus: 404,
+ code: 'OBJECT_API_DISABLED',
+ });
+ const { container } = failing(err);
+ const box = await panel(container, 'list-error-state');
+ expect(box.querySelector('[data-testid="list-error-retry"]')).toBeNull();
+ // The panel itself is still there — "no retry" must not be reached by
+ // rendering nothing at all.
+ expect(box.getAttribute('data-slot')).toBe('data-error-state');
+ });
+
+ it('CONTROL: the genuine empty branch is still a DataEmptyState with role="status"', async () => {
+ const { container } = renderWith(() => Promise.resolve([]));
+ const box = await panel(container, 'empty-state');
+ expect(box.getAttribute('data-slot')).toBe('data-empty-state');
+ expect(box.getAttribute('role')).toBe('status');
+ });
+});