How htmx Executes Scripts After Swapping Content: A Deep Dive into the Source Code
When htmx swaps HTML content received from an AJAX response, it clones <script> elements and inserts them directly into the live DOM to trigger immediate execution while respecting Content Security Policy settings.
When working with dynamic content updates in the bigskysoftware/htmx library, understanding how inline JavaScript gets executed is crucial for debugging and security. By default, htmx processes and runs scripts embedded in swapped HTML fragments, but this behavior can be configured or disabled entirely. The implementation relies on a sophisticated DOM manipulation strategy that ensures browser compatibility while supporting strict CSP environments.
The DocumentFragment Pipeline and Script Detection
After htmx receives an AJAX response, it constructs a DocumentFragment from the returned HTML using the internal makeFragment utility. According to the source code in src/htmx.js (lines 636‑638), if the global configuration htmx.config.allowScriptTags is set to true (the default defined at line 160), the fragment is passed to the normalizeScriptTags function for processing.
If script execution is disabled via configuration, htmx strips all <script> elements from the fragment at lines 640‑641 before insertion, ensuring no code execution occurs during the swap operation.
How normalizeScriptTags Executes Scripts
The normalizeScriptTags function (lines 577‑590) implements a three-step process to safely execute JavaScript after content insertion:
Detecting Valid JavaScript Nodes
Before processing, htmx validates each script using isJavaScriptScriptNode (lines 66‑68). This utility checks that the script has a valid JavaScript MIME type (text/javascript, module, or empty type), ensuring that non-executable scripts or data blocks are ignored during the normalization process.
Cloning Script Elements with duplicateScript
For each valid JavaScript node, htmx invokes duplicateScript (lines 549‑558) to create a fresh <script> element. This function:
- Copies all attributes from the original node
- Transfers the text content to the new element
- Forces
async = falseto guarantee synchronous execution - Optionally injects a nonce from
htmx.config.inlineScriptNoncefor CSP compatibility
Live DOM Insertion for Immediate Execution
The cloned script is inserted directly before the original node using parent.insertBefore(newScript, script), and the original is immediately removed. Because the new element is created and inserted into the live DOM (not via a <template> element), browsers execute its code immediately. This technique circumvents browser limitations where scripts inside <template> tags remain inert when moved to the document.
Configuration Options for Script Security
The bigskysoftware/htmx repository provides granular control over script execution through configuration flags that accommodate both development convenience and strict security policies.
Enabling and Disabling Scripts with allowScriptTags
By default, htmx.config.allowScriptTags is true (line 160), allowing inline scripts to run automatically after swaps. To disable script execution entirely—for example, when implementing a strict Content Security Policy without 'unsafe-inline'—set this configuration to false:
htmx.config.allowScriptTags = false;
When disabled, htmx removes all <script> tags from swapped content at lines 640‑641, preventing any code execution regardless of the response content.
CSP Compatibility with inlineScriptNonce
For environments requiring nonce-based CSP directives, htmx supports the htmx.config.inlineScriptNonce property. When configured, the duplicateScript function automatically adds the nonce attribute to cloned script elements:
htmx.config.inlineScriptNonce = 'abc123';
This allows browsers to execute inline scripts that would otherwise be blocked by CSP policies requiring 'nonce-abc123'.
Practical Implementation Examples
Default Script Execution
When allowScriptTags remains enabled (default), any JavaScript returned in an htmx response executes immediately:
<button hx-get="/update" hx-swap="innerHTML">Load Content</button>
If /update returns:
<div>
<script>
console.log('Executed after swap');
document.dispatchEvent(new CustomEvent('contentLoaded'));
</script>
<p>New content loaded</p>
</div>
The console logs immediately upon swap completion, and the custom event fires as expected.
Disabling Scripts for Strict CSP
To prevent script execution in environments with strict security requirements:
// In your initialization code or htmx-config.js
htmx.config.allowScriptTags = false;
Now the same response above would render the paragraph but strip the <script> element entirely, preventing execution while maintaining the HTML structure.
Using Nonce Attributes
For CSP-compliant inline script execution:
htmx.config.inlineScriptNonce = document.querySelector('meta[name="csp-nonce"]').content;
This configuration ensures all dynamically inserted scripts receive the correct nonce attribute, satisfying 'nonce-source' CSP directives without requiring 'unsafe-inline'.
Summary
- htmx creates a DocumentFragment from AJAX responses and processes scripts via
normalizeScriptTagsbefore DOM insertion. - The
duplicateScriptfunction clones script elements to force immediate execution in the live DOM, settingasync=falseand preserving attributes. - Script execution is controlled by
htmx.config.allowScriptTags(default:trueat line 160), which can be disabled to strip all scripts for CSP compliance. - The
htmx.config.inlineScriptNonceproperty enables nonce injection into cloned scripts for environments with strict Content Security Policies. - Only scripts passing the
isJavaScriptScriptNodecheck (lines 66‑68) are processed, ensuring type validation before execution.
Frequently Asked Questions
Does htmx execute scripts by default?
Yes. By default, htmx.config.allowScriptTags is set to true in the source code at line 160. When htmx swaps content containing <script> tags, it clones and inserts them into the live DOM to trigger immediate execution. You must explicitly set this configuration to false to disable this behavior.
Why does htmx clone script elements instead of using the original?
htmx uses the duplicateScript function (lines 549‑558) to create fresh script elements because browsers typically do not execute scripts moved from a <template> or DocumentFragment into the live DOM. By creating a new element and inserting it directly with parent.insertBefore(), htmx ensures the browser recognizes and executes the script immediately. The cloning process also allows htmx to inject nonce attributes and force synchronous execution with async = false.
How can I prevent script execution for CSP compliance?
Set htmx.config.allowScriptTags = false before any swaps occur. When disabled, htmx will strip all <script> elements from the response at lines 640‑641 of src/htmx.js, preventing any inline code execution. This configuration is essential for environments using strict Content Security Policies without 'unsafe-inline' permissions.
Do external scripts (src attribute) work with htmx swaps?
Yes. The duplicateScript function copies all attributes from the original script element, including src, type, and defer. When cloned and inserted into the DOM, external scripts load and execute according to their specified attributes. However, if allowScriptTags is false, these scripts are removed regardless of whether they are inline or external.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →