diff --git a/apps/readest-app/src/__tests__/components/HighlightColorsEditor.test.tsx b/apps/readest-app/src/__tests__/components/HighlightColorsEditor.test.tsx
new file mode 100644
index 00000000..c6546b4e
--- /dev/null
+++ b/apps/readest-app/src/__tests__/components/HighlightColorsEditor.test.tsx
@@ -0,0 +1,115 @@
+import React, { useState } from 'react';
+import { describe, it, expect, vi, afterEach } from 'vitest';
+import { render, screen, cleanup, fireEvent } from '@testing-library/react';
+
+import HighlightColorsEditor from '@/components/settings/color/HighlightColorsEditor';
+import { HIGHLIGHT_COLOR_HEX } from '@/services/constants';
+import type { DefaultHighlightColor, HighlightColor, UserHighlightColor } from '@/types/book';
+
+vi.mock('@/hooks/useTranslation', () => ({
+ useTranslation: () => (s: string) => s,
+}));
+
+// Mocked SketchPicker that tracks mount/unmount via a shared module-level
+// registry, so we can detect whether the picker was remounted across a hex
+// change. Each mount gets a unique id; a mount is "alive" while its cleanup
+// hasn't run.
+const mountLog: Array<{ id: number; alive: boolean }> = [];
+let nextMountId = 0;
+
+vi.mock('react-color', () => ({
+ SketchPicker: ({
+ color,
+ onChange,
+ }: {
+ color: string;
+ onChange: (c: { hex: string }) => void;
+ }) => {
+ const [mountId] = useState(() => {
+ const id = nextMountId++;
+ mountLog.push({ id, alive: true });
+ return id;
+ });
+ React.useEffect(() => {
+ return () => {
+ const entry = mountLog.find((m) => m.id === mountId);
+ if (entry) entry.alive = false;
+ };
+ }, [mountId]);
+ return (
+
onChange({ hex: '#112233' })}
+ />
+ );
+ },
+ ColorResult: undefined,
+}));
+
+afterEach(() => {
+ cleanup();
+ mountLog.length = 0;
+ nextMountId = 0;
+});
+
+const Harness: React.FC<{ initialUserColors: UserHighlightColor[] }> = ({ initialUserColors }) => {
+ const [userColors, setUserColors] = useState
(initialUserColors);
+ const [customColors, setCustomColors] =
+ useState>(HIGHLIGHT_COLOR_HEX);
+ const [labels, setLabels] = useState>>({});
+
+ return (
+ {}}
+ />
+ );
+};
+
+describe('HighlightColorsEditor — user color SketchPicker stability', () => {
+ it('keeps the SketchPicker mounted when the user-color hex updates (so drag is not interrupted)', () => {
+ render();
+
+ // The user color row's ColorInput renders a text input with the hex.
+ // Find it (the predefined palette also renders inputs; the user row's
+ // input is last in the document because it's rendered after the defaults).
+ const hexInputs = screen.getAllByDisplayValue(/^#/);
+ const userHexInput = hexInputs[hexInputs.length - 1]!;
+ expect(userHexInput).toHaveProperty('value', '#aabbcc');
+
+ // Open the SketchPicker for this user color.
+ fireEvent.click(userHexInput);
+
+ const picker = screen.getByTestId('mock-sketch-picker');
+ const initialMountId = picker.getAttribute('data-mount-id');
+ expect(initialMountId).not.toBeNull();
+
+ // Simulate an onChange coming from the SketchPicker during drag.
+ // (In the real picker this fires continuously as the user drags.)
+ fireEvent.click(picker);
+
+ // After the hex update, the picker should STILL be mounted (same id).
+ // If the parent row unmounted due to `key={hex}` changing, the original
+ // mount would be gone and React would have mounted a fresh one — the
+ // drag's window-level mouse listeners would be torn down with it.
+ const sameMount = mountLog.find((m) => String(m.id) === initialMountId);
+ expect(sameMount, 'SketchPicker mount with initial id should still exist').toBeDefined();
+ expect(sameMount!.alive, 'SketchPicker should not be unmounted on hex change').toBe(true);
+
+ // And a picker for the new color should still be visible in the DOM.
+ const pickerAfter = screen.queryByTestId('mock-sketch-picker');
+ expect(pickerAfter).not.toBeNull();
+ expect(pickerAfter!.getAttribute('data-color')).toBe('#112233');
+ expect(pickerAfter!.getAttribute('data-mount-id')).toBe(initialMountId);
+ });
+});
diff --git a/apps/readest-app/src/components/settings/color/HighlightColorsEditor.tsx b/apps/readest-app/src/components/settings/color/HighlightColorsEditor.tsx
index a2bfb287..837417e7 100644
--- a/apps/readest-app/src/components/settings/color/HighlightColorsEditor.tsx
+++ b/apps/readest-app/src/components/settings/color/HighlightColorsEditor.tsx
@@ -253,7 +253,7 @@ const HighlightColorsEditor: React.FC = ({
{userHighlightColors.length > 0 && (
{userHighlightColors.map(({ hex, label }, index) => (
-