Repository: biomejs/biome (2026-04-15T12:28:08Z)

Issue:
🐛 Cannot parse nested CSS block inside embedded snippets when a property follows
### Environment information

Playground

### What happened?

https://biomejs.dev/playground/?lintRules=noFloatingPromises&tab=formatter&pane=Diagnostics&code=LwAvACAAVwBvAHIAawBzAAoAcwB0AHkAbABlAGQALgBkAGkAdgBgAAoAIAAgAGIAYQBjAGsAZwByAG8AdQBuAGQAOgAgAGIAbABhAGMAawA7AAoAIAAgAGQAaQB2ADoAZgBpAHIAcwB0AC0AbwBmAC0AdAB5AHAAZQAgAHsACgAgACAAIAAgAGMAbwBsAG8AcgA6ACAAYgBsAGEAYwBrADsACgAgACAAfQAKAGAAOwAKAAoALwAvACAARABvAGUAcwAgAG4AbwB0ACAAdwBvAHIAawAKAHMAdAB5AGwAZQBkAC4AZABpAHYAYAAKACAAIABkAGkAdgA6AGYAaQByAHMAdAAtAG8AZgAtAHQAeQBwAGUAIAB7AAoAIAAgACAAIABjAG8AbABvAHIAOgAgAGIAbABhAGMAawA7AAoAIAAgAH0ACgAgACAAYgBhAGMAawBnAHIAbwB1AG4AZAA6ACAAYgBsAGEAYwBrADsACgBgADsACgA%3D

### Expected result

No parse error

### Code of Conduct

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


Discussion:

Patch:
diff --git a/.changeset/fix-issue-9994-styled-css.md b/.changeset/fix-issue-9994-styled-css.md
new file mode 100644
index 000000000000..c08dcc4c713c
--- /dev/null
+++ b/.changeset/fix-issue-9994-styled-css.md
@@ -0,0 +1,5 @@
+---
+"@biomejs/biome": patch
+---
+
+Fixed [#9994](https://github.com/biomejs/biome/issues/9994): Biome now parses nested CSS rules correctly when declarations follow them inside embedded snippets.
diff --git a/crates/biome_css_parser/src/lib.rs b/crates/biome_css_parser/src/lib.rs
index 270b17ed63d1..250b75472f33 100644
--- a/crates/biome_css_parser/src/lib.rs
+++ b/crates/biome_css_parser/src/lib.rs
@@ -352,4 +352,26 @@ mod tests {
             );
         }
     }
+
+    #[test]
+    fn styled_snippet_allows_nested_rule_before_declaration() {
+        let css_code = r#"
+div:first-of-type {
+    color: black;
+}
+background: black;
+"#;
+
+        let parse = parse_css(
+            css_code,
+            CssFileSource::css().with_embedding_kind(EmbeddingKind::Styled),
+            CssParserOptions::default(),
+        );
+
+        assert!(
+            !parse.has_errors(),
+            "styled embedded CSS should parse without errors: {:?}",
+            parse.diagnostics()
+        );
+    }
 }
diff --git a/crates/biome_css_parser/src/syntax/block/declaration_or_rule_list_block.rs b/crates/biome_css_parser/src/syntax/block/declaration_or_rule_list_block.rs
index 61d8168f386e..fa46e3f0efb4 100644
--- a/crates/biome_css_parser/src/syntax/block/declaration_or_rule_list_block.rs
+++ b/crates/biome_css_parser/src/syntax/block/declaration_or_rule_list_block.rs
@@ -9,11 +9,12 @@ use crate::syntax::scss::{
 };
 use crate::syntax::{
     CssSyntaxFeatures, is_at_any_declaration_with_semicolon, is_at_metavariable,
-    is_at_nested_qualified_rule, parse_any_declaration_with_semicolon, parse_metavariable,
-    parse_nested_qualified_rule, try_parse,
+    is_at_nested_qualified_rule, is_at_qualified_rule, parse_any_declaration_with_semicolon,
+    parse_metavariable, parse_nested_qualified_rule, parse_qualified_rule, try_parse,
     try_parse_nested_qualified_rule_without_selector_recovery,
 };
 use biome_css_syntax::CssSyntaxKind::*;
+use biome_css_syntax::EmbeddingKind;
 use biome_css_syntax::{CssSyntaxKind, T};
 use biome_parser::parse_lists::ParseNodeList;
 use biome_parser::parse_recovery::{ParseRecovery, RecoveryResult};
