diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Revision history for text-rope-zipper
 
+## 0.1.1.0  -- 2024-07-28
+
+* Fix stickyCol
+
 ## 0.1.0.0  -- 2024-01-15
 
-* First version.
+* First version
diff --git a/src/Data/Text/Rope/Zipper.hs b/src/Data/Text/Rope/Zipper.hs
--- a/src/Data/Text/Rope/Zipper.hs
+++ b/src/Data/Text/Rope/Zipper.hs
@@ -43,8 +43,8 @@
 import Data.Text.Rope qualified as Rope
 import GHC.Generics (Generic)
 import GHC.Records qualified as GHC
-import Util
 import Prelude hiding (lines, null)
+import Util
 
 -- | A RopeZipper is similar in concept to a 'TextZipper', but tracks the
 -- lines before the cursor, lines after the cursor, and the current line of the
@@ -144,10 +144,9 @@
          in r
                 { currentLine
                 , stickyCol =
-                    if (r.stickyCol > currentLine.cursor && absDx > 0)
-                        || (absDy /= 0 && absDx == 0)
-                        then r.stickyCol
-                        else currentLine.cursor
+                    if absDx > 0 && absDy == 0 && r.currentLine.cursor /= currentLine.cursor
+                        then currentLine.cursor
+                        else r.stickyCol
                 }
   where
     Position{posLine = oldY, posColumn = oldX} = r.cursor
diff --git a/test/Data/Text/Rope/ZipperSpec.hs b/test/Data/Text/Rope/ZipperSpec.hs
--- a/test/Data/Text/Rope/ZipperSpec.hs
+++ b/test/Data/Text/Rope/ZipperSpec.hs
@@ -9,6 +9,7 @@
 import Data.Text.Rope.Zipper
 import Data.Text.Rope.Zipper qualified as RopeZipper
 import Util
+import Data.Foldable (foldl')
 
 rawOutput :: (HasCallStack) => Property -> IO ()
 rawOutput =
@@ -54,21 +55,22 @@
 instance Arbitrary VerticalMove where
     arbitrary = VerticalMove <$> elements [Up, Down, FirstLine, LastLine]
 
-moveZipper :: Move -> RopeZipper -> RopeZipper
-moveZipper Forward = moveForward
-moveZipper Backward = moveBackward
-moveZipper Up = moveUp
-moveZipper Down = moveDown
-moveZipper LineStart = moveToLineStart
-moveZipper LineEnd = moveToLineEnd
-moveZipper FirstLine = moveToFirstLine
-moveZipper LastLine = moveToLastLine
-moveZipper (Rel (dy, dx)) = moveCursor $ \Position{..} ->
-    Position
-        { posLine = boundedAdd dy posLine
-        , posColumn = boundedAdd dx posColumn
-        }
-moveZipper (Abs c) = setCursor c
+moveZipper :: [Move] -> RopeZipper -> RopeZipper
+moveZipper = flip . foldl' $ flip \case
+    Forward -> moveForward
+    Backward -> moveBackward
+    Up -> moveUp
+    Down -> moveDown
+    LineStart -> moveToLineStart
+    LineEnd -> moveToLineEnd
+    FirstLine -> moveToFirstLine
+    LastLine -> moveToLastLine
+    (Rel (dy, dx)) -> moveCursor \Position{..} ->
+        Position
+            { posLine = boundedAdd dy posLine
+            , posColumn = boundedAdd dx posColumn
+            }
+    (Abs c) -> setCursor c
 
 positionToPair :: Position -> (Int, Int)
 positionToPair Position{..} = (fromIntegral posLine, fromIntegral posColumn)
@@ -129,7 +131,7 @@
                 LastLine -> (maxBound, x)
                 Rel (dy, dx) -> (y + dy, x + dx)
                 Abs pos -> positionToPair pos
-        let zipper' = moveZipper move zipper
+        let zipper' = moveZipper [move] zipper
         zipper'.cursor `shouldBe` moveCursor' zipper.cursor
         toRope zipper `shouldBe` toRope zipper'
 
@@ -138,11 +140,18 @@
             `shouldBe` (fromIntegral . length . RopeZipper.lines) zipper
 
     it "sticks the last requested column" $ property $ \zipper (VerticalMove move) -> do
-        let zipper' = moveZipper move zipper
+        let zipper' = moveZipper [move] zipper
         let lineLen' =
-                fromIntegral
-                    $ TextZipper.length zipper'.currentLine
-                    - if TextZipper.hasTrailingNewline zipper'.currentLine then 1 else 0
+                fromIntegral $
+                    TextZipper.length zipper'.currentLine
+                        - if TextZipper.hasTrailingNewline zipper'.currentLine then 1 else 0
         let expectedColumn = min zipper.cursor.posColumn lineLen'
         zipper'.cursor.posColumn `shouldBe` expectedColumn
         moveCursor (\p -> p{posLine = zipper.cursor.posLine}) zipper' `shouldBe` zipper
+
+    it "unsticks the last requested column" do
+        let zipper = fromText "main :: IO ()\nmain = pure ()"
+            lefts = 4 :: Int
+            ups = 1 :: Int
+            zipper' = zipper & moveZipper (replicate lefts Backward <> replicate ups Up)
+        zipper'.cursor `shouldBe` Position{posLine = zipper.cursor.posLine - fromIntegral ups, posColumn = zipper.cursor.posColumn - fromIntegral lefts}
diff --git a/test/Util.hs b/test/Util.hs
--- a/test/Util.hs
+++ b/test/Util.hs
@@ -52,7 +52,7 @@
 instance Arbitrary RopeZipper where
     arbitrary = RopeZipper.fromParts <$> arbitrary <*> arbitrary
 
-infixr 9 !?
+infixl 9 !?
 
 -- | Get element n of a list, or Nothing. Like `!!` but safe.
 (!?) :: [a] -> Int -> Maybe a
diff --git a/text-rope-zipper.cabal b/text-rope-zipper.cabal
--- a/text-rope-zipper.cabal
+++ b/text-rope-zipper.cabal
@@ -1,6 +1,6 @@
 cabal-version:       3.0
 name:                text-rope-zipper
-version:             0.1.0.0
+version:             0.1.1.0
 synopsis:            2D text zipper based on text-rope
 homepage:            https://github.com/ners/text-rope-zipper/blob/master/README.md
 license:             Apache-2.0
