diff --git a/leptos/tests/ssr.rs b/leptos/tests/ssr.rs
index 999989937..2b8e62108 100644
--- a/leptos/tests/ssr.rs
+++ b/leptos/tests/ssr.rs
@@ -103,6 +103,76 @@ fn test_classes() {
assert_eq!(rendered.to_html(), "
");
}
+#[cfg(feature = "ssr")]
+#[test]
+fn test_class_with_class_directive_merge() {
+ use leptos::prelude::*;
+
+ // class= followed by class: should merge
+ let rendered: View> = view! {
+
+ };
+
+ assert_eq!(rendered.to_html(), "");
+}
+
+#[cfg(feature = "ssr")]
+#[test]
+fn test_solo_class_directive() {
+ use leptos::prelude::*;
+
+ // Solo class: directive should work without class attribute
+ let rendered: View> = view! {
+
+ };
+
+ assert_eq!(rendered.to_html(), "");
+}
+
+#[cfg(feature = "ssr")]
+#[test]
+fn test_class_directive_with_static_class() {
+ use leptos::prelude::*;
+
+ // class:foo comes after class= due to macro sorting
+ // The class= clears buffer, then class:foo appends
+ let rendered: View> = view! {
+
+ };
+
+ // After macro sorting: class="bar" class:foo=true
+ // Expected: "bar foo"
+ assert_eq!(rendered.to_html(), "");
+}
+
+#[cfg(feature = "ssr")]
+#[test]
+fn test_global_class_applied() {
+ use leptos::prelude::*;
+
+ // Test that a global class is properly applied
+ let rendered: View> = view! { class="global",
+
+ };
+
+ assert_eq!(rendered.to_html(), "");
+}
+
+#[cfg(feature = "ssr")]
+#[test]
+fn test_multiple_class_attributes_overwrite() {
+ use leptos::prelude::*;
+
+ // When multiple class attributes are applied, the last one should win (browser behavior)
+ // This simulates what happens when attributes are combined programmatically
+ let el = leptos::html::div().class("first").class("second");
+
+ let html = el.to_html();
+
+ // The second class attribute should overwrite the first
+ assert_eq!(html, "");
+}
+
#[cfg(feature = "ssr")]
#[test]
fn ssr_with_styles() {
diff --git a/tachys/src/html/class.rs b/tachys/src/html/class.rs
index ce3bf70c9..3fcad1167 100644
--- a/tachys/src/html/class.rs
+++ b/tachys/src/html/class.rs
@@ -57,6 +57,10 @@ where
_style: &mut String,
_inner_html: &mut String,
) {
+ // If this is a class="..." attribute (not class:name=value), clear previous value
+ if self.class.should_overwrite() {
+ class.clear();
+ }
class.push(' ');
self.class.to_html(class);
}
@@ -156,6 +160,12 @@ pub trait IntoClass: Send {
/// Renders the class to HTML.
fn to_html(self, class: &mut String);
+ /// Whether this class attribute should overwrite previous class values.
+ /// Returns `true` for `class="..."` attributes, `false` for `class:name=value` directives.
+ fn should_overwrite(&self) -> bool {
+ false
+ }
+
/// Renders the class to HTML for a ``.
#[allow(unused)] // it's used with `nightly` feature
fn to_template(class: &mut String) {}
@@ -289,6 +299,10 @@ impl IntoClass for &str {
class.push_str(self);
}
+ fn should_overwrite(&self) -> bool {
+ true
+ }
+
fn hydrate(
self,
el: &crate::renderer::types::Element,
@@ -346,6 +360,10 @@ impl IntoClass for Cow<'_, str> {
IntoClass::to_html(&*self, class);
}
+ fn should_overwrite(&self) -> bool {
+ true
+ }
+
fn hydrate(
self,
el: &crate::renderer::types::Element,
@@ -403,6 +421,10 @@ impl IntoClass for String {
IntoClass::to_html(self.as_str(), class);
}
+ fn should_overwrite(&self) -> bool {
+ true
+ }
+
fn hydrate(
self,
el: &crate::renderer::types::Element,
@@ -460,6 +482,10 @@ impl IntoClass for Arc {
IntoClass::to_html(self.as_ref(), class);
}
+ fn should_overwrite(&self) -> bool {
+ true
+ }
+
fn hydrate(
self,
el: &crate::renderer::types::Element,