Repository: biomejs/biome (Rust)

Issue:
HTML/Svelte/Astro/Vue formatting incorrectly lowercases component names that are variations of HTML element tag names
### Environment information

```bash
CLI:
  Version:                      2.3.0
  Color support:                true

Platform:
  CPU Architecture:             aarch64
  OS:                           macos

Environment:
  BIOME_LOG_PATH:               unset
  BIOME_LOG_PREFIX_NAME:        unset
  BIOME_CONFIG_PATH:            unset
  BIOME_THREADS:                unset
  NO_COLOR:                     unset
  TERM:                         xterm-256color
  JS_RUNTIME_VERSION:           v24.6.0
  JS_RUNTIME_NAME:              node
  NODE_PACKAGE_MANAGER:         pnpm/10.19.0

Biome Configuration:
  Status:                       Loaded successfully
  Path:                         biome.json
  Formatter enabled:            true
  Linter enabled:               true
  Assist enabled:               true
  VCS enabled:                  false

Formatter:
  Format with errors:           unset
  Indent style:                 Space
  Indent width:                 2
  Line ending:                  unset
  Line width:                   unset
  Attribute position:           unset
  Bracket spacing:              unset
  Includes:                     **/*.*, !node_modules, !.svelte-kit

JavaScript Formatter:
  Enabled:                      true
  JSX quote style:              Double Quotes
  Quote properties:             unset
  Trailing commas:              unset
  Semicolons:                   unset
  Arrow parentheses:            unset
  Bracket spacing:              unset
  Bracket same line:            unset
  Quote style:                  Double Quotes
  Indent style:                 unset
  Indent width:                 unset
  Line ending:                  unset
  Line width:                   unset
  Attribute position:           unset

JSON Formatter:
  Enabled:                      unset
  Indent style:                 unset
  Indent width:                 unset
  Line ending:                  unset
  Line width:                   unset
  Trailing Commas:              unset
  Expand lists:                 unset

CSS Formatter:
  Enabled:                      true
  Indent style:                 unset
  Indent width:                 unset
  Line ending:                  unset
  Line width:                   unset
  Quote style:                  Double Quotes

GraphQL Formatter:
  Enabled:                      unset
  Indent style:                 unset
  Indent width:                 unset
  Line ending:                  unset
  Line width:                   unset
  Bracket spacing:              unset
  Quote style:                  unset

Workspace:
  Open Documents:               0
```

### Configuration

```JSON
{
  "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
  "linter": {
    "enabled": true
  },
  "formatter": {
    "enabled": true,
    "includes": ["**/*.*", "!node_modules", "!.svelte-kit"],
    "indentStyle": "space",
    "indentWidth": 2
  },
  "javascript": {
    "formatter": {
      "enabled": true,
      "quoteStyle": "double",
      "jsxQuoteStyle": "double"
    }
  },
  "css": {
    "formatter": {
      "enabled": true,
      "quoteStyle": "double"
    },
    "parser": {
      "cssModules": true
    }
  },
  "html": {
    "experimentalFullSupportEnabled": true,
    "formatter": {
      "enabled": true,
      "indentWidth": 2
    }
  },
  "overrides": [
    {
      "includes": ["**/*.svelte"],
      "linter": {
        "rules": {
          "style": {
            "useConst": "off",
            "useImportType": "off"
          },
          "correctness": {
            "noUnusedVariables": "off",
            "noUnusedImports": "off"
          }
        }
      }
    }
  ]
}
```

### Playground link

https://biomejs.dev/playground/?files.main.svelte=PABzAGMAcgBpAHAAdAAgAGwAYQBuAGcAPQAiAHQAcwAiAD4ACgAgACAAaQBtAHAAbwByAHQAIAB7AEIAdQB0AHQAbwBuAH0AIABmAHIAbwBtACAAIgBiAHUAdAB0AG8AbgAuAHMAdgBlAGwAdABlACIACgA8AC8AcwBjAHIAaQBwAHQAPgAKAAoAPABCAHUAdAB0AG8AbgAgAGwAYQBiAGUAbAA9ACIAdABlAHMAdAAgAGIAdQB0AHQAbwBuACIALwA%2BAA%3D%3D&files.button.svelte=PABzAGMAcgBpAHAAdAAgAGwAYQBuAGcAPQAiAHQAcwAiAD4ACgAgACAAYwBvAG4AcwB0ACAAewAgAGwAYQBiAGUAbAAgAH0AOgAgAHsAbABhAGIAZQBsADoAIABzAHQAcgBpAG4AZwB9ACAAPQAgACQAcAByAG8AcABzACgAKQA7AAoAPAAvAHMAYwByAGkAcAB0AD4ACgAKADwAYgB1AHQAdABvAG4APgB7AGwAYQBiAGUAbAB9ADwALwBiAHUAdAB0AG8AbgA%2BAA%3D%3D

### Code of Conduct

- [x] I agree to follow Biome's Code of Conduct


