diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap
index 17c4e80b160..b8ddb5812ba 100644
--- a/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap
+++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap
@@ -26,6 +26,21 @@ return function render(_ctx, _cache) {
 }"
 `;
 
+exports[`compiler: transform v-model > no expression 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+  with (_ctx) {
+    const { openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
+
+    return (_openBlock(), _createElementBlock("input", {
+      "foo-value": fooValue,
+      "onUpdate:fooValue": $event => ((fooValue) = $event)
+    }, null, 40 /* PROPS, NEED_HYDRATION */, ["foo-value", "onUpdate:fooValue"]))
+  }
+}"
+`;
+
 exports[`compiler: transform v-model > simple expression (with multilines) 1`] = `
 "const _Vue = Vue
 
diff --git a/packages/compiler-core/__tests__/transforms/vModel.spec.ts b/packages/compiler-core/__tests__/transforms/vModel.spec.ts
index 82dd4909fd6..6915e7f499d 100644
--- a/packages/compiler-core/__tests__/transforms/vModel.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/vModel.spec.ts
@@ -394,6 +394,42 @@ describe('compiler: transform v-model', () => {
     expect(generate(root, { mode: 'module' }).code).toMatchSnapshot()
   })
 
+  test('no expression', () => {
+    const root = parseWithVModel('<input v-model:foo-value />')
+    const node = root.children[0] as ElementNode
+    const props = ((node.codegenNode as VNodeCall).props as ObjectExpression)
+      .properties
+    expect(props[0]).toMatchObject({
+      key: {
+        content: 'foo-value',
+        isStatic: true,
+      },
+      value: {
+        content: 'fooValue',
+        isStatic: false,
+      },
+    })
+
+    expect(props[1]).toMatchObject({
+      key: {
+        content: 'onUpdate:fooValue',
+        isStatic: true,
+      },
+      value: {
+        children: [
+          '$event => ((',
+          {
+            content: 'fooValue',
+            isStatic: false,
+          },
+          ') = $event)',
+        ],
+      },
+    })
+
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
   test('should cache update handler w/ cacheHandlers: true', () => {
     const root = parseWithVModel('<input v-model="foo" />', {
       prefixIdentifiers: true,
@@ -508,14 +544,26 @@ describe('compiler: transform v-model', () => {
   })
 
   describe('errors', () => {
-    test('missing expression', () => {
+    test('missing argument and expression', () => {
       const onError = vi.fn()
       parseWithVModel('<span v-model />', { onError })
 
       expect(onError).toHaveBeenCalledTimes(1)
       expect(onError).toHaveBeenCalledWith(
         expect.objectContaining({
-          code: ErrorCodes.X_V_MODEL_NO_EXPRESSION,
+          code: ErrorCodes.X_V_MODEL_NO_ARGUMENT_AND_EXPRESSION,
+        }),
+      )
+    })
+
+    test('invalid argument for same-name shorthand', () => {
+      const onError = vi.fn()
+      parseWithVModel(`<div v-model:[arg] />`, { onError })
+
+      expect(onError).toHaveBeenCalledTimes(1)
+      expect(onError).toHaveBeenCalledWith(
+        expect.objectContaining({
+          code: ErrorCodes.X_V_MODEL_INVALID_SAME_NAME_ARGUMENT,
         }),
       )
     })
diff --git a/packages/compiler-core/src/errors.ts b/packages/compiler-core/src/errors.ts
index 58e113ab19e..c980fbc7b38 100644
--- a/packages/compiler-core/src/errors.ts
+++ b/packages/compiler-core/src/errors.ts
@@ -84,7 +84,8 @@ export enum ErrorCodes {
   X_V_SLOT_DUPLICATE_SLOT_NAMES,
   X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
   X_V_SLOT_MISPLACED,
-  X_V_MODEL_NO_EXPRESSION,
+  X_V_MODEL_NO_ARGUMENT_AND_EXPRESSION,
+  X_V_MODEL_INVALID_SAME_NAME_ARGUMENT,
   X_V_MODEL_MALFORMED_EXPRESSION,
   X_V_MODEL_ON_SCOPE_VARIABLE,
   X_V_MODEL_ON_PROPS,
@@ -172,7 +173,8 @@ export const errorMessages: Record<ErrorCodes, string> = {
     `Extraneous children found when component already has explicitly named ` +
     `default slot. These children will be ignored.`,
   [ErrorCodes.X_V_SLOT_MISPLACED]: `v-slot can only be used on components or <template> tags.`,
-  [ErrorCodes.X_V_MODEL_NO_EXPRESSION]: `v-model is missing expression.`,
+  [ErrorCodes.X_V_MODEL_NO_ARGUMENT_AND_EXPRESSION]: `v-model is missing argument and expression.`,
+  [ErrorCodes.X_V_MODEL_INVALID_SAME_NAME_ARGUMENT]: `v-model with same-name shorthand only allows static argument.`,
   [ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model value must be a valid JavaScript member expression.`,
   [ErrorCodes.X_V_MODEL_ON_SCOPE_VARIABLE]: `v-model cannot be used on v-for or v-slot scope variables because they are not writable.`,
   [ErrorCodes.X_V_MODEL_ON_PROPS]: `v-model cannot be used on a prop, because local prop bindings are not writable.\nUse a v-bind binding combined with a v-on listener that emits update:x event instead.`,
diff --git a/packages/compiler-core/src/transforms/vModel.ts b/packages/compiler-core/src/transforms/vModel.ts
index 598c1ea4387..927627b3097 100644
--- a/packages/compiler-core/src/transforms/vModel.ts
+++ b/packages/compiler-core/src/transforms/vModel.ts
@@ -19,14 +19,39 @@ import {
 import { IS_REF } from '../runtimeHelpers'
 import { BindingTypes } from '../options'
 import { camelize } from '@vue/shared'
+import { transformBindShorthand } from './vBind'
 
 export const transformModel: DirectiveTransform = (dir, node, context) => {
-  const { exp, arg } = dir
+  const { arg, loc } = dir
+  let { exp } = dir
+
   if (!exp) {
-    context.onError(
-      createCompilerError(ErrorCodes.X_V_MODEL_NO_EXPRESSION, dir.loc),
-    )
-    return createTransformProps()
+    if (!arg) {
+      context.onError(
+        createCompilerError(
+          ErrorCodes.X_V_MODEL_NO_ARGUMENT_AND_EXPRESSION,
+          dir.loc,
+        ),
+      )
+      return createTransformProps()
+    }
+
+    // same-name shorthand - v-model:arg is expanded to v-model:arg="arg"
+    if (arg.type !== NodeTypes.SIMPLE_EXPRESSION || !arg.isStatic) {
+      // only simple expression is allowed for same-name shorthand
+      context.onError(
+        createCompilerError(
+          ErrorCodes.X_V_MODEL_INVALID_SAME_NAME_ARGUMENT,
+          arg.loc,
+        ),
+      )
+      return createTransformProps([
+        createObjectProperty(arg, createSimpleExpression('', true, loc)),
+      ])
+    }
+
+    transformBindShorthand(dir, context)
+    exp = dir.exp!
   }
 
   // we assume v-model directives are always parsed
diff --git a/packages/compiler-dom/src/errors.ts b/packages/compiler-dom/src/errors.ts
index b47624840ab..5bffa4e53a1 100644
--- a/packages/compiler-dom/src/errors.ts
+++ b/packages/compiler-dom/src/errors.ts
@@ -21,7 +21,7 @@ export function createDOMCompilerError(
 }
 
 export enum DOMErrorCodes {
-  X_V_HTML_NO_EXPRESSION = 53 /* ErrorCodes.__EXTEND_POINT__ */,
+  X_V_HTML_NO_EXPRESSION = 54 /* ErrorCodes.__EXTEND_POINT__ */,
   X_V_HTML_WITH_CHILDREN,
   X_V_TEXT_NO_EXPRESSION,
   X_V_TEXT_WITH_CHILDREN,