@@ -43,6 +44,7 @@ impl ParseBlockBody for DeclarationOrRuleListBlock {
 #[inline]
 fn is_at_declaration_or_rule_item(p: &mut CssParser) -> bool {
     is_at_at_rule(p)
+        || is_at_top_level_qualified_rule(p)
         || is_at_nested_qualified_rule(p)
         || is_at_scss_nesting_declaration(p)
         || is_at_scss_declaration(p)
@@ -51,6 +53,13 @@ fn is_at_declaration_or_rule_item(p: &mut CssParser) -> bool {
         || is_at_metavariable(p)
 }
 
+#[inline]
+fn is_at_top_level_qualified_rule(p: &mut CssParser) -> bool {
+    !p.state().is_nesting_block
+        && matches!(p.source_type.as_embedding_kind(), EmbeddingKind::Styled)
+        && is_at_qualified_rule(p)
+}
+
 struct DeclarationOrRuleListParseRecovery {
     end_kind: CssSyntaxKind,
 }
@@ -162,7 +171,11 @@ impl ParseNodeList for DeclarationOrRuleList {
                 // } <---
                 // The closing brace indicates the end of the declaration block.
                 // If either condition is true, the declaration is considered valid.
-                let valid = matches!(p.last(), Some(T![;])) || p.at(self.end_kind);
+                // Reject speculative declaration parses that stop at the end of an inner block
+                // rather than the current declaration-or-rule list.
+                // This avoids misclassifying selectors as declarations in embedded snippets.
+                let valid =
+                    (matches!(p.last(), Some(T![;])) && !p.at(T!['}'])) || p.at(self.end_kind);
                 if valid { Ok(declaration) } else { Err(()) }
             });
 
@@ -199,6 +212,26 @@ impl ParseNodeList for DeclarationOrRuleList {
                 return rule;
             }
 
+            // Styled snippets allow top-level qualified rules, but plain
+            // declarations like `background: black;` are ambiguous with a
+            // selector prefix. Only treat the construct as a qualified rule
+            // after the speculative declaration parse has already failed.
+            if is_at_top_level_qualified_rule(p) {
+                let rule = try_parse(p, |p| {
+                    let rule = parse_qualified_rule(p);
+
+                    if p.at(self.end_kind) || is_at_declaration_or_rule_item(p) {
+                        Ok(rule)
+                    } else {
+                        Err(())
+                    }
+                });
+
+                if let Ok(rule) = rule {
+                    return rule;
+                }
+            }
+
             // If both parsing attempts fail,
             // fall back to parsing the block as a declaration,
             // because declaration error is more relevant.


Test patch:
diff --git a/crates/biome_service/src/workspace/server.tests.rs b/crates/biome_service/src/workspace/server.tests.rs
index 60bd18164ae6..7a194a9c2ffb 100644
--- a/crates/biome_service/src/workspace/server.tests.rs
+++ b/crates/biome_service/src/workspace/server.tests.rs
@@ -833,6 +833,89 @@ const PortfolioIcon = styled.div`
     ");
 }
 
+#[test]
+fn issue_9994() {
+    const FILE_PATH: &str = "/project/file.js";
+    const FILE_CONTENT: &str = r#"styled.div`
+  div:first-of-type {
+    color: black;
+  }
+  background: black;
+`;
+"#;
+
+    let fs = MemoryFileSystem::default();
+    fs.insert(Utf8PathBuf::from(FILE_PATH), FILE_CONTENT);
+
+    let (workspace, project_key) = setup_workspace_and_open_project(fs, "/");
+
+    workspace
+        .update_settings(UpdateSettingsParams {
+            project_key,
+            workspace_directory: None,
+            configuration: Configuration {
+                javascript: Some(JsConfiguration {
+                    experimental_embedded_snippets_enabled: Some(true.into()),
+                    ..Default::default()
+                }),
+                ..Default::default()
+            },
+            extended_configurations: vec![],
+            module_graph_resolution_kind: ModuleGraphResolutionKind::None,
+        })
+        .unwrap();
+
+    workspace
+        .open_file(OpenFileParams {
+            project_key,
+            path: BiomePath::new(FILE_PATH),
+            content: FileContent::FromServer,
+            document_file_source: None,
+            persist_node_cache: false,
+            inline_config: None,
+        })
+        .unwrap();
+
+    let diagnostics = workspace
+        .pull_diagnostics(PullDiagnosticsParams {
+            project_key,
+            path: BiomePath::new(FILE_PATH),
+            only: vec![],
+            skip: vec![],
+            enabled_rules: vec![],
+            categories: Default::default(),
+            include_code_fix: false,
+            inline_config: None,
+            max_diagnostics: None,
+            diagnostic_level: Severity::Error,
+            enforce_assist: false,
+        })
+        .unwrap();
+
+    assert!(
+        diagnostics.diagnostics.is_empty(),
+        "Expected no diagnostics for issue #9994, got: {:#?}",
+        diagnostics.diagnostics
+    );
+
+    let result = workspace
+        .format_file(FormatFileParams {
+            project_key,
+            path: Utf8PathBuf::from(FILE_PATH).into(),
+            inline_config: None,
+        })
+        .unwrap();
+
+    insta::assert_snapshot!(result.as_code(), @r#"
+    styled.div`
+    	div:first-of-type {
+    		color: black;
+    	}
+    	background: black;
+    `;
+    "#);
+}
+
 #[test]
 fn issue_9113() {
     const FILE_PATH: &str = "/project/file.ts";