Discussion: I think we can prevent name casing manipulation if it's not a html file 
I would think this is a necessary change since many UI libraries will use uppercase canonical tag names as their component names. 
- https://svelte.carbondesignsystem.com/components/Button
- https://sveltematerialui.com/demo/button/
- https://www.shadcn-svelte.com/docs/components/button
I'm interested in attempting to solve this issue. I can start today and report progress later. (first time contributor)
I made an initial pull request. Here is the key part of the diff, [`crates/biome_html_formatter/src/utils/metadata.rs` around line 720](https://github.com/biomejs/biome/pull/7869/files#diff-7667efddb391a54d39030f891da2c0d7932615de957d5535dda7ca06a162c8ecR726)
```Rust
pub(crate) fn is_canonical_html_tag(tag_name: &HtmlTagName, file_source: &HtmlFileSource) -> bool {
    // Only lowercase tags in pure HTML files, not in component frameworks
    if !file_source.is_html() {
        return false;
    }
```



Fix (patch):
diff --git a/.changeset/fix-html-component-tag-casing.md b/.changeset/fix-html-component-tag-casing.md
new file mode 100644
index 000000000000..7445f23f192c
--- /dev/null
+++ b/.changeset/fix-html-component-tag-casing.md
@@ -0,0 +1,5 @@
+---
+"@biomejs/biome": patch
+---
+
+Fixed [#7864](https://github.com/biomejs/biome/issues/7864): Biome now preserves component tag name casing in Svelte, Astro, and Vue files.
diff --git a/crates/biome_html_formatter/src/context.rs b/crates/biome_html_formatter/src/context.rs
index e1dbd0780e20..9955d7897fc1 100644
--- a/crates/biome_html_formatter/src/context.rs
+++ b/crates/biome_html_formatter/src/context.rs
@@ -9,8 +9,11 @@ use biome_html_syntax::{HtmlFileSource, HtmlLanguage};
 
 use crate::comments::{FormatHtmlComment, HtmlCommentStyle, HtmlComments};
 
-#[derive(Debug, Clone, Default)]
+#[derive(Debug, Clone)]
 pub struct HtmlFormatOptions {
+    /// The file source.
+    file_source: HtmlFileSource,
+
     /// The indent style.
     indent_style: IndentStyle,
 
@@ -46,13 +49,35 @@ pub struct HtmlFormatOptions {
     self_close_void_elements: SelfCloseVoidElements,
 }
 
+impl Default for HtmlFormatOptions {
+    fn default() -> Self {
+        Self {
+            file_source: HtmlFileSource::html(),
+            indent_style: IndentStyle::default(),
+            indent_width: IndentWidth::default(),
+            line_ending: LineEnding::default(),
+            line_width: LineWidth::default(),
+            attribute_position: AttributePosition::default(),
+            bracket_same_line: BracketSameLine::default(),
+            whitespace_sensitivity: WhitespaceSensitivity::default(),
+            indent_script_and_style: IndentScriptAndStyle::default(),
+            self_close_void_elements: SelfCloseVoidElements::default(),
+        }
+    }
+}
+
 impl HtmlFormatOptions {
-    pub fn new(_file_source: HtmlFileSource) -> Self {
+    pub fn new(file_source: HtmlFileSource) -> Self {
         Self {
+            file_source,
             ..Default::default()
         }
     }
 
+    pub fn file_source(&self) -> &HtmlFileSource {
+        &self.file_source
+    }
+
     pub fn with_indent_style(mut self, indent_style: IndentStyle) -> Self {
         self.indent_style = indent_style;
         self
diff --git a/crates/biome_html_formatter/src/html/auxiliary/opening_element.rs b/crates/biome_html_formatter/src/html/auxiliary/opening_element.rs
index aca7a34600b3..7c755da0a747 100644
--- a/crates/biome_html_formatter/src/html/auxiliary/opening_element.rs
+++ b/crates/biome_html_formatter/src/html/auxiliary/opening_element.rs
@@ -1,7 +1,7 @@
 use crate::{
     html::lists::attribute_list::FormatHtmlAttributeListOptions,
     prelude::*,
-    utils::metadata::{is_canonical_html_tag, is_element_whitespace_sensitive},
+    utils::metadata::{is_element_whitespace_sensitive, should_lowercase_html_tag},
 };
 use biome_formatter::{FormatRuleWithOptions, GroupId, write};
 use biome_html_syntax::{HtmlOpeningElement, HtmlOpeningElementFields};
@@ -48,7 +48,7 @@ impl FormatNodeRule<HtmlOpeningElement> for FormatHtmlOpeningElement {
         let l_angle_token = l_angle_token?;
         let name = name?;
         let is_whitespace_sensitive = is_element_whitespace_sensitive(f, &name);
-        let is_canonical_html_tag = is_canonical_html_tag(&name);
+        let is_canonical_html_element = should_lowercase_html_tag(f, &name);
 
         let bracket_same_line = f.options().bracket_same_line().value();
 
@@ -66,7 +66,7 @@ impl FormatNodeRule<HtmlOpeningElement> for FormatHtmlOpeningElement {
                 attributes
                     .format()
                     .with_options(FormatHtmlAttributeListOptions {
-                        is_canonical_html_element: is_canonical_html_tag,
+                        is_canonical_html_element,
                         tag_name: Some(name.clone()),
                     })
                     .fmt(f)?;
diff --git a/crates/biome_html_formatter/src/html/auxiliary/self_closing_element.rs b/crates/biome_html_formatter/src/html/auxiliary/self_closing_element.rs
index 07248b8b9bd4..67c864e22880 100644
--- a/crates/biome_html_formatter/src/html/auxiliary/self_closing_element.rs
+++ b/crates/biome_html_formatter/src/html/auxiliary/self_closing_element.rs
@@ -1,6 +1,6 @@
 use crate::{
     html::lists::attribute_list::FormatHtmlAttributeListOptions, prelude::*,
-    utils::metadata::is_canonical_html_tag,
+    utils::metadata::should_lowercase_html_tag,
 };
 use biome_formatter::write;
 use biome_html_syntax::{HtmlSelfClosingElement, HtmlSelfClosingElementFields};
@@ -18,7 +18,7 @@ impl FormatNodeRule<HtmlSelfClosingElement> for FormatHtmlSelfClosingElement {
         let bracket_same_line = f.options().bracket_same_line().value();
         let self_close_void_elements = f.options().self_close_void_elements();
         let name = name?;
-        let is_canonical_html_tag = is_canonical_html_tag(&name);
+        let is_canonical_html_element = should_lowercase_html_tag(f, &name);
 
         write!(f, [l_angle_token.format(), name.format()])?;
 
@@ -29,7 +29,7 @@ impl FormatNodeRule<HtmlSelfClosingElement> for FormatHtmlSelfClosingElement {
                 attributes
                     .format()
                     .with_options(FormatHtmlAttributeListOptions {
-                        is_canonical_html_element: is_canonical_html_tag,
+                        is_canonical_html_element,
                         tag_name: Some(name.clone()),
                     })
                     .fmt(f)?;
diff --git a/crates/biome_html_formatter/src/html/auxiliary/tag_name.rs b/crates/biome_html_formatter/src/html/auxiliary/tag_name.rs
index aa2e46a5cfac..f7eec8ba0c6b 100644
--- a/crates/biome_html_formatter/src/html/auxiliary/tag_name.rs
+++ b/crates/biome_html_formatter/src/html/auxiliary/tag_name.rs
@@ -1,6 +1,6 @@
 use crate::{
     prelude::*,
-    utils::{formatters::FormatTokenAsLowercase, metadata::is_canonical_html_tag},
+    utils::{formatters::FormatTokenAsLowercase, metadata::should_lowercase_html_tag},
 };
 use biome_formatter::write;
 use biome_html_syntax::{HtmlTagName, HtmlTagNameFields};
@@ -8,11 +8,9 @@ use biome_html_syntax::{HtmlTagName, HtmlTagNameFields};
 pub(crate) struct FormatHtmlTagName;
 impl FormatNodeRule<HtmlTagName> for FormatHtmlTagName {
     fn fmt_fields(&self, node: &HtmlTagName, f: &mut HtmlFormatter) -> FormatResult<()> {
-        // TODO: maybe move this check to a parent node so we aren't checking this twice per tag?
-        let is_canonical_html_tag = is_canonical_html_tag(node);
         let HtmlTagNameFields { value_token } = node.as_fields();
 
-        if is_canonical_html_tag {
+        if should_lowercase_html_tag(f, node) {
             write!(f, [FormatTokenAsLowercase::from(value_token?)])
         } else {
             write![f, [value_token.format()]]
diff --git a/crates/biome_html_formatter/src/utils/metadata.rs b/crates/biome_html_formatter/src/utils/metadata.rs
index 1d18d30a7a19..7774fff5cdfc 100644
--- a/crates/biome_html_formatter/src/utils/metadata.rs
+++ b/crates/biome_html_formatter/src/utils/metadata.rs
@@ -724,6 +724,14 @@ pub(crate) fn is_canonical_html_tag(tag_name: &HtmlTagName) -> bool {
     is_canonical_html_tag_name(tag_name.text_trimmed())
 }
 
+/// Whether a tag should be lowercased in the current formatting context.
+///
+/// Returns `true` only for canonical HTML tags in pure HTML files (.html).
+/// Component frameworks preserve tag name casing.
+pub(crate) fn should_lowercase_html_tag(f: &HtmlFormatter, tag_name: &HtmlTagName) -> bool {
+    f.options().file_source().is_html() && is_canonical_html_tag(tag_name)
+}
+
 /// Whether the given attribute name is a known HTML attribute for the given tag name.
 ///
 /// See [`HTML_ATTRIBUTES_BY_TAG`], [`HTML_GLOBAL_ATTRIBUTES`].