diff --git a/crates/ui-egui/src/caret_glide.rs b/crates/ui-egui/src/caret_glide.rs
--- a/crates/ui-egui/src/caret_glide.rs
+++ b/crates/ui-egui/src/caret_glide.rs
@@ -17,7 +17,10 @@
 //!
 //! What never glides: a caret that moved because the view changed (zoom, a resized window,
 //! scrolling, another document), one that wasn't drawn the frame before (a selection, the canvas
-//! unfocused), and moves that start or end off screen. Our choice; Word wasn't measured there.
+//! unfocused), moves that start or end off screen, and moves the input method makes: its
+//! composition is drawn over the page, so the caret's place in it is not a place in the text, and
+//! committing eight letters of pinyin as two Chinese characters would slide the caret back over
+//! them. Our choice; Word wasn't measured there.
 //!
 //! With animations off the caret jumps and blinks on and off, as it always did: File ▸ Options ▸
 //! Advanced ▸ Animation (the `ui.caretAnimation` command) turns them off, and so do egui's
@@ -72,6 +75,9 @@ pub struct Frame {
     pub pointer: bool,
     /// Seconds since the blink was restarted (by the last command or click).
     pub since_blink: f64,
+    /// The caret is drawn at the input method's cursor in a composition, or the input method
+    /// sent something this frame (it can end a composition and commit its text a frame apart).
+    pub composing: bool,
 }
 
 /// How to draw the caret this frame.
