diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts
index 51113edef69..ef5051f42f7 100644
--- a/packages/runtime-dom/__tests__/customElement.spec.ts
+++ b/packages/runtime-dom/__tests__/customElement.spec.ts
@@ -223,6 +223,21 @@ describe('defineCustomElement', () => {
expect(e.getAttribute('baz-qux')).toBe('four')
})
+ test('props via hyphen property', async () => {
+ const Comp = defineCustomElement({
+ props: {
+ fooBar: Boolean,
+ },
+ render() {
+ return 'Comp'
+ },
+ })
+ customElements.define('my-el-comp', Comp)
+ render(h('my-el-comp', { 'foo-bar': true }), container)
+ const el = container.children[0]
+ expect((el as any).outerHTML).toBe('')
+ })
+
test('attribute -> prop type casting', async () => {
const E = defineCustomElement({
props: {
diff --git a/packages/runtime-dom/src/modules/props.ts b/packages/runtime-dom/src/modules/props.ts
index 93d45c9e160..86424eb0d66 100644
--- a/packages/runtime-dom/src/modules/props.ts
+++ b/packages/runtime-dom/src/modules/props.ts
@@ -1,5 +1,5 @@
import { DeprecationTypes, compatUtils, warn } from '@vue/runtime-core'
-import { includeBooleanAttr } from '@vue/shared'
+import { camelize, includeBooleanAttr } from '@vue/shared'
import { unsafeToTrustedHTML } from '../nodeOps'
// functions. The user is responsible for using them with only trusted content.
@@ -95,7 +95,7 @@ export function patchDOMProp(
// some properties has getter, no setter, will error in 'use strict'
// eg.
try {
- el[key] = value
+ el[camelize(key)] = value
} catch (e: any) {
// do not warn if value is auto-coerced from nullish values
if (__DEV__ && !needRemove) {