diff --git a/rinja_parser/Cargo.toml b/rinja_parser/Cargo.toml
index 35ab8185..14bef8fd 100644
--- a/rinja_parser/Cargo.toml
+++ b/rinja_parser/Cargo.toml
@@ -23,7 +23,7 @@ harness = false
 [dependencies]
 memchr = "2"
 serde = { version = "1.0", optional = true, features = ["derive"] }
-winnow = "0.6.23"
+winnow = "0.7.0"
 
 [dev-dependencies]
 criterion = "0.5"
diff --git a/rinja_parser/src/expr.rs b/rinja_parser/src/expr.rs
index a6f0264d..c2e4f9cf 100644
--- a/rinja_parser/src/expr.rs
+++ b/rinja_parser/src/expr.rs
@@ -6,7 +6,7 @@ use winnow::ascii::digit1;
 use winnow::combinator::{
     alt, cut_err, fail, not, opt, peek, preceded, repeat, separated, terminated,
 };
-use winnow::error::{ErrorKind, ParserError as _};
+use winnow::error::ParserError as _;
 use winnow::stream::Stream as _;
 
 use crate::node::CondTest;
@@ -625,11 +625,7 @@ impl<'a> Suffix<'a> {
                         expr = WithSpan::new(Expr::RustMacro(vec![name], args), before_suffix)
                     }
                     _ => {
-                        return Err(winnow::error::ErrMode::from_error_kind(
-                            &before_suffix,
-                            ErrorKind::Tag,
-                        )
-                        .cut());
+                        return Err(winnow::error::ErrMode::from_input(&before_suffix).cut());
                     }
                 },
             }
diff --git a/rinja_parser/src/lib.rs b/rinja_parser/src/lib.rs
index 83188e6f..23e0eb14 100644
--- a/rinja_parser/src/lib.rs
+++ b/rinja_parser/src/lib.rs
@@ -10,12 +10,12 @@ use std::path::Path;
 use std::sync::Arc;
 use std::{fmt, str};
 
-use winnow::Parser;
 use winnow::ascii::take_escaped;
 use winnow::combinator::{alt, cut_err, delimited, fail, not, opt, peek, preceded, repeat};
-use winnow::error::{ErrorKind, FromExternalError};
+use winnow::error::FromExternalError;
 use winnow::stream::{AsChar, Stream as _};
 use winnow::token::{any, one_of, take_till, take_while};
+use winnow::{ModalParser, Parser};
 
 pub mod expr;
 pub use expr::{Attr, Expr, Filter, TyGenerics};
@@ -308,28 +308,34 @@ impl<'a> ErrorContext<'a> {
             message: Some(message.into()),
         }
     }
+
+    fn backtrack(self) -> winnow::error::ErrMode<Self> {
+        winnow::error::ErrMode::Backtrack(self)
+    }
+
+    fn cut(self) -> winnow::error::ErrMode<Self> {
+        winnow::error::ErrMode::Cut(self)
+    }
 }
 
 impl<'a> winnow::error::ParserError<&'a str> for ErrorContext<'a> {
-    fn from_error_kind(input: &&'a str, _code: ErrorKind) -> Self {
+    type Inner = Self;
+
+    fn from_input(input: &&'a str) -> Self {
         Self {
             span: (*input).into(),
             message: None,
         }
     }
 
-    fn append(
-        self,
-        _: &&'a str,
-        _: &<&str as winnow::stream::Stream>::Checkpoint,
-        _: ErrorKind,
-    ) -> Self {
-        self
+    #[inline(always)]
+    fn into_inner(self) -> Result<Self::Inner, Self> {
+        Ok(self)
     }
 }
 
 impl<'a, E: std::fmt::Display> FromExternalError<&'a str, E> for ErrorContext<'a> {
-    fn from_external_error(input: &&'a str, _kind: ErrorKind, e: E) -> Self {
+    fn from_external_error(input: &&'a str, e: E) -> Self {
         Self {
             span: (*input).into(),
             message: Some(Cow::Owned(e.to_string())),
@@ -337,12 +343,6 @@ impl<'a, E: std::fmt::Display> FromExternalError<&'a str, E> for ErrorContext<'a
     }
 }
 
-impl<'a> From<ErrorContext<'a>> for winnow::error::ErrMode<ErrorContext<'a>> {
-    fn from(cx: ErrorContext<'a>) -> Self {
-        Self::Cut(cx)
-    }
-}
-
 #[inline]
 fn skip_ws0<'a>(i: &mut &'a str) -> ParseResult<'a, ()> {
     *i = i.trim_ascii_start();
