diff --git a/README.md b/README.md
index 4dbf106..205c8f6 100644
--- a/README.md
+++ b/README.md
@@ -280,9 +280,9 @@ Where possible, it also smooths out the differences between versions:
these natively, so skip the extension there.
- **Features that HTMX 4 dropped.** Where HTMX 4 has a close equivalent, the toolkit uses it.
For example, HTMX 4 has no `HX-Trigger-After-Swap` and `HX-Trigger-After-Settle` headers,
- so events with these timings are sent in `HX-Trigger` and fire when the request completes.
+ so events with these timings are sent in `HX-Trigger`, which HTMX 4 handles after the swap and settle steps.
-Some differences can't be hidden. For example, in HTMX 4 an event can't wait for the settle step.
+Some differences can't be hidden. For example, in HTMX 4 a response event can't fire before the swap.
If you aren't sure which version to pick, see [Choose an HTMX version](https://rameel.github.io/ramstack.htmxtoolkit/articles/choosing-version.html).
For all the differences, see [Version compatibility](https://rameel.github.io/ramstack.htmxtoolkit/articles/version-compatibility.html).
diff --git a/docs/articles/responses.md b/docs/articles/responses.md
index a084889..c1b698b 100644
--- a/docs/articles/responses.md
+++ b/docs/articles/responses.md
@@ -205,14 +205,17 @@ The `hx-trigger` listener can still refresh a component while JavaScript handles
### Timing and repeated events
HTMX 1.x and 2.x distinguish receive, after-swap, and after-settle response headers.
-HTMX 4.x delivers these Toolkit events through `HX-Trigger` when the request completes, after the swap when one occurs.
+HTMX 4.x delivers these Toolkit events through `HX-Trigger` when the request completes,
+after the swap and settle steps when a swap occurs.
See [Version compatibility](version-compatibility.md#response-events).
-Calling `TriggerEvent` several times adds events to the same response, and the browser receives them in call order.
-A JSON header cannot hold the same name twice, so when a name repeats within one header, all events of that header
-are sent under the reserved `rs:event` name and the [Toolkit script](antiforgery.md#what-the-toolkit-script-does)
-replays them in order. Keep the script loaded when using repeated event names: without it, none of the events
-in that header are triggered. Do not use the name `rs:event`.
+Calling `TriggerEvent` several times adds events to the same response, and the browser receives the events
+of each header in call order. A JSON header cannot hold the same name twice, so when a name repeats within
+one header, all events of that header are sent under the reserved `rs:event` name and
+the [Toolkit script](antiforgery.md#what-the-toolkit-script-does) replays them in order.
+Keep the script loaded when using repeated event names: without it, none of the events
+in that header are triggered. In HTMX 4.x all timings share `HX-Trigger`, so this also applies to a name
+added with different timings. Do not use the name `rs:event`.
### Serialize details with Native AOT
diff --git a/docs/articles/troubleshooting.md b/docs/articles/troubleshooting.md
index 5b4c63d..2fc6974 100644
--- a/docs/articles/troubleshooting.md
+++ b/docs/articles/troubleshooting.md
@@ -237,8 +237,15 @@ If no method is present, the URL Tag Helper generates `hx-get`.
## Event timing differs after moving to HTMX 4
HTMX 1.x and 2.x have separate trigger response headers for receive, after swap, and after settle.
-HTMX 4.x receives Toolkit events through `HX-Trigger` at request completion.
-Review uses of `HtmxTriggerTiming.AfterSettle` during migration.
+HTMX 4.x receives all Toolkit events through `HX-Trigger` when the request completes, after the swap and settle steps.
+During migration, review:
+
+- `HtmxTriggerTiming.Receive` events whose handlers must run before the swap.
+ Move that logic to an `htmx:before:swap` listener.
+- `AfterSwap` and `AfterSettle` events that must not fire when the response is not swapped,
+ for example with status code 204.
+
+See [Response events](version-compatibility.md#response-events).
## A response event does not refresh another component
diff --git a/docs/articles/version-compatibility.md b/docs/articles/version-compatibility.md
index 488c79e..2caa1d0 100644
--- a/docs/articles/version-compatibility.md
+++ b/docs/articles/version-compatibility.md
@@ -74,8 +74,23 @@ Review the target version's guide rather than copying a configuration delegate u
## Response events
HTMX 1.x and 2.x distinguish `HX-Trigger`, `HX-Trigger-After-Swap`, and `HX-Trigger-After-Settle`.
-HTMX 4.x emits Toolkit events through `HX-Trigger` when the request completes.
-An `AfterSettle` timing therefore cannot retain its separate V1/V2 timing under V4.
+HTMX 4.x has only `HX-Trigger`, so the Toolkit emits events of every `HtmxTriggerTiming` through it.
+
+| Timing | HTMX 1.x and 2.x | HTMX 4.x |
+|---|---|---|
+| `Receive` | When the response is received, before the swap | When the request completes |
+| `AfterSwap` | After the swap step, only if the response is swapped | When the request completes |
+| `AfterSettle` | After the settle step, only if the response is swapped | When the request completes |
+
+HTMX 4.x completes the request after the swap and settle steps, so `AfterSwap` and `AfterSettle` events
+still fire after the DOM is updated. Two differences remain:
+
+- `Receive` events no longer fire before the swap. To act before the swap, handle `htmx:before:swap` on the client.
+- `AfterSwap` and `AfterSettle` events also fire when the response is not swapped,
+ for example with status code 204 or an `HX-Location` header.
+
+Because all timings share one header, events fire in call order regardless of timing, and a name added
+with different timings counts as repeated. See [Timing and repeated events](responses.md#timing-and-repeated-events).
## Polling
diff --git a/src/Ramstack.HtmxToolkit/HtmxResponse.cs b/src/Ramstack.HtmxToolkit/HtmxResponse.cs
index fc2e33b..1ea962b 100644
--- a/src/Ramstack.HtmxToolkit/HtmxResponse.cs
+++ b/src/Ramstack.HtmxToolkit/HtmxResponse.cs
@@ -223,14 +223,16 @@ public HtmxResponse Reselect(string value) =>
///
///
///
- /// In HTMX 4.x, every value is emitted through
- /// HX-Trigger and runs when the request completes (after the swap whenever one is performed).
+ /// HTMX 4.x has only the HX-Trigger header, so every value
+ /// is emitted through it. These events fire when the request completes, which is after the swap
+ /// and settle steps whenever a swap is performed.
/// See PR #3900.
///
///
- /// Events are triggered in the order they are added. If an event name is added more than once
- /// for the same header, all events of that header are replayed by the Toolkit script,
- /// which must be loaded for any of them to be triggered.
+ /// Events of one header are triggered in the order they are added. If an event name is added
+ /// more than once for the same header, all events of that header are replayed by the Toolkit script,
+ /// which must be loaded for any of them to be triggered. In HTMX 4.x, this also applies
+ /// to a name added with different timings.
///
///
public HtmxResponse TriggerEvent(string eventName, HtmxTriggerTiming timing = HtmxTriggerTiming.Receive) =>
@@ -242,14 +244,16 @@ public HtmxResponse TriggerEvent(string eventName, HtmxTriggerTiming timing = Ht
///
///
///
- /// In HTMX 4.x, every value is emitted through HX-Trigger
- /// and runs when the request completes (after the swap whenever one is performed).
+ /// HTMX 4.x has only the HX-Trigger header, so every value
+ /// is emitted through it. These events fire when the request completes, which is after the swap
+ /// and settle steps whenever a swap is performed.
/// See PR #3900.
///
///
- /// Events are triggered in the order they are added. If an event name is added more than once
- /// for the same header, all events of that header are replayed by the Toolkit script,
- /// which must be loaded for any of them to be triggered.
+ /// Events of one header are triggered in the order they are added. If an event name is added
+ /// more than once for the same header, all events of that header are replayed by the Toolkit script,
+ /// which must be loaded for any of them to be triggered. In HTMX 4.x, this also applies
+ /// to a name added with different timings.
///
///
/// The event name to trigger.
@@ -282,14 +286,16 @@ static HtmxResponse TriggerEventImpl(HtmxResponse response, string eventName, ob
///
///
///
- /// In HTMX 4.x, every value is emitted through HX-Trigger
- /// and runs when the request completes (after the swap whenever one is performed).
+ /// HTMX 4.x has only the HX-Trigger header, so every value
+ /// is emitted through it. These events fire when the request completes, which is after the swap
+ /// and settle steps whenever a swap is performed.
/// See PR #3900.
///
///
- /// Events are triggered in the order they are added. If an event name is added more than once
- /// for the same header, all events of that header are replayed by the Toolkit script,
- /// which must be loaded for any of them to be triggered.
+ /// Events of one header are triggered in the order they are added. If an event name is added
+ /// more than once for the same header, all events of that header are replayed by the Toolkit script,
+ /// which must be loaded for any of them to be triggered. In HTMX 4.x, this also applies
+ /// to a name added with different timings.
///
///
public HtmxResponse TriggerEvent(string eventName, T detail, JsonTypeInfo jsonTypeInfo, HtmxTriggerTiming timing = HtmxTriggerTiming.Receive) =>
diff --git a/src/Ramstack.HtmxToolkit/HtmxResponseHeaders.cs b/src/Ramstack.HtmxToolkit/HtmxResponseHeaders.cs
index b1d9a3d..70f0ee5 100644
--- a/src/Ramstack.HtmxToolkit/HtmxResponseHeaders.cs
+++ b/src/Ramstack.HtmxToolkit/HtmxResponseHeaders.cs
@@ -138,7 +138,9 @@ public string Reselect
///
/// HTMX 1.x and 2.x trigger these events when the response is received,
/// whereas HTMX 4.x triggers them when the request completes
- /// (after the swap whenever one is performed).
+ /// (after the swap and settle steps whenever a swap is performed).
+ /// In HTMX 4.x, the list also includes the events added with other timings,
+ /// because all timings share the HX-Trigger header.
///
///
public IReadOnlyList>? Trigger => PendingEvents.TryGet(_response)?.GetEvents(HtmxTriggerTiming.Receive);
@@ -155,9 +157,9 @@ public string Reselect
/// the property returns .
///
///
- /// In HTMX 4.x, assigned events are accumulated in
- /// and emitted through HX-Trigger when the request completes
- /// (after the swap whenever one is performed).
+ /// HTMX 4.x has no HX-Trigger-After-Swap header, so these events are accumulated
+ /// in and emitted through HX-Trigger when the request completes
+ /// (after the swap and settle steps whenever a swap is performed).
///
///
public IReadOnlyList>? TriggerAfterSwap => PendingEvents.TryGet(_response)?.GetEvents(HtmxTriggerTiming.AfterSwap);
@@ -174,10 +176,9 @@ public string Reselect
/// the property returns .
///
///
- /// In HTMX 4.x, assigned events are accumulated in
- /// and emitted through HX-Trigger when the request completes
- /// (after the swap whenever one is performed); the requested after-settle timing
- /// cannot be preserved.
+ /// HTMX 4.x has no HX-Trigger-After-Settle header, so these events are accumulated
+ /// in and emitted through HX-Trigger when the request completes
+ /// (after the swap and settle steps whenever a swap is performed).
///
///
public IReadOnlyList>? TriggerAfterSettle => PendingEvents.TryGet(_response)?.GetEvents(HtmxTriggerTiming.AfterSettle);
diff --git a/src/Ramstack.HtmxToolkit/HtmxTriggerTiming.cs b/src/Ramstack.HtmxToolkit/HtmxTriggerTiming.cs
index 80de07a..e705dca 100644
--- a/src/Ramstack.HtmxToolkit/HtmxTriggerTiming.cs
+++ b/src/Ramstack.HtmxToolkit/HtmxTriggerTiming.cs
@@ -9,10 +9,11 @@ public enum HtmxTriggerTiming
/// Maps to the HX-Trigger header.
///
///
- /// HTMX 1.x and 2.x trigger these events when the response is received.
+ /// HTMX 1.x and 2.x trigger these events when the response is received, before the swap.
///
- /// HTMX 4.x triggers them when the request completes, which is after the swap
- /// whenever one is performed.
+ /// HTMX 4.x triggers them when the request completes, which is after the swap and settle steps
+ /// whenever a swap is performed. To act before the swap in HTMX 4.x, handle htmx:before:swap
+ /// on the client.
/// See PR #3900.
///
///
@@ -22,10 +23,14 @@ public enum HtmxTriggerTiming
/// Maps to the HX-Trigger-After-Swap header, which triggers events after the swap step.
///
///
- /// HTMX 1.x and 2.x emit these events through HX-Trigger-After-Swap.
///
- /// HTMX 4.x emits them through HX-Trigger, which also fires when the request
- /// completes (after the swap whenever one is performed).
+ /// HTMX 1.x and 2.x trigger these events only when the response is swapped,
+ /// so they do not fire for a status code such as 204.
+ ///
+ ///
+ /// HTMX 4.x has no HX-Trigger-After-Swap header, so these events are emitted through
+ /// HX-Trigger. They fire when the request completes: after the swap and settle steps
+ /// whenever a swap is performed, and also when the response is not swapped.
/// See PR #3900.
///
///
@@ -36,10 +41,14 @@ public enum HtmxTriggerTiming
/// after the settle step.
///
///
- /// HTMX 1.x and 2.x emit these events through HX-Trigger-After-Settle.
///
- /// HTMX 4.x emits them through HX-Trigger when the request completes, i.e. after
- /// the swap whenever one is performed; the requested after-settle timing cannot be preserved.
+ /// HTMX 1.x and 2.x trigger these events only when the response is swapped,
+ /// so they do not fire for a status code such as 204.
+ ///
+ ///
+ /// HTMX 4.x has no HX-Trigger-After-Settle header, so these events are emitted through
+ /// HX-Trigger. They fire when the request completes: after the swap and settle steps
+ /// whenever a swap is performed, and also when the response is not swapped.
/// See PR #3900.
///
///