diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index 8a9689d..7555afc 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -14,6 +14,9 @@ macro_rules! dollar {
 macro_rules! eq {
     () => (TtToken(_, token::Eq))
 }
+macro_rules! not {
+    () => (TtToken(_, token::Not))
+}
 macro_rules! semi {
     () => (TtToken(_, token::Semi))
 }
@@ -154,14 +157,22 @@ impl<'cx, 's, 'i, 'r, 'o> Parser<'cx, 's, 'i, 'r, 'o> {
     fn attrs(&mut self) {
         while let [ident!(name), eq!(), ..] = self.input {
             self.shift(2);
-            self.render.attribute_start(name.as_str());
-            {
-                let old_in_attr = self.in_attr;
-                self.in_attr = true;
-                self.markup();
-                self.in_attr = old_in_attr;
+            if let [not!(), ..] = self.input {
+                // Empty attribute
+                self.shift(1);
+                self.render.attribute_empty(name.as_str());
+            } else {
+                // Non-empty attribute
+                self.render.attribute_start(name.as_str());
+                {
+                    // Parse a value under an attribute context
+                    let old_in_attr = self.in_attr;
+                    self.in_attr = true;
+                    self.markup();
+                    self.in_attr = old_in_attr;
+                }
+                self.render.attribute_end();
             }
-            self.render.attribute_end();
         }
     }
 
diff --git a/maud_macros/src/render.rs b/maud_macros/src/render.rs
index acb63ab..542625d 100644
--- a/maud_macros/src/render.rs
+++ b/maud_macros/src/render.rs
@@ -77,6 +77,11 @@ impl<'cx, 's, 'o> Renderer<'cx, 's, 'o> {
         self.write("=\"");
     }
 
+    pub fn attribute_empty(&mut self, name: &str) {
+        self.write(" ");
+        self.write(name);
+    }
+
     pub fn attribute_end(&mut self) {
         self.write("\"");
     }