@@ -361,8 +361,8 @@ fn skip_ws1<'a>(i: &mut &'a str) -> ParseResult<'a, ()> {
 }
 
 fn ws<'a, O>(
-    inner: impl Parser<&'a str, O, ErrorContext<'a>>,
-) -> impl Parser<&'a str, O, ErrorContext<'a>> {
+    inner: impl ModalParser<&'a str, O, ErrorContext<'a>>,
+) -> impl ModalParser<&'a str, O, ErrorContext<'a>> {
     delimited(skip_ws0, inner, skip_ws0)
 }
 
@@ -370,8 +370,8 @@ fn ws<'a, O>(
 /// Returns tuple that would be returned when parsing `end`.
 fn skip_till<'a, 'b, O>(
     candidate_finder: impl crate::memchr_splitter::Splitter,
-    end: impl Parser<&'a str, O, ErrorContext<'a>>,
-) -> impl Parser<&'a str, (&'a str, O), ErrorContext<'a>> {
+    end: impl ModalParser<&'a str, O, ErrorContext<'a>>,
+) -> impl ModalParser<&'a str, (&'a str, O), ErrorContext<'a>> {
     let mut next = alt((end.map(Some), any.map(|_| None)));
     move |i: &mut &'a str| loop {
         *i = match candidate_finder.split(i) {
@@ -392,7 +392,7 @@ fn skip_till<'a, 'b, O>(
     }
 }
 
-fn keyword(k: &str) -> impl Parser<&str, &str, ErrorContext<'_>> {
+fn keyword(k: &str) -> impl ModalParser<&str, &str, ErrorContext<'_>> {
     identifier.verify(move |v: &str| v == k)
 }
 
@@ -508,7 +508,7 @@ fn num_lit<'a>(i: &mut &'a str) -> ParseResult<'a, Num<'a>> {
 fn separated_digits<'a>(
     radix: u32,
     start: bool,
-) -> impl Parser<&'a str, &'a str, ErrorContext<'a>> {
+) -> impl ModalParser<&'a str, &'a str, ErrorContext<'a>> {
     (
         move |i: &mut &'a _| match start {
             true => Ok(()),
@@ -758,7 +758,7 @@ impl State<'_, '_> {
                 control.escape_default(),
                 self.syntax.block_end.escape_default(),
             );
-            Err(ParseErr::backtrack(ErrorContext::new(message, *i).into()))
+            Err(ErrorContext::new(message, *i).backtrack())
         } else {
             Ok(())
         }
diff --git a/rinja_parser/src/node.rs b/rinja_parser/src/node.rs
index 13688632..5b35ad72 100644
--- a/rinja_parser/src/node.rs
+++ b/rinja_parser/src/node.rs
@@ -1,13 +1,13 @@
 use std::collections::HashSet;
 use std::str::{self, FromStr};
 
-use winnow::Parser;
 use winnow::combinator::{
     alt, cut_err, delimited, empty, eof, fail, not, opt, peek, preceded, repeat, separated,
     terminated,
 };
 use winnow::stream::Stream as _;
 use winnow::token::{any, literal, rest};
