diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md
index ce41753c2b25bb..4ff71c634a01be 100644
--- a/packages/components/CHANGELOG.md
+++ b/packages/components/CHANGELOG.md
@@ -17,6 +17,7 @@
### Bug Fixes
+- `InputControl`, `NumberControl`, `UnitControl`: Ignore the drag gesture for touch input, where it has no cursor affordance and is easy to trigger by accident ([#38865](https://github.com/WordPress/gutenberg/issues/38865), [#81519](https://github.com/WordPress/gutenberg/pull/81519)).
- `navigateRegions`: Only outline the Publish and Save controls when their region has focus ([#84343](https://github.com/WordPress/gutenberg/pull/84343)).
## 42.0.0 (2026-10-07)
diff --git a/packages/components/src/input-control/input-field.tsx b/packages/components/src/input-control/input-field.tsx
index 9bd9aef6c959fe..c447cfb1a290e6 100644
--- a/packages/components/src/input-control/input-field.tsx
+++ b/packages/components/src/input-control/input-field.tsx
@@ -133,6 +133,16 @@ function InputField(
( dragProps ) => {
const { distance, dragging, event, target } = dragProps;
+ // The drag gesture is not offered for touch input, where there is
+ // no cursor affordance for it, and where it's easy to accidentally
+ // change the value when only meaning to tap or move the caret.
+ if (
+ event.pointerType === 'touch' ||
+ event.type.startsWith( 'touch' )
+ ) {
+ return;
+ }
+
// The `target` prop always references the `input` element while, by
// default, the `dragProps.event.target` property would reference the real
// event target (i.e. any DOM element that the pointer is hovering while
diff --git a/packages/components/src/number-control/test/index.browser.test.tsx b/packages/components/src/number-control/test/index.browser.test.tsx
new file mode 100644
index 00000000000000..f374be989acb4b
--- /dev/null
+++ b/packages/components/src/number-control/test/index.browser.test.tsx
@@ -0,0 +1,47 @@
+import { describe, expect, it } from 'vitest';
+import { fireEvent } from '@testing-library/react';
+import { render } from 'vitest-browser-react';
+import { useState } from '@wordpress/element';
+import NumberControl from '..';
+
+function StatefulNumberControl() {
+ const [ value, setValue ] = useState< string | undefined >( '5' );
+
+ return ;
+}
+
+function dragInput( input: Element, pointerType: 'touch' | 'mouse' ) {
+ const pointer = {
+ pointerId: 1,
+ pointerType,
+ isPrimary: true,
+ button: 0,
+ buttons: 1,
+ clientX: 50,
+ };
+
+ fireEvent.pointerDown( input, { ...pointer, clientY: 100 } );
+ fireEvent.pointerMove( window, { ...pointer, clientY: 80 } );
+ fireEvent.pointerMove( window, { ...pointer, clientY: 40 } );
+ fireEvent.pointerUp( window, { ...pointer, clientY: 40, buttons: 0 } );
+}
+
+describe( 'NumberControl drag to change value', () => {
+ it( 'does not change the value when dragged with touch input', async () => {
+ const screen = await render( );
+ const input = screen.getByRole( 'spinbutton' );
+
+ dragInput( input.element(), 'touch' );
+
+ await expect.element( input ).toHaveValue( 5 );
+ } );
+
+ it( 'changes the value when dragged with mouse input', async () => {
+ const screen = await render( );
+ const input = screen.getByRole( 'spinbutton' );
+
+ dragInput( input.element(), 'mouse' );
+
+ await expect.element( input ).toHaveValue( 55 );
+ } );
+} );
diff --git a/test/unit/vitest-policy-exceptions.json b/test/unit/vitest-policy-exceptions.json
index f2da449a99a789..8b4618f1f16ee7 100644
--- a/test/unit/vitest-policy-exceptions.json
+++ b/test/unit/vitest-policy-exceptions.json
@@ -3,7 +3,8 @@
"packages/components/src/focal-point-picker/test/index.browser.test.tsx": "Starts an unreleased mouse gesture to verify cancellation when native Tab moves focus away; subsequent movement uses native hover.",
"packages/media-editor/src/image-editor/react/components/stencils/test/rectangle-stencil.browser.test.tsx": "Uses exact pointer and touch payloads for capture lifecycle coverage, plus direct key events only where fake timers verify resize debounce timing.",
"packages/media-editor/src/image-editor/react/components/test/cropper.browser.test.tsx": "Uses exact pointer, wheel, and transition payloads for crop geometry and lifecycle coverage, plus a direct key event only where fake timers isolate resize timing.",
- "packages/components/src/color-picker/test/index.browser.test.tsx": "Supplies exact bottom-edge mouse coordinates for click-to-black saturation coverage, where native hit testing excludes the lower boundary. Drag and keyboard paths use native input."
+ "packages/components/src/color-picker/test/index.browser.test.tsx": "Supplies exact bottom-edge mouse coordinates for click-to-black saturation coverage, where native hit testing excludes the lower boundary. Drag and keyboard paths use native input.",
+ "packages/components/src/number-control/test/index.browser.test.tsx": "Supplies identical touch and mouse PointerEvent sequences to verify the per-pointer-type drag policy; Browser Mode userEvent does not support touch drags."
},
"jsdomBrowserApis": {
"test/unit/config/setup-globals.vitest.jsdom.test.js": "This infrastructure regression must exercise the opt-in matchMedia mock in the jsdom project that installs it.",