diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs
index b2b888f..c42f5e5 100644
--- a/maud_macros/src/parse.rs
+++ b/maud_macros/src/parse.rs
@@ -142,6 +142,11 @@ impl<'cx, 'a, 'i> Parser<'cx, 'a, 'i> {
                 self.shift(2);
                 self.if_expr(sp)?;
             },
+            // While
+            [at!(), keyword!(sp, k), ..] if k.is_keyword(keywords::While) => {
+                self.shift(2);
+                self.while_expr(sp)?;
+            },
             // For
             [at!(), keyword!(sp, k), ..] if k.is_keyword(keywords::For) => {
                 self.shift(2);
@@ -248,6 +253,28 @@ impl<'cx, 'a, 'i> Parser<'cx, 'a, 'i> {
         Ok(())
     }
 
+    /// Parses and renders an `@while` expression.
+    ///
+    /// The leading `@while` should already be consumed.
+    fn while_expr(&mut self, sp: Span) -> PResult<()> {
+        let mut cond = vec![];
+        let body;
+        loop { match *self.input {
+            [TokenTree::Delimited(sp, ref d), ..] if d.delim == DelimToken::Brace => {
+                self.shift(1);
+                body = self.block(sp, &d.tts)?;
+                break;
+            },
+            [ref tt, ..] => {
+                self.shift(1);
+                cond.push(tt.clone());
+            },
+            [] => parse_error!(self, sp, "expected body for this @while"),
+        }}
+        self.render.emit_while(cond, body);
+        Ok(())
+    }
+
     /// Parses and renders a `@for` expression.
     ///
     /// The leading `@for` should already be consumed.
diff --git a/maud_macros/src/render.rs b/maud_macros/src/render.rs
index 6efbc69..f1b034f 100644
--- a/maud_macros/src/render.rs
+++ b/maud_macros/src/render.rs
@@ -138,6 +138,15 @@ impl<'cx, 'a> Renderer<'cx, 'a> {
         self.push(stmt);
     }
 
+    /// Emits an `while` expression.
+    ///
+    /// The condition is a token tree (not an expression) so we don't
+    /// need to special-case `while let`.
+    pub fn emit_while(&mut self, cond: Vec<TokenTree>, body: Vec<Stmt>) {
+        let stmt = quote_stmt!(self.cx, while $cond { $body }).unwrap();
+        self.push(stmt);
+    }
+
     pub fn emit_for(&mut self, pattern: P<Pat>, iterable: P<Expr>, body: Vec<Stmt>) {
         let stmt = quote_stmt!(self.cx, for $pattern in $iterable { $body }).unwrap();
         self.push(stmt);
diff --git a/maud_macros/tests/control_structures.rs b/maud_macros/tests/control_structures.rs
index 32a3166..1f4332f 100644
--- a/maud_macros/tests/control_structures.rs
+++ b/maud_macros/tests/control_structures.rs
@@ -35,6 +35,28 @@ fn if_let() {
     }
 }
 
+#[test]
+fn while_expr() {
+    let mut numbers = (0..3).into_iter().peekable();
+    let s = html! {
+        ul @while numbers.peek().is_some() {
+            li (numbers.next().unwrap())
+        }
+    }.into_string();
+    assert_eq!(s, "<ul><li>0</li><li>1</li><li>2</li></ul>");
+}
+
+#[test]
+fn while_let_expr() {
+    let mut numbers = (0..3).into_iter();
+    let s = html! {
+        ul @while let Some(n) = numbers.next() {
+            li (n)
+        }
+    }.into_string();
+    assert_eq!(s, "<ul><li>0</li><li>1</li><li>2</li></ul>");
+}
+
 #[test]
 fn for_expr() {
     let ponies = ["Apple Bloom", "Scootaloo", "Sweetie Belle"];