+use winnow::{ModalParser, Parser};
 
 use crate::memchr_splitter::{Splitter1, Splitter2, Splitter3};
 use crate::{
@@ -120,7 +120,7 @@ impl<'a> Node<'a> {
         .parse_next(i)?;
         match closed {
             true => Ok(node),
-            false => Err(ErrorContext::unclosed("block", s.syntax.block_end, start).into()),
+            false => Err(ErrorContext::unclosed("block", s.syntax.block_end, start).cut()),
         }
     }
 
@@ -188,7 +188,7 @@ impl<'a> Node<'a> {
         .parse_next(i)?;
         match closed {
             true => Ok(Self::Expr(Ws(pws, nws), expr)),
-            false => Err(ErrorContext::unclosed("expression", s.syntax.expr_end, start).into()),
+            false => Err(ErrorContext::unclosed("expression", s.syntax.expr_end, start).cut()),
         }
     }
 
@@ -218,8 +218,8 @@ impl<'a> Node<'a> {
 
 fn cut_node<'a, O>(
     kind: Option<&'static str>,
-    inner: impl Parser<&'a str, O, ErrorContext<'a>>,
-) -> impl Parser<&'a str, O, ErrorContext<'a>> {
+    inner: impl ModalParser<&'a str, O, ErrorContext<'a>>,
+) -> impl ModalParser<&'a str, O, ErrorContext<'a>> {
     let mut inner = cut_err(inner);
     move |i: &mut &'a str| {
         let start = *i;
@@ -1096,7 +1096,7 @@ impl<'a> Lit<'a> {
                 return fail.parse_next(i);
             }
             Some(content) => content,
-            None => rest.parse_next(i)?, // there is no {block,comment,expr}_start: take everything
+            None => rest.parse_next(i)?, /* there is no {block,comment,expr}_start: take everything */
         };
         Ok(WithSpan::new(Self::split_ws_parts(content), start))
     }
@@ -1352,7 +1352,7 @@ impl<'a> Comment<'a> {
                 let tag = opt(skip_till(splitter, |i: &mut _| tag(i, s))).parse_next(i)?;
                 let Some((inclusive, tag)) = tag else {
                     return Err(
-                        ErrorContext::unclosed("comment", s.syntax.comment_end, start).into(),
+                        ErrorContext::unclosed("comment", s.syntax.comment_end, start).cut(),
                     );
                 };
                 match tag {
@@ -1414,7 +1414,7 @@ pub struct Ws(pub Option<Whitespace>, pub Option<Whitespace>);
 fn end_node<'a, 'g: 'a>(
     node: &'g str,
     expected: &'g str,
-) -> impl Parser<&'a str, &'a str, ErrorContext<'a>> + 'g {
+) -> impl ModalParser<&'a str, &'a str, ErrorContext<'a>> + 'g {
     move |i: &mut &'a str| {
         let start = i.checkpoint();
         let actual = ws(identifier).parse_next(i)?;
diff --git a/rinja_parser/src/target.rs b/rinja_parser/src/target.rs
index 5a2a39bb..56d722fe 100644
--- a/rinja_parser/src/target.rs
+++ b/rinja_parser/src/target.rs
@@ -1,6 +1,6 @@
-use winnow::Parser;
 use winnow::combinator::{alt, opt, peek, preceded, separated};
 use winnow::token::one_of;
+use winnow::{ModalParser, Parser};
 
 use crate::{
     CharLit, ErrorContext, Num, ParseErr, ParseResult, PathOrIdentifier, State, StrLit, WithSpan,
@@ -217,7 +217,7 @@ fn verify_name<'a>(
 fn collect_targets<'a, T>(
     i: &mut &'a str,
     delim: char,
-    one: impl Parser<&'a str, T, ErrorContext<'a>>,
+    one: impl ModalParser<&'a str, T, ErrorContext<'a>>,
 ) -> ParseResult<'a, (bool, Vec<T>)> {
     let opt_comma = ws(opt(',')).map(|o| o.is_some());
     let mut opt_end = ws(opt(one_of(delim))).map(|o| o.is_some());