diff --git a/apps/oxfmt/test/cli/embedded_languages/__snapshots__/embedded_languages.test.ts.snap b/apps/oxfmt/test/cli/embedded_languages/__snapshots__/embedded_languages.test.ts.snap
index 69f5530c5f7e6..8dbd5b5c517b2 100644
--- a/apps/oxfmt/test/cli/embedded_languages/__snapshots__/embedded_languages.test.ts.snap
+++ b/apps/oxfmt/test/cli/embedded_languages/__snapshots__/embedded_languages.test.ts.snap
@@ -12,6 +12,26 @@ const styles = css\`.button{color:red;background:blue;padding:10px 20px;}.contai
const styledComponent = styled\`background-color:#ffffff;border-radius:4px;padding:8px;\`;
+// ============================================================================
+// Member Expression Tags - styled.div, styled.button, etc.
+// ============================================================================
+
+const cssGlobal = css.global\`.reset{margin:0;padding:0;box-sizing:border-box;}\`;
+
+const styledDiv = styled.div\`width:100%;height:100vh;background-color:#f0f0f0;\`;
+
+const styledLink = styled["a"]\`text-decoration:none;color:#007bff;font-weight:bold;\`;
+
+const styledButton = styled(Button)\`font-size:16px;color:#333;padding:12px 24px;\`;
+
+// ============================================================================
+// Plain template strings within css props or styled-jsx
+// ============================================================================
+
+const cssProp =
Hello World
;
+
+const styledJsx = ;
+
// ============================================================================
// GraphQL - Tagged template literals with gql and graphql tags
// ============================================================================
@@ -48,6 +68,8 @@ npm install package
const mixedStyles = css\`.button{color:red;}\`;
+const mixedStyled = styled.button\`padding:10px;\`;
+
const mixedQuery = gql\`query{users{name}}\`;
const mixedTemplate = html\`Title
\`;
@@ -157,6 +179,62 @@ const styledComponent = styled\`
padding: 8px;
\`;
+// ============================================================================
+// Member Expression Tags - styled.div, styled.button, etc.
+// ============================================================================
+
+const cssGlobal = css.global\`
+ .reset {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ }
+\`;
+
+const styledDiv = styled.div\`
+ width: 100%;
+ height: 100vh;
+ background-color: #f0f0f0;
+\`;
+
+const styledLink = styled["a"]\`
+ text-decoration: none;
+ color: #007bff;
+ font-weight: bold;
+\`;
+
+const styledButton = styled(Button)\`
+ font-size: 16px;
+ color: #333;
+ padding: 12px 24px;
+\`;
+
+// ============================================================================
+// Plain template strings within css props or styled-jsx
+// ============================================================================
+
+const cssProp = (
+
+ Hello World
+
+);
+
+const styledJsx = (
+
+);
+
// ============================================================================
// GraphQL - Tagged template literals with gql and graphql tags
// ============================================================================
@@ -225,6 +303,10 @@ const mixedStyles = css\`
}
\`;
+const mixedStyled = styled.button\`
+ padding: 10px;
+\`;
+
const mixedQuery = gql\`
query {
users {
@@ -348,6 +430,26 @@ const styles = css\`.button{color:red;background:blue;padding:10px 20px;}.contai
const styledComponent = styled\`background-color:#ffffff;border-radius:4px;padding:8px;\`;
+// ============================================================================
+// Member Expression Tags - styled.div, styled.button, etc.
+// ============================================================================
+
+const cssGlobal = css.global\`.reset{margin:0;padding:0;box-sizing:border-box;}\`;
+
+const styledDiv = styled.div\`width:100%;height:100vh;background-color:#f0f0f0;\`;
+
+const styledLink = styled["a"]\`text-decoration:none;color:#007bff;font-weight:bold;\`;
+
+const styledButton = styled(Button)\`font-size:16px;color:#333;padding:12px 24px;\`;
+
+// ============================================================================
+// Plain template strings within css props or styled-jsx
+// ============================================================================
+
+const cssProp = Hello World
;
+
+const styledJsx = ;
+
// ============================================================================
// GraphQL - Tagged template literals with gql and graphql tags
// ============================================================================
@@ -384,6 +486,8 @@ npm install package
const mixedStyles = css\`.button{color:red;}\`;
+const mixedStyled = styled.button\`padding:10px;\`;
+
const mixedQuery = gql\`query{users{name}}\`;
const mixedTemplate = html\`Title
\`;
@@ -493,6 +597,62 @@ const styledComponent = styled\`
padding: 8px;
\`;
+// ============================================================================
+// Member Expression Tags - styled.div, styled.button, etc.
+// ============================================================================
+
+const cssGlobal = css.global\`
+ .reset {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ }
+\`;
+
+const styledDiv = styled.div\`
+ width: 100%;
+ height: 100vh;
+ background-color: #f0f0f0;
+\`;
+
+const styledLink = styled["a"]\`
+ text-decoration: none;
+ color: #007bff;
+ font-weight: bold;
+\`;
+
+const styledButton = styled(Button)\`
+ font-size: 16px;
+ color: #333;
+ padding: 12px 24px;
+\`;
+
+// ============================================================================
+// Plain template strings within css props or styled-jsx
+// ============================================================================
+
+const cssProp = (
+
+ Hello World
+
+);
+
+const styledJsx = (
+
+);
+
// ============================================================================
// GraphQL - Tagged template literals with gql and graphql tags
// ============================================================================
@@ -561,6 +721,10 @@ const mixedStyles = css\`
}
\`;
+const mixedStyled = styled.button\`
+ padding: 10px;
+\`;
+
const mixedQuery = gql\`
query {
users {
@@ -684,6 +848,26 @@ const styles = css\`.button{color:red;background:blue;padding:10px 20px;}.contai
const styledComponent = styled\`background-color:#ffffff;border-radius:4px;padding:8px;\`;
+// ============================================================================
+// Member Expression Tags - styled.div, styled.button, etc.
+// ============================================================================
+
+const cssGlobal = css.global\`.reset{margin:0;padding:0;box-sizing:border-box;}\`;
+
+const styledDiv = styled.div\`width:100%;height:100vh;background-color:#f0f0f0;\`;
+
+const styledLink = styled["a"]\`text-decoration:none;color:#007bff;font-weight:bold;\`;
+
+const styledButton = styled(Button)\`font-size:16px;color:#333;padding:12px 24px;\`;
+
+// ============================================================================
+// Plain template strings within css props or styled-jsx
+// ============================================================================
+
+const cssProp = Hello World
;
+
+const styledJsx = ;
+
// ============================================================================
// GraphQL - Tagged template literals with gql and graphql tags
// ============================================================================
@@ -720,6 +904,8 @@ npm install package
const mixedStyles = css\`.button{color:red;}\`;
+const mixedStyled = styled.button\`padding:10px;\`;
+
const mixedQuery = gql\`query{users{name}}\`;
const mixedTemplate = html\`Title
\`;
@@ -815,6 +1001,32 @@ const styles = css\`.button{color:red;background:blue;padding:10px 20px;}.contai
const styledComponent = styled\`background-color:#ffffff;border-radius:4px;padding:8px;\`;
+// ============================================================================
+// Member Expression Tags - styled.div, styled.button, etc.
+// ============================================================================
+
+const cssGlobal = css.global\`.reset{margin:0;padding:0;box-sizing:border-box;}\`;
+
+const styledDiv = styled.div\`width:100%;height:100vh;background-color:#f0f0f0;\`;
+
+const styledLink = styled["a"]\`text-decoration:none;color:#007bff;font-weight:bold;\`;
+
+const styledButton = styled(Button)\`font-size:16px;color:#333;padding:12px 24px;\`;
+
+// ============================================================================
+// Plain template strings within css props or styled-jsx
+// ============================================================================
+
+const cssProp = (
+
+ Hello World
+
+);
+
+const styledJsx = (
+
+);
+
// ============================================================================
// GraphQL - Tagged template literals with gql and graphql tags
// ============================================================================
@@ -851,6 +1063,8 @@ npm install package
const mixedStyles = css\`.button{color:red;}\`;
+const mixedStyled = styled.button\`padding:10px;\`;
+
const mixedQuery = gql\`query{users{name}}\`;
const mixedTemplate = html\`Title
\`;
diff --git a/apps/oxfmt/test/cli/embedded_languages/fixtures/embedded_languages.js b/apps/oxfmt/test/cli/embedded_languages/fixtures/embedded_languages.js
index 1719c57f9ee06..22a6895552ace 100644
--- a/apps/oxfmt/test/cli/embedded_languages/fixtures/embedded_languages.js
+++ b/apps/oxfmt/test/cli/embedded_languages/fixtures/embedded_languages.js
@@ -6,6 +6,26 @@ const styles = css`.button{color:red;background:blue;padding:10px 20px;}.contain
const styledComponent = styled`background-color:#ffffff;border-radius:4px;padding:8px;`;
+// ============================================================================
+// Member Expression Tags - styled.div, styled.button, etc.
+// ============================================================================
+
+const cssGlobal = css.global`.reset{margin:0;padding:0;box-sizing:border-box;}`;
+
+const styledDiv = styled.div`width:100%;height:100vh;background-color:#f0f0f0;`;
+
+const styledLink = styled["a"]`text-decoration:none;color:#007bff;font-weight:bold;`;
+
+const styledButton = styled(Button)`font-size:16px;color:#333;padding:12px 24px;`;
+
+// ============================================================================
+// Plain template strings within css props or styled-jsx
+// ============================================================================
+
+const cssProp = Hello World
;
+
+const styledJsx = ;
+
// ============================================================================
// GraphQL - Tagged template literals with gql and graphql tags
// ============================================================================
@@ -42,6 +62,8 @@ npm install package
const mixedStyles = css`.button{color:red;}`;
+const mixedStyled = styled.button`padding:10px;`;
+
const mixedQuery = gql`query{users{name}}`;
const mixedTemplate = html`Title
`;
diff --git a/crates/oxc_formatter/src/print/template.rs b/crates/oxc_formatter/src/print/template.rs
index 027fe93f81562..0427ac38c992b 100644
--- a/crates/oxc_formatter/src/print/template.rs
+++ b/crates/oxc_formatter/src/print/template.rs
@@ -8,7 +8,7 @@ use oxc_span::{GetSpan, Span};
use crate::{
ExternalCallbacks, IndentWidth,
- ast_nodes::{AstNode, AstNodeIterator},
+ ast_nodes::{AstNode, AstNodeIterator, AstNodes},
format_args,
formatter::{
Format, FormatElement, Formatter, TailwindContextEntry, VecBuffer,
@@ -29,6 +29,9 @@ use super::FormatWrite;
impl<'a> FormatWrite<'a> for AstNode<'a, TemplateLiteral<'a>> {
fn write(&self, f: &mut Formatter<'_, 'a>) {
+ if try_format_css_template(self, f) {
+ return;
+ }
let template = TemplateLike::TemplateLiteral(self);
write!(f, template);
}
@@ -758,6 +761,46 @@ impl<'a> Format<'a> for EachTemplateTable<'a> {
}
}
+fn get_tag_name<'a>(expr: &'a Expression<'a>) -> Option<&'a str> {
+ let expr = expr.get_inner_expression();
+ match expr {
+ Expression::Identifier(ident) => Some(ident.name.as_str()),
+ Expression::StaticMemberExpression(member) => get_tag_name(&member.object),
+ Expression::ComputedMemberExpression(exp) => get_tag_name(&exp.object),
+ Expression::CallExpression(call) => get_tag_name(&call.callee),
+ _ => None,
+ }
+}
+
+fn format_embedded_template<'a>(
+ f: &mut Formatter<'_, 'a>,
+ tag_name: &str,
+ template_content: &str,
+) -> bool {
+ let Some(Ok(formatted)) =
+ f.context().external_callbacks().format_embedded(tag_name, template_content)
+ else {
+ return false;
+ };
+
+ // Format with proper template literal structure:
+ // - Opening backtick
+ // - Hard line break (newline after backtick)
+ // - Indented content (each line will be indented)
+ // - Hard line break (newline before closing backtick)
+ // - Closing backtick
+ let format_content = format_with(|f: &mut Formatter<'_, 'a>| {
+ let content = f.context().allocator().alloc_str(&formatted);
+ for line in content.split('\n') {
+ write!(f, [text(line), hard_line_break()]);
+ }
+ });
+
+ write!(f, ["`", block_indent(&format_content), "`"]);
+
+ true
+}
+
/// Try to format a tagged template with the embedded formatter if supported.
/// Returns `Some(result)` if formatting was attempted, `None` if not applicable.
fn try_format_embedded_template<'a>(
@@ -769,11 +812,10 @@ fn try_format_embedded_template<'a>(
return false;
}
- let Expression::Identifier(ident) = &tagged.tag else {
+ let Some(tag_name) = get_tag_name(&tagged.tag) else {
return false;
};
- let tag_name = ident.name.as_str();
// Check if the tag is supported by the embedded formatter
if !ExternalCallbacks::is_supported_tag(tag_name) {
return false;
@@ -782,26 +824,59 @@ fn try_format_embedded_template<'a>(
// Get the external callbacks from the context
let template_content = quasi.quasis[0].value.raw.as_str();
- let Some(Ok(formatted)) =
- f.context().external_callbacks().format_embedded(tag_name, template_content)
- else {
+ format_embedded_template(f, tag_name, template_content)
+}
+
+/// Check if the template literal is inside a `css` prop or `
+/// ```
+fn is_in_css_jsx<'a>(node: &AstNode<'a, TemplateLiteral<'a>>) -> bool {
+ let AstNodes::JSXExpressionContainer(container) = node.parent else {
return false;
};
- // Format with proper template literal structure:
- // - Opening backtick
- // - Hard line break (newline after backtick)
- // - Indented content (each line will be indented)
- // - Hard line break (newline before closing backtick)
- // - Closing backtick
- let format_content = format_with(|f: &mut Formatter<'_, 'a>| {
- let content = f.context().allocator().alloc_str(&formatted);
- for line in content.split('\n') {
- write!(f, [text(line), hard_line_break()]);
+ match container.parent {
+ AstNodes::JSXAttribute(attribute) => {
+ if let JSXAttributeName::Identifier(ident) = &attribute.name
+ && ident.name == "css"
+ {
+ return true;
+ }
}
- });
+ AstNodes::JSXElement(element) => {
+ if let JSXElementName::Identifier(ident) = &element.opening_element.name
+ && ident.name == "style"
+ && element.opening_element.attributes.iter().any(|attr| {
+ matches!(attr.as_attribute().and_then(|a| a.name.as_identifier()), Some(name) if name.name == "jsx")
+ })
+ {
+ return true;
+ }
+ }
+ _ => {}
+ }
+ false
+}
- write!(f, ["`", block_indent(&format_content), "`"]);
+/// Try to format a template literal inside css prop or styled-jsx with the embedded formatter.
+/// Returns `true` if formatting was attempted, `false` if not applicable.
+fn try_format_css_template<'a>(
+ template_literal: &AstNode<'a, TemplateLiteral<'a>>,
+ f: &mut Formatter<'_, 'a>,
+) -> bool {
+ if !template_literal.is_no_substitution_template() {
+ return false;
+ }
- true
+ if !is_in_css_jsx(template_literal) {
+ return false;
+ }
+
+ let quasi = template_literal.quasis();
+ let template_content = quasi[0].value.raw.as_str();
+
+ format_embedded_template(f, "css", template_content)
}