diff --git a/.changeset/lemon-impalas-check.md b/.changeset/lemon-impalas-check.md
new file mode 100644
index 000000000000..8ece8032d1d3
--- /dev/null
+++ b/.changeset/lemon-impalas-check.md
@@ -0,0 +1,5 @@
+---
+"@biomejs/biome": patch
+---
+
+Fixed [#8190](https://github.com/biomejs/biome/issues/8190): The HTML parser will now parse Vue event handlers that contain `:` correctly, e.g. `@update:modelValue="onUpdate"`.
diff --git a/crates/biome_html_formatter/tests/specs/html/vue/event-with-colon.vue b/crates/biome_html_formatter/tests/specs/html/vue/event-with-colon.vue
new file mode 100644
index 000000000000..0c264b19c63f
--- /dev/null
+++ b/crates/biome_html_formatter/tests/specs/html/vue/event-with-colon.vue
@@ -0,0 +1 @@
+
diff --git a/crates/biome_html_formatter/tests/specs/html/vue/event-with-colon.vue.snap b/crates/biome_html_formatter/tests/specs/html/vue/event-with-colon.vue.snap
new file mode 100644
index 000000000000..e8ab8faee7f0
--- /dev/null
+++ b/crates/biome_html_formatter/tests/specs/html/vue/event-with-colon.vue.snap
@@ -0,0 +1,33 @@
+---
+source: crates/biome_formatter_test/src/snapshot_builder.rs
+info: vue/event-with-colon.vue
+---
+# Input
+
+```vue
+
+
+```
+
+
+=============================
+
+# Outputs
+
+## Output 1
+
+-----
+Indent style: Tab
+Indent width: 2
+Line ending: LF
+Line width: 80
+Attribute Position: Auto
+Bracket same line: false
+Whitespace sensitivity: css
+Indent script and style: false
+Self close void elements: never
+-----
+
+```vue
+
+```
diff --git a/crates/biome_html_parser/src/lexer/mod.rs b/crates/biome_html_parser/src/lexer/mod.rs
index f576d9a48c09..bdc290be2be5 100644
--- a/crates/biome_html_parser/src/lexer/mod.rs
+++ b/crates/biome_html_parser/src/lexer/mod.rs
@@ -35,6 +35,7 @@ enum IdentifierContext {
Doctype,
Svelte,
Vue,
+ VueDirectiveArgument,
}
impl IdentifierContext {
@@ -147,6 +148,9 @@ impl<'src> HtmlLexer<'src> {
// https://html.spec.whatwg.org/multipage/syntax.html#start-tags
self.consume_tag_name(current)
}
+ _ if (self.current_kind == T![@] && is_attribute_name_byte_vue(current)) => {
+ self.consume_identifier(current, IdentifierContext::VueDirectiveArgument)
+ }
_ if (self.current_kind != T![<] && is_attribute_name_byte_vue(current)) => {
self.consume_identifier(current, IdentifierContext::Vue)
}
@@ -471,6 +475,18 @@ impl<'src> HtmlLexer<'src> {
len += 1;
}
+ self.advance(1)
+ } else {
+ break;
+ }
+ }
+ IdentifierContext::VueDirectiveArgument => {
+ if is_attribute_name_byte_vue(byte) || byte == b':' {
+ if len < BUFFER_SIZE {
+ buffer[len] = byte;
+ len += 1;
+ }
+
self.advance(1)
} else {
break;
diff --git a/crates/biome_html_parser/tests/html_specs/ok/vue/event-with-colon.vue b/crates/biome_html_parser/tests/html_specs/ok/vue/event-with-colon.vue
new file mode 100644
index 000000000000..0c264b19c63f
--- /dev/null
+++ b/crates/biome_html_parser/tests/html_specs/ok/vue/event-with-colon.vue
@@ -0,0 +1 @@
+
diff --git a/crates/biome_html_parser/tests/html_specs/ok/vue/event-with-colon.vue.snap b/crates/biome_html_parser/tests/html_specs/ok/vue/event-with-colon.vue.snap
new file mode 100644
index 000000000000..483aa9fde51e
--- /dev/null
+++ b/crates/biome_html_parser/tests/html_specs/ok/vue/event-with-colon.vue.snap
@@ -0,0 +1,75 @@
+---
+source: crates/biome_html_parser/tests/spec_test.rs
+expression: snapshot
+---
+## Input
+
+```vue
+
+
+```
+
+
+## AST
+
+```
+HtmlRoot {
+ bom_token: missing (optional),
+ frontmatter: missing (optional),
+ directive: missing (optional),
+ html: HtmlElementList [
+ HtmlSelfClosingElement {
+ l_angle_token: L_ANGLE@0..1 "<" [] [],
+ name: HtmlTagName {
+ value_token: HTML_LITERAL@1..5 "Foo" [] [Whitespace(" ")],
+ },
+ attributes: HtmlAttributeList [
+ VueVOnShorthandDirective {
+ at_token: AT@5..6 "@" [] [],
+ arg: VueStaticArgument {
+ name_token: HTML_LITERAL@6..23 "update:modelValue" [] [],
+ },
+ modifiers: VueModifierList [],
+ initializer: HtmlAttributeInitializerClause {
+ eq_token: EQ@23..24 "=" [] [],
+ value: HtmlString {
+ value_token: HTML_STRING_LITERAL@24..35 "\"onUpdate\"" [] [Whitespace(" ")],
+ },
+ },
+ },
+ ],
+ slash_token: SLASH@35..36 "/" [] [],
+ r_angle_token: R_ANGLE@36..37 ">" [] [],
+ },
+ ],
+ eof_token: EOF@37..38 "" [Newline("\n")] [],
+}
+```
+
+## CST
+
+```
+0: HTML_ROOT@0..38
+ 0: (empty)
+ 1: (empty)
+ 2: (empty)
+ 3: HTML_ELEMENT_LIST@0..37
+ 0: HTML_SELF_CLOSING_ELEMENT@0..37
+ 0: L_ANGLE@0..1 "<" [] []
+ 1: HTML_TAG_NAME@1..5
+ 0: HTML_LITERAL@1..5 "Foo" [] [Whitespace(" ")]
+ 2: HTML_ATTRIBUTE_LIST@5..35
+ 0: VUE_V_ON_SHORTHAND_DIRECTIVE@5..35
+ 0: AT@5..6 "@" [] []
+ 1: VUE_STATIC_ARGUMENT@6..23
+ 0: HTML_LITERAL@6..23 "update:modelValue" [] []
+ 2: VUE_MODIFIER_LIST@23..23
+ 3: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@23..35
+ 0: EQ@23..24 "=" [] []
+ 1: HTML_STRING@24..35
+ 0: HTML_STRING_LITERAL@24..35 "\"onUpdate\"" [] [Whitespace(" ")]
+ 3: SLASH@35..36 "/" [] []
+ 4: R_ANGLE@36..37 ">" [] []
+ 4: EOF@37..38 "" [Newline("\n")] []
+
+```