diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index cd68d08..5d99a00 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -481,7 +481,8 @@ impl<'cx, 'a, 'i> Parser<'cx, 'a, 'i> {
 
     /// Parses and renders the attributes of an element.
     fn attrs(&mut self) -> PResult<()> {
-        let mut classes = Vec::new();
+        let mut classes_static = Vec::new();
+        let mut classes_toggled = Vec::new();
         let mut ids = Vec::new();
         loop {
             let old_input = self.input;
@@ -525,7 +526,18 @@ impl<'cx, 'a, 'i> Parser<'cx, 'a, 'i> {
                 (Err(_), &[dot!(), ident!(_, _), ..]) => {
                     // Class shorthand
                     self.shift(1);
-                    classes.push(self.name().unwrap());
+                    let class_name = self.name().unwrap();
+                    match *self.input {
+                        [TokenTree::Delimited(_, ref d), ..] if d.delim == DelimToken::Bracket => {
+                            // Toggle the class based on a boolean expression
+                            self.shift(1);
+                            let cond = self.with_rust_parser(d.tts.clone(), RustParser::parse_expr)?;
+                            let cond = cond.to_tokens(self.render.cx);
+                            classes_toggled.push((cond, class_name));
+                        },
+                        // Emit the class unconditionally
+                        _ => classes_static.push(class_name),
+                    }
                 },
                 (Err(_), &[pound!(), ident!(_, _), ..]) => {
                     // ID shorthand
@@ -538,9 +550,22 @@ impl<'cx, 'a, 'i> Parser<'cx, 'a, 'i> {
                 },
             }
         }
-        if !classes.is_empty() {
+        if !classes_static.is_empty() || !classes_toggled.is_empty() {
             self.render.attribute_start("class");
-            self.render.string(&classes.join(" "));
+            self.render.string(&classes_static.join(" "));
+            for (i, (cond, mut class_name)) in classes_toggled.into_iter().enumerate() {
+                // If a class comes first in the list, then it shouldn't be
+                // prefixed by a space
+                if i > 0 || !classes_static.is_empty() {
+                    class_name = format!(" {}", class_name);
+                }
+                let body = {
+                    let mut r = self.render.fork();
+                    r.string(&class_name);
+                    r.into_stmts()
+                };
+                self.render.emit_if(cond, body, None);
+            }
             self.render.attribute_end();
         }
         if !ids.is_empty() {
diff --git a/maud_macros/tests/basic_syntax.rs b/maud_macros/tests/basic_syntax.rs
index cc53953..30ccaff 100644
--- a/maud_macros/tests/basic_syntax.rs
+++ b/maud_macros/tests/basic_syntax.rs
@@ -3,6 +3,8 @@
 
 extern crate maud;
 
+use maud::Markup;
+
 #[test]
 fn literals() {
     let s = html!("du\tcks" "-23" "3.14\n" "geese").into_string();
@@ -76,6 +78,22 @@ fn empty_attributes() {
     assert_eq!(s, r#"<div readonly><input type="checkbox" checked></div>"#);
 }
 
+#[test]
+fn toggle_empty_attributes() {
+    let rocks = true;
+    let s = html!({
+        input checked?[true] /
+        input checked?[false] /
+        input checked?[rocks] /
+        input checked?[!rocks] /
+    }).into_string();
+    assert_eq!(s, concat!(
+            r#"<input checked>"#,
+            r#"<input>"#,
+            r#"<input checked>"#,
+            r#"<input>"#));
+}
+
 #[test]
 fn colons_in_names() {
     let s = html!(pon-pon:controls-alpha a on:click="yay()" "Yay!").into_string();
@@ -121,6 +139,26 @@ fn hyphens_in_class_names() {
     assert_eq!(s, r#"<p class="rocks-these are--my--rocks">yes</p>"#);
 }
 
+#[test]
+fn toggle_classes() {
+    fn test(is_cupcake: bool, is_muffin: bool) -> Markup {
+        html!(p.cupcake[is_cupcake].muffin[is_muffin] "Testing!")
+    }
+    assert_eq!(test(true, true).into_string(), r#"<p class="cupcake muffin">Testing!</p>"#);
+    assert_eq!(test(false, true).into_string(), r#"<p class=" muffin">Testing!</p>"#);
+    assert_eq!(test(true, false).into_string(), r#"<p class="cupcake">Testing!</p>"#);
+    assert_eq!(test(false, false).into_string(), r#"<p class="">Testing!</p>"#);
+}
+
+#[test]
+fn mixed_classes() {
+    fn test(is_muffin: bool) -> Markup {
+        html!(p.cupcake.muffin[is_muffin].lamington "Testing!")
+    }
+    assert_eq!(test(true).into_string(), r#"<p class="cupcake lamington muffin">Testing!</p>"#);
+    assert_eq!(test(false).into_string(), r#"<p class="cupcake lamington">Testing!</p>"#);
+}
+
 #[test]
 fn ids_shorthand() {
     let s = html!(p { "Hi, " span#thing { "Lyra" } "!" }).into_string();
diff --git a/maud_macros/tests/splices.rs b/maud_macros/tests/splices.rs
index 9270ec0..b5dc006 100644
--- a/maud_macros/tests/splices.rs
+++ b/maud_macros/tests/splices.rs
@@ -37,22 +37,6 @@ fn attributes() {
     assert_eq!(s, r#"<img src="pinkie.jpg" alt="Pinkie Pie">"#);
 }
 
-#[test]
-fn empty_attributes() {
-    let rocks = true;
-    let s = html!({
-        input checked?[true] /
-        input checked?[false] /
-        input checked?[rocks] /
-        input checked?[!rocks] /
-    }).into_string();
-    assert_eq!(s, concat!(
-            r#"<input checked>"#,
-            r#"<input>"#,
-            r#"<input checked>"#,
-            r#"<input>"#));
-}
-
 static BEST_PONY: &'static str = "Pinkie Pie";
 
 #[test]