@@ -114,6 +120,8 @@ pub struct CaretGlide {
     /// at the end of a frame, so the move they make is seen one frame later.
     keys: bool,
     pointer: bool,
+    /// Last frame's [`Frame::composing`].
+    composing: bool,
     cause: u64,
     view: u64,
     frame_nr: u64,
@@ -138,6 +146,7 @@ impl CaretGlide {
             fade: None,
             keys: false,
             pointer: false,
+            composing: false,
             cause: 0,
             view: 0,
             frame_nr: 0,
@@ -153,11 +162,13 @@ impl CaretGlide {
         let same_view = f.view == self.view;
         let by_keys = f.keys || (self.keys && continuous);
         let by_pointer = f.pointer || (self.pointer && continuous);
+        let composing = f.composing || (self.composing && continuous);
         self.cause = f.cause;
         self.view = f.view;
         self.frame_nr = f.frame_nr;
         self.keys = f.keys;
         self.pointer = f.pointer;
+        self.composing = f.composing;
         let sane = is_finite(f.target) && f.now.is_finite();
         if !(f.shown && sane) {
             self.reset();
@@ -181,7 +192,7 @@ impl CaretGlide {
             self.fade = Some(FadeIn { from: was_opacity, started: f.now });
             self.glide = match heading {
                 // Keys glide, from where the caret was heading (not from where it has got to).
-                Some(from) if by_keys && !by_pointer && same_view => {
+                Some(from) if by_keys && !by_pointer && !composing && same_view => {
                     let seen = f.viewport.expand(OFFSCREEN_SLACK);
                     let on_screen = |b: Bar| seen.intersects(Rect::from_two_pos(b[0], b[1]));
                     (on_screen(from) && on_screen(f.target)).then_some(Glide { from, to: f.target, started: f.now })
@@ -193,6 +204,10 @@ impl CaretGlide {
             self.glide = None;
             self.fade = None;
         }
+        // A composition draws the caret at the input method's cursor: a glide under way stops.
+        if composing {
+            self.glide = None;
+        }
         // The text shifted under a glide (pixel snapping while scrolling): carry the glide along.
         if let Some(g) = &mut self.glide
             && g.to != f.target
@@ -358,11 +373,12 @@ pub fn look(app: &WordApp, ui: &Ui, content_origin: Pos2, geo: &Geometry, target
     }
     let off = content_origin.to_vec2();
     let since = since_ms / 1000.0;
-    let (keys, pointer, now) = ui.input(|i| {
+    let (keys, pointer, ime, now) = ui.input(|i| {
         let typed = |e: &egui::Event| {
             matches!(e, egui::Event::Key { pressed: true, .. } | egui::Event::Text(_) | egui::Event::Paste(_) | egui::Event::Ime(_))
         };
-        (i.events.iter().any(typed), i.pointer.any_down() || i.pointer.any_pressed() || i.pointer.any_released(), i.time)
+        let ime = i.events.iter().any(|e| matches!(e, egui::Event::Ime(_)));
+        (i.events.iter().any(typed), i.pointer.any_down() || i.pointer.any_pressed() || i.pointer.any_released(), ime, i.time)
     });
     let frame = Frame {
         target: [target[0] - off, target[1] - off],
@@ -375,6 +391,7 @@ pub fn look(app: &WordApp, ui: &Ui, content_origin: Pos2, geo: &Geometry, target
         keys,
         pointer,
         since_blink: since,
+        composing: ime || !app.canvas.ime_preedit.is_empty(),
     };
     let (mut look, gliding, secs) = ui.ctx().data_mut(|d| {
         let state = d.get_temp_mut_or_default::<CaretGlide>(state_id());
diff --git a/crates/ui-egui/src/caret_glide/tests.rs b/crates/ui-egui/src/caret_glide/tests.rs
--- a/crates/ui-egui/src/caret_glide/tests.rs
+++ b/crates/ui-egui/src/caret_glide/tests.rs
@@ -30,6 +30,7 @@ impl Rig {
             keys: false,
             pointer: false,
             since_blink: 0.0,
+            composing: false,
         };
         let mut rig = Rig { glide: CaretGlide::with_secs(GLIDE_SECS), f, restarted: 100.0 };
         assert_eq!(rig.glide.frame(rig.f), Look { bar: at, opacity: 1.0, moving: false }, "the first frame draws the caret in place");
@@ -127,6 +128,22 @@ fn a_key_move_glides_and_lands_exactly() {
     assert_eq!(r.step(DT), Look { bar: bar(200.0, 90.0), opacity: 1.0, moving: false }, "then it rests");
 }
 
+/// A composition that starts while the caret is still gliding stops the glide: the caret is
+/// drawn at the input method's cursor at once, not carried there by the rest of the glide.
+#[test]
+fn a_composition_stops_a_glide_under_way() {
+    let mut r = Rig::new(bar(100.0, 50.0));
+    r.key_to(bar(200.0, 50.0));
+    r.step(DT);
+    assert!(r.glide.is_gliding());
+    // The input method's cursor sits inside the composition: same cause, new target.
+    r.f.composing = true;
+    r.f.target = bar(240.0, 50.0);
+    let look = r.step(DT);
+    assert!(!r.glide.is_gliding(), "the glide stops: {look:?}");
+    assert_eq!(look.bar, bar(240.0, 50.0));
+}
+
 #[test]
 fn the_glide_is_the_same_at_any_frame_rate() {
     let at = |hz: f64| {
@@ -475,3 +492,58 @@ fn the_canvas_caret_glides_after_a_key() {
     h.step();
     assert!(state(&h.ctx).drawn().is_some());
 }
+
+/// Committing an input method's composition puts the caret after the committed text at once.
+/// While composing, the caret is drawn at the input method's cursor inside the composition (which
+/// is drawn over the page in the interface font); the committed text has its own width, often
+/// much narrower (eight letters of pinyin for two Chinese characters), so a glide from there
+/// would slide the caret back over the text it just typed. A key after the commit still glides.
+#[test]
+fn an_input_method_commit_does_not_glide_from_the_composition() {
+    let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Default::default());
+    a.run("text.insert", json!({"text": "Some text "})).unwrap();
+    a.run("ui.animations", json!({"value": "on"})).unwrap();
+    let mut h = egui_kittest::Harness::builder().with_size(vec2(1440.0, 900.0)).with_step_dt(DT as f32).build_ui_state(
+        |ui, app: &mut WordApp| {
+            let ctx = ui.ctx().clone();
+            app.logic(&ctx);
+            app.ui(ui);
+        },
+        a,
+    );
+    h.ctx.all_styles_mut(|s| s.animation_time = 0.1);
+    for _ in 0..30 {
+        h.step();
+    }
+    let before = state(&h.ctx).drawn().expect("the focused canvas draws a caret");
+    for (spelling, commit, lone) in [("zhongguo", "中国", false), ("shi", "是", false), ("nihao", "你好", true)] {
+        h.event(egui::Event::Ime(egui::ImeEvent::Preedit { text: spelling.into(), active_range_chars: None }));
+        for _ in 0..3 {
+            h.step();
+        }
+        let composing = state(&h.ctx).drawn().expect("the caret is drawn in the composition");
+        assert!(composing[0].x > before[0].x + 10.0, "{spelling}: the caret is at the composition's end");
+        // Some input methods clear the composition and commit in the same batch.
+        if lone {
+            h.event(egui::Event::Ime(egui::ImeEvent::Preedit { text: String::new(), active_range_chars: None }));
+        }
+        h.event(egui::Event::Ime(egui::ImeEvent::Commit(commit.into())));
+        h.step();
+        let mut drawn = Vec::new();
+        for _ in 0..20 {
+            h.step();
+            let s = state(&h.ctx);
+            assert!(!s.is_gliding(), "{spelling}: the caret glides from the composition: {drawn:?}");
+            drawn.push(s.drawn().expect("the caret stays drawn")[0].x);
+        }
+        let committed = h.state().session.doc.plain_text(wordcraft_doc::StoryRef::Body);
+        assert!(committed.ends_with(commit), "{spelling}: committed: {committed:?}");
+        assert!(drawn.windows(2).all(|w| w[0] == w[1]), "{spelling}: the caret rests after the commit: {drawn:?}");
+    }
+    // An ordinary key still glides.
+    h.event(key(egui::Key::Home));
+    for _ in 0..3 {
+        h.step();
+    }
+    assert!(state(&h.ctx).is_gliding(), "Home after a commit glides as before");
+}
diff --git a/docs/typing-parity.md b/docs/typing-parity.md
--- a/docs/typing-parity.md
+++ b/docs/typing-parity.md
@@ -1,6 +1,6 @@
 # Typing parity with Word
 
-> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-25 · **Change:** trivial (`*Bold* and _italic_ as real formatting` in AutoFormat As You Type; previously trivial: highlight target includes selected paragraph marks, #4085; previously trivial: full-target highlight check beyond 10,000 runs, #4085; previously trivial: queued IME replacements are rejected before mutation after their marked composition loses its body/equation target; previously trivial: the transactional equation delete proves its commit with the engine revision; previously trivial: a refused byte-counted equation deletion drops only the immediately adjacent stale replacement, so a later direct commit still applies; previously trivial: a tracked own-insertion shifts the rest of a two-sided IME deletion; previously trivial: AutoCorrect/AutoFormat classification compares the actual paragraph; previously trivial: a replaced body starts a new input-method context and interrupts the composition; a quote the engine leaves as typed is the input method's own change; equation `DeleteSurrounding` erases counted characters; previously trivial: AutoCorrect finds the word after a no-break space or an ideographic space; previously trivial: a command that changes nothing is not an edit: no undo step, no unsaved changes, no relayout, and the redo stack survives it; previously trivial: while an input method composes, the caret and its candidate window sit at the input method's cursor inside the composition; previously trivial: Ctrl+Tab types a tab in a table cell, #3137; previously trivial: List AutoFormat triggers in front of an already-written paragraph, not only a fresh empty one, checked in Word for Windows, #3352; previously trivial: AltGr types its characters without opening KeyTips on Linux, #2388; previously trivial: Delete on whole selected cells leaves one empty paragraph in each, with the cell's last paragraph's formatting; nested tables go too, #2802; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial: a text box that keeps the keyboard no longer resets the input method every frame, #2986; previously trivial: a slash opening a word travels whole to the next line, #2963; previously trivial: typing over a selection takes the first selected character's formatting, also when the selection was made right to left, #2879; previously trivial: Turkish and Azerbaijani case mapping in AutoCorrect, #1923; previously trivial: smart quotes in Hebrew pinned: logical order, ” in acronyms as in Word, #1670; previously trivial: Backspace on a selected table, rows or columns deletes them, Delete clears them, #1844; previously trivial: typed text takes its direction from its own letters, #1543; previously trivial: an input method's request to erase text around the caret is honoured, so web typing in Cyrillic and other non-Latin text no longer repeats, #1480; previously trivial: `-->` AutoCorrect replacement, #2223; previously trivial: a heading-list preset keeps Heading styles and makes other paragraphs Heading 1, as in Word, #1793; previously trivial: numbered headings: a heading-linked Multilevel List numbers new headings by their style; numbering keeps a heading a heading, #1793; previously trivial: reversing an automatic capital adds an exception, #1857; previously trivial: smart quotes open after guillemets and low quotes and nest; previously trivial: Enter on an empty list item numbered by its paragraph style moves it up a level, #2143; previously trivial: Backspace after a section break and Delete before it remove the break, also on the last paragraph, #2127; previously trivial: a table inserted in right-to-left text is right to left, so Tab moves left, #2634; previously trivial: Shift+Delete cuts the selection on every platform, #1669; previously trivial: Select All then Delete removes a table the document starts with, #1198; previously trivial: Alt+numpad character codes, #961; previously trivial: Down and Up from a line start beside a table go into it, #1007; previously trivial: sample-text formulas on Enter, #1067; previously trivial: Track Changes: Accept All after a tracked delete from a paragraph end keeps the next paragraph; Reject All after typing into an empty paragraph keeps it (#864); previously minor: Correct TWo INitial CApitals, cAPS LOCK and table-cell capitals, #544; previously trivial: complex-script caret and Backspace, #604; Move Paragraph Up/Down, Promote/Demote, Go Back and Alt+X keys, #343; the body IME deletion maps signed placeholder growth/shrink, keeps control markers outside the range and scopes its undo join to the matching replacement) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
+> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-27 · **Change:** trivial (committing an input method's composition puts the caret after the text without a glide back from the marked text; previously trivial: `*Bold* and _italic_ as real formatting` in AutoFormat As You Type; previously trivial: highlight target includes selected paragraph marks, #4085; previously trivial: full-target highlight check beyond 10,000 runs, #4085; previously trivial: queued IME replacements are rejected before mutation after their marked composition loses its body/equation target; previously trivial: the transactional equation delete proves its commit with the engine revision; previously trivial: a refused byte-counted equation deletion drops only the immediately adjacent stale replacement, so a later direct commit still applies; previously trivial: a tracked own-insertion shifts the rest of a two-sided IME deletion; previously trivial: AutoCorrect/AutoFormat classification compares the actual paragraph; previously trivial: a replaced body starts a new input-method context and interrupts the composition; a quote the engine leaves as typed is the input method's own change; equation `DeleteSurrounding` erases counted characters; previously trivial: AutoCorrect finds the word after a no-break space or an ideographic space; previously trivial: a command that changes nothing is not an edit: no undo step, no unsaved changes, no relayout, and the redo stack survives it; previously trivial: while an input method composes, the caret and its candidate window sit at the input method's cursor inside the composition; previously trivial: Ctrl+Tab types a tab in a table cell, #3137; previously trivial: List AutoFormat triggers in front of an already-written paragraph, not only a fresh empty one, checked in Word for Windows, #3352; previously trivial: AltGr types its characters without opening KeyTips on Linux, #2388; previously trivial: Delete on whole selected cells leaves one empty paragraph in each, with the cell's last paragraph's formatting; nested tables go too, #2802; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial: a text box that keeps the keyboard no longer resets the input method every frame, #2986; previously trivial: a slash opening a word travels whole to the next line, #2963; previously trivial: typing over a selection takes the first selected character's formatting, also when the selection was made right to left, #2879; previously trivial: Turkish and Azerbaijani case mapping in AutoCorrect, #1923; previously trivial: smart quotes in Hebrew pinned: logical order, ” in acronyms as in Word, #1670; previously trivial: Backspace on a selected table, rows or columns deletes them, Delete clears them, #1844; previously trivial: typed text takes its direction from its own letters, #1543; previously trivial: an input method's request to erase text around the caret is honoured, so web typing in Cyrillic and other non-Latin text no longer repeats, #1480; previously trivial: `-->` AutoCorrect replacement, #2223; previously trivial: a heading-list preset keeps Heading styles and makes other paragraphs Heading 1, as in Word, #1793; previously trivial: numbered headings: a heading-linked Multilevel List numbers new headings by their style; numbering keeps a heading a heading, #1793; previously trivial: reversing an automatic capital adds an exception, #1857; previously trivial: smart quotes open after guillemets and low quotes and nest; previously trivial: Enter on an empty list item numbered by its paragraph style moves it up a level, #2143; previously trivial: Backspace after a section break and Delete before it remove the break, also on the last paragraph, #2127; previously trivial: a table inserted in right-to-left text is right to left, so Tab moves left, #2634; previously trivial: Shift+Delete cuts the selection on every platform, #1669; previously trivial: Select All then Delete removes a table the document starts with, #1198; previously trivial: Alt+numpad character codes, #961; previously trivial: Down and Up from a line start beside a table go into it, #1007; previously trivial: sample-text formulas on Enter, #1067; previously trivial: Track Changes: Accept All after a tracked delete from a paragraph end keeps the next paragraph; Reject All after typing into an empty paragraph keeps it (#864); previously minor: Correct TWo INitial CApitals, cAPS LOCK and table-cell capitals, #544; previously trivial: complex-script caret and Backspace, #604; Move Paragraph Up/Down, Promote/Demote, Go Back and Alt+X keys, #343; the body IME deletion maps signed placeholder growth/shrink, keeps control markers outside the range and scopes its undo join to the matching replacement) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
 
 What Word does when you type, observed black-box (same keystrokes typed into Word and
 WordCraft, results read back paragraph by paragraph) and pinned by tests in
@@ -394,6 +394,10 @@ again.
   and the caret and the candidate window sit at the input method's cursor inside it: the end of
   the range it reports, or the end of the marked text when it reports none. The range is clamped
   to the text (`canvas::tests::the_caret_follows_the_input_method_cursor_in_the_composition`).
+  When the composition is committed the caret is at once after the committed text: it doesn't
+  glide there from its place in the marked text, which is drawn over the page and is often much
+  wider than what it turns into (`zhongguo` for 中国), so the glide went back over the new text
+  (`caret_glide::tests::an_input_method_commit_does_not_glide_from_the_composition`).
 - A composition belongs to the text it was started on. When a command replaces the body under
   the same document id — Replace Document Text, a version restore, Compare, Undo/Redo — or opens
   a new document, the body's replacement epoch advances, the platform clears the composition, and
@@ -492,6 +496,7 @@ again.
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-27 | trivial | Input methods: committing a composition puts the caret after the committed text at once; before, it glided from its place in the wider marked text, back over the new text. |
 | 2026-10-25 | trivial | Highlight decisions include fully selected paragraph marks in whole cells and ordinary ranges, including empty paragraphs; partial text columns exclude unselected marks (#4085). |
 | 2026-10-25 | trivial | Highlight toggle checks the full target beyond 10,000 runs without collecting resolved formatting; mixed and inherited tails and Undo/Redo covered (#4085). |
 | 2026-10-24 | trivial | Input methods: reject queued old-composition replacements before mutation after body/equation target replacement (new regression validation pending); equation `DeleteSurrounding` is one transactional command whose checked revision proves a receipt; a caret-only step in a mixed request refuses the whole deletion, and an over-cap request clears the painted pre-edit and the previous replacement join; a refused byte-counted equation deletion drops only the immediately adjacent stale replacement, never a later direct commit; a tracked own-insertion shifts the following range; a body deletion maps signed placeholder growth/shrink, keeps control markers outside the range and scopes its replacement undo join to the matching `text.insert`; refusals clear the painted pre-edit and joins; AutoCorrect/AutoFormat classification compares the paragraph as typed; equation replacement interrupts the composition |
