An HTMX extension for selective input inclusion. Unlike traditional forms where all inputs are always submitted, this extension lets you scope inputs so they're only included when you want them to be.
In standard HTML forms and HTMX, all inputs within a form are included in every request. This becomes problematic when:
- You have multiple logical forms in one
<form>element - Different buttons should submit different sets of inputs
- You want to reuse the same input names for different purposes
- You need fine-grained control over which data gets sent
hx-scope solves this by letting you tag inputs with scopes and only including them when their scope matches the triggering element.
This is the killer feature: Inputs with hx-name are completely excluded from parent form submission. They're only included when explicitly selected by an hx-scope selector.
nameattribute → Included in standard form submissionhx-nameattribute → Excluded from form submission, only included viahx-scope
This enables component isolation within forms:
<form hx-post="/checkout">
<!-- Line item component 1 - uses hx-name -->
<div class="line-item" hx-ext="scoped-inputs">
<input hx-name="quantity" class="item-1" value="2">
<input hx-name="price" class="item-1" value="10.00">
<button hx-post="/calculate" hx-scope=".item-1" hx-target="#total-1">Calculate</button>
<span id="total-1"></span>
<!-- This uses 'name' so it's submitted with the form -->
<input type="hidden" name="line_total_1" value="20.00">
</div>
<!-- Line item component 2 - uses hx-name -->
<div class="line-item" hx-ext="scoped-inputs">
<input hx-name="quantity" class="item-2" value="1">
<input hx-name="price" class="item-2" value="15.00">
<button hx-post="/calculate" hx-scope=".item-2" hx-target="#total-2">Calculate</button>
<span id="total-2"></span>
<input type="hidden" name="line_total_2" value="15.00">
</div>
<button type="submit">Checkout</button> <!-- Only submits line_total_1, line_total_2 -->
</form>graph TB
subgraph Form["<form hx-post='/checkout'>"]
subgraph Item1["Line Item 1 (Component)"]
Q1["<b>hx-name</b>='quantity'<br/>class='item-1'<br/>❌ Excluded from form"]
P1["<b>hx-name</b>='price'<br/>class='item-1'<br/>❌ Excluded from form"]
Calc1["Calculate Button<br/><b>hx-scope</b>='.item-1'"]
Total1["<b>name</b>='line_total_1'<br/>✅ Included in form"]
end
subgraph Item2["Line Item 2 (Component)"]
Q2["<b>hx-name</b>='quantity'<br/>class='item-2'<br/>❌ Excluded from form"]
P2["<b>hx-name</b>='price'<br/>class='item-2'<br/>❌ Excluded from form"]
Calc2["Calculate Button<br/><b>hx-scope</b>='.item-2'"]
Total2["<b>name</b>='line_total_2'<br/>✅ Included in form"]
end
Submit["Checkout Button"]
end
Calc1 -->|"Sends only<br/>quantity + price<br/>from .item-1"| API1["/calculate"]
Calc2 -->|"Sends only<br/>quantity + price<br/>from .item-2"| API2["/calculate"]
Submit -->|"Sends only<br/>line_total_1<br/>+ line_total_2"| Checkout["/checkout"]
API1 -.->|"Updates"| Total1
API2 -.->|"Updates"| Total2
style Q1 fill:#fee2e2,stroke:#dc2626
style P1 fill:#fee2e2,stroke:#dc2626
style Q2 fill:#fee2e2,stroke:#dc2626
style P2 fill:#fee2e2,stroke:#dc2626
style Total1 fill:#d1fae5,stroke:#22c55e
style Total2 fill:#d1fae5,stroke:#22c55e
style Calc1 fill:#dbeafe,stroke:#3b82f6
style Calc2 fill:#dbeafe,stroke:#3b82f6
style Submit fill:#dbeafe,stroke:#3b82f6
Legend:
- 🔴 Red boxes (
hx-name) = Excluded from parent form, only included viahx-scope - 🟢 Green boxes (
name) = Included in standard form submission - 🔵 Blue boxes = Buttons with different scope targets
What happens:
- Click "Calculate" on item 1 → Only sends
quantityandpricefrom.item-1to/calculate - Server returns updated total → Updates
line_total_1hidden input - Click main "Checkout" → Form submission only sends
line_total_1andline_total_2(not the intermediate quantity/price values)
Without hx-scope, you'd have no way to:
- Use the same input names (
quantity,price) in multiple components - Calculate intermediate values without submitting them to the parent form
- Keep component logic isolated while still being in the same
<form>
<script src="https://unpkg.com/htmx.org@1.9.0"></script>
<script src="path/to/hx-scope.js"></script>npm install hx-scopeThen include in your HTML:
<script src="node_modules/htmx.org/dist/htmx.js"></script>
<script src="node_modules/hx-scope/hx-scope.js"></script>Interactive demos are available in the examples/ directory. To run them:
# Clone the repository
git clone https://github.com/ocomsoft/hx-scope.git
cd hx-scope
# IMPORTANT: Start the web server from the ROOT directory (hx-scope/), not examples/
# This ensures the demos can load ../hx-scope.js correctly
# Start a local web server (choose one):
python -m http.server 8000
# or
npx serve
# or
php -S localhost:8000
# Then open http://localhost:8000/examples/ in your browserImportant Notes:
- The demos use a Service Worker to intercept and display request details, so they must be served from a web server (opening files directly with
file://won't work) - Run the server from the root
hx-scope/directory, not fromexamples/ - Access the demos at
http://localhost:8000/examples/(note the/examples/path)
-
Demo 1: Basic CSS Class Selectors Two separate forms on the same page using class selectors
-
Demo 2: Multiple Selectors Comma-separated CSS selectors to include inputs from multiple groups
-
Demo 3: Attribute Selectors & Combining Selectors Attribute selectors and combining them with classes
-
Demo 4: Checkbox hx-off-value Feature Using
hx-off-valueto send specific values when checkboxes are unchecked -
Demo 5: Component Isolation - Line Item Calculation ⭐ The killer feature! Multiple isolated components with reusable input names inside a single form
<!-- Traditional approach: ALL inputs are submitted with EVERY button click -->
<form>
<input name="username" value="john">
<input name="email" value="john@example.com">
<input name="admin-note" value="note">
<button hx-post="/user">Save User</button> <!-- Sends ALL 3 inputs -->
<button hx-post="/admin">Save Note</button> <!-- Also sends ALL 3 inputs -->
</form>Problem: Both buttons send all three inputs, even though the user button doesn't need admin-note and the admin button doesn't need username or email.
<div hx-ext="scoped-inputs">
<input type="text" hx-name="username" class="user-form" value="john">
<input type="text" hx-name="email" class="user-form" value="john@example.com">
<input type="text" hx-name="admin-note" class="admin-form" value="note">
<button hx-post="/user" hx-scope=".user-form">Save User</button> <!-- Only sends username, email -->
<button hx-post="/admin" hx-scope=".admin-form">Save Note</button> <!-- Only sends admin-note -->
</div>Solution: Each button uses a CSS selector to specify which inputs to include. You have complete control using familiar CSS syntax.
- Enable the extension: Add
hx-ext="scoped-inputs"to a parent element - Add CSS selector to trigger: Add
hx-scope="<css-selector>"to the element that triggers the HTMX request - Name your inputs: Use
hx-name="param-name"on inputs you want to include - Make inputs selectable: Add classes, IDs, or attributes that match your selectors
-
hx-scope: Applied to triggers (buttons, links, etc.)- Contains a CSS selector that matches the inputs to include
- Examples:
.user-form,#section1 input,[data-form="user"],.step-1, .step-2 - Any valid CSS selector works: classes, IDs, attributes, combinators, pseudo-selectors, etc.
-
hx-name: Applied to inputs- Defines the parameter name for the input value
- Similar to the standard
nameattribute - Only inputs with
hx-nameAND matching the CSS selector are included
-
hx-off-value: Optional, applied to checkboxes- Defines what value to send when the checkbox is unchecked
- Without this attribute, unchecked checkboxes send nothing
- Example:
<input type="checkbox" hx-name="subscribe" hx-off-value="0" value="1"> - When checked: sends
subscribe=1, when unchecked: sendssubscribe=0 - Useful for boolean fields where you need explicit true/false values
The extension queries for elements matching the hx-scope selector, then includes only those with hx-name:
- If an element matches the selector AND has
hx-name, it's included - If an element doesn't match the selector, it's excluded (even with
hx-name) - If an element matches but lacks
hx-name, it's ignored
<div hx-ext="scoped-inputs">
<!-- Using comma-separated selectors to include multiple groups -->
<button hx-post="/submit-all" hx-scope=".user-form, .profile-form">Submit All</button>
<!-- Using descendant selectors -->
<button hx-post="/submit-section" hx-scope="#user-section input[hx-name]">Submit Section</button>
<!-- Using attribute selectors -->
<button hx-post="/submit-step" hx-scope="[data-step='1']">Submit Step 1</button>
<div id="user-section">
<input type="text" hx-name="username" class="user-form" value="john">
<input type="text" hx-name="email" class="user-form profile-form" value="john@example.com">
</div>
<input type="text" hx-name="bio" class="profile-form" value="Developer">
<input type="text" hx-name="age" data-step="1" value="30">
</div><div hx-ext="scoped-inputs">
<h2>User Registration</h2>
<input type="text" hx-name="username" class="registration">
<input type="email" hx-name="email" class="registration">
<button hx-post="/register" hx-scope=".registration">Register</button>
<h2>Newsletter Signup</h2>
<input type="email" hx-name="email" class="newsletter">
<button hx-post="/newsletter" hx-scope=".newsletter">Subscribe</button>
<h2>Global Analytics</h2>
<input type="hidden" hx-name="page-id" class="analytics">
<!-- Include analytics with both forms using multiple selectors -->
<button hx-post="/register-with-tracking" hx-scope=".registration, .analytics">Register (tracked)</button>
</div>Key difference: Without scoping, clicking "Register" would send all inputs (username, both emails, page-id). With hx-scope, you use CSS selectors to include exactly the inputs you want.
<div hx-ext="scoped-inputs">
<div id="shipping-info">
<input hx-name="shipping_address" value="123 Main St">
<input hx-name="shipping_city" value="Boston">
</div>
<div id="billing-info">
<input hx-name="billing_address" value="456 Oak Ave">
<input hx-name="billing_city" value="NYC">
</div>
<!-- Include all inputs within #shipping-info -->
<button hx-post="/update-shipping" hx-scope="#shipping-info input[hx-name]">
Update Shipping
</button>
<!-- Include inputs from both sections -->
<button hx-post="/checkout" hx-scope="#shipping-info input, #billing-info input">
Checkout
</button>
</div><div hx-ext="scoped-inputs">
<input hx-name="username" class="user-data sensitive">
<input hx-name="email" class="user-data">
<input hx-name="bio" class="user-data">
<input hx-name="password" class="user-data sensitive">
<!-- Include all user-data EXCEPT sensitive fields -->
<button hx-post="/preview" hx-scope=".user-data:not(.sensitive)">
Preview Profile
</button>
<!-- Include everything -->
<button hx-post="/save" hx-scope=".user-data">
Save All
</button>
</div><div hx-ext="scoped-inputs" id="form-container">
<div class="section">
<input hx-name="field1" value="direct child">
<div class="nested">
<input hx-name="field2" value="nested descendant">
</div>
</div>
<!-- Direct children only (>) -->
<button hx-post="/api/direct" hx-scope=".section > input[hx-name]">
Submit Direct Children
</button>
<!-- All descendants (space) -->
<button hx-post="/api/all" hx-scope=".section input[hx-name]">
Submit All Descendants
</button>
</div><div hx-ext="scoped-inputs">
<div class="step" data-step="1">
<input hx-name="name" value="John">
<input hx-name="age" value="30">
</div>
<div class="step" data-step="2">
<input hx-name="address" value="123 Main">
<input hx-name="city" value="Boston">
</div>
<div class="step" data-step="3">
<input hx-name="card_number" value="****">
<input hx-name="cvv" value="123">
</div>
<!-- Submit current step only -->
<button hx-post="/save-step-1" hx-scope="[data-step='1'] input[hx-name]">
Save Step 1
</button>
<!-- Submit steps 1 and 2 together -->
<button hx-post="/save-progress" hx-scope="[data-step='1'] input, [data-step='2'] input">
Save Progress
</button>
<!-- Submit all steps -->
<button hx-post="/submit-all" hx-scope=".step input[hx-name]">
Complete Wizard
</button>
</div><div hx-ext="scoped-inputs">
<input hx-name="public_name" class="profile" data-visibility="public">
<input hx-name="public_bio" class="profile" data-visibility="public">
<input hx-name="private_email" class="profile" data-visibility="private">
<input hx-name="admin_notes" class="admin">
<!-- Only public profile fields -->
<button hx-post="/api/public" hx-scope=".profile[data-visibility='public']">
Update Public Profile
</button>
<!-- All profile fields (public + private) -->
<button hx-post="/api/profile" hx-scope=".profile">
Update Full Profile
</button>
<!-- Everything including admin -->
<button hx-post="/api/admin-save" hx-scope=".profile, .admin">
Admin Save
</button>
</div><div hx-ext="scoped-inputs">
<input hx-name="required_field1" data-required>
<input hx-name="optional_field1">
<input hx-name="required_field2" data-required>
<input hx-name="optional_field2">
<!-- Only required fields -->
<button hx-post="/validate-required" hx-scope="[data-required]">
Check Required Fields
</button>
<!-- Only optional fields -->
<button hx-post="/validate-optional" hx-scope="input[hx-name]:not([data-required])">
Check Optional Fields
</button>
</div><div hx-ext="scoped-inputs">
<!-- User A inputs -->
<input hx-name="email" data-user="user-a" value="alice@example.com">
<input hx-name="phone" data-user="user-a" value="555-0001">
<!-- User B inputs -->
<input hx-name="email" data-user="user-b" value="bob@example.com">
<input hx-name="phone" data-user="user-b" value="555-0002">
<!-- Global/shared inputs -->
<input hx-name="organization" data-shared value="Acme Corp">
<!-- Update just User A -->
<button hx-post="/update-user/a" hx-scope="[data-user='user-a']">
Update Alice
</button>
<!-- Update User B with shared data -->
<button hx-post="/update-user/b" hx-scope="[data-user='user-b'], [data-shared]">
Update Bob (+ Org)
</button>
</div><div hx-ext="scoped-inputs">
<input hx-name="title" type="text" value="My Post">
<textarea hx-name="content">Post content here</textarea>
<input hx-name="publish_date" type="date">
<input hx-name="featured_image" type="file">
<input hx-name="tags" type="text">
<!-- Text inputs only -->
<button hx-post="/save-text" hx-scope="input[type='text']">
Save Text Fields
</button>
<!-- Everything except file inputs -->
<button hx-post="/save-draft" hx-scope="input:not([type='file']), textarea">
Save Draft
</button>
<!-- All inputs and textareas -->
<button hx-post="/publish" hx-scope="input[hx-name], textarea[hx-name]">
Publish
</button>
</div><div hx-ext="scoped-inputs">
<!-- Standard checkboxes with hx-off-value -->
<input type="checkbox" hx-name="notifications" class="settings" hx-off-value="0" value="1" checked>
<input type="checkbox" hx-name="newsletter" class="settings" hx-off-value="false" value="true">
<input type="checkbox" hx-name="dark_mode" class="settings" hx-off-value="no" value="yes" checked>
<!-- Without hx-off-value, unchecked boxes send nothing -->
<input type="checkbox" hx-name="terms_accepted" class="settings" value="1">
<button hx-post="/save-settings" hx-scope=".settings">
Save Settings
</button>
</div>What gets sent:
- If all are checked:
notifications=1&newsletter=true&dark_mode=yes&terms_accepted=1 - If all unchecked:
notifications=0&newsletter=false&dark_mode=no(terms_accepted not sent)
This is useful for:
- Boolean preferences where you need explicit true/false values
- Database fields that expect 1/0 or yes/no
- APIs that require explicit false values instead of omitted fields
This example demonstrates the power of hx-name vs name separation - multiple isolated components with reusable input names inside a single form:
<form hx-post="/checkout" hx-ext="scoped-inputs">
<h2>Shopping Cart</h2>
<!-- Line Item 1 -->
<div class="line-item">
<h3>Product A</h3>
<label>
Quantity:
<input type="number" hx-name="quantity" class="item-1" value="2" min="1">
</label>
<label>
Price:
<input type="number" hx-name="price" class="item-1" value="10.00" step="0.01" readonly>
</label>
<button hx-post="/calculate" hx-scope=".item-1" hx-target="#line-1-total">
Calculate Line Total
</button>
<div id="line-1-total">
Total: $<span id="total-1-value">20.00</span>
</div>
<!-- This hidden input uses 'name' so it's submitted with checkout -->
<input type="hidden" name="line_total_1" id="line-1-hidden" value="20.00">
<input type="hidden" name="product_id_1" value="prod_a">
</div>
<!-- Line Item 2 - Same input names, different scope! -->
<div class="line-item">
<h3>Product B</h3>
<label>
Quantity:
<input type="number" hx-name="quantity" class="item-2" value="1" min="1">
</label>
<label>
Price:
<input type="number" hx-name="price" class="item-2" value="15.00" step="0.01" readonly>
</label>
<button hx-post="/calculate" hx-scope=".item-2" hx-target="#line-2-total">
Calculate Line Total
</button>
<div id="line-2-total">
Total: $<span id="total-2-value">15.00</span>
</div>
<input type="hidden" name="line_total_2" id="line-2-hidden" value="15.00">
<input type="hidden" name="product_id_2" value="prod_b">
</div>
<!-- Line Item 3 -->
<div class="line-item">
<h3>Product C</h3>
<label>
Quantity:
<input type="number" hx-name="quantity" class="item-3" value="3" min="1">
</label>
<label>
Price:
<input type="number" hx-name="price" class="item-3" value="7.50" step="0.01" readonly>
</label>
<button hx-post="/calculate" hx-scope=".item-3" hx-target="#line-3-total">
Calculate Line Total
</button>
<div id="line-3-total">
Total: $<span id="total-3-value">22.50</span>
</div>
<input type="hidden" name="line_total_3" id="line-3-hidden" value="22.50">
<input type="hidden" name="product_id_3" value="prod_c">
</div>
<hr>
<div>
<strong>Cart Total: $57.50</strong>
</div>
<button type="submit">Proceed to Checkout</button>
</form>How it works:
-
Component isolation: Each line item uses
hx-name="quantity"andhx-name="price"- the same names! This is possible because they're scoped to different classes (.item-1,.item-2,.item-3) -
Intermediate calculations: When you click "Calculate Line Total" on item 1:
- Only
.item-1inputs are sent to/calculate - Server calculates
quantity * priceand returns the result - The hidden input
line_total_1(withnameattribute) is updated - Other line items are unaffected
- Only
-
Form submission: When you click "Proceed to Checkout":
- Form sends:
line_total_1=20.00&product_id_1=prod_a&line_total_2=15.00&product_id_2=prod_b&line_total_3=22.50&product_id_3=prod_c - Does NOT send the intermediate
quantityandpricevalues (they usehx-name, notname)
- Form sends:
Backend example (/calculate endpoint):
@app.post("/calculate")
def calculate(quantity: float, price: float):
total = quantity * price
return f"Total: ${total:.2f}"Why this matters:
- Reusable component logic: Each line item is self-contained with identical input names
- No naming conflicts: Multiple components can coexist in the same form
- Clean form submission: Only final calculated values are submitted, not intermediate inputs
- Progressive enhancement: Each line item can calculate independently without affecting others
Unlike traditional forms that submit all inputs regardless of which button is clicked, hx-scope gives you precise control using CSS selectors:
- Multiple logical forms in one container: Use classes like
.user-formand.admin-formto distinguish different form groups in the same area - Conditional data submission: Different buttons use different selectors to send completely different sets of inputs, even if they're intermingled
- Prevent data leakage: Ensure sensitive inputs are only sent when explicitly selected, not accidentally included in every request
- Structural selection: Use selectors like
#section1 inputto include all inputs within a specific container - Attribute-based grouping: Use
[data-step="1"]to group inputs by custom attributes - Wizard-style forms: Use
.step-1,.step-2classes and submit only the current step - Complex selection logic: Combine selectors with commas (
.form1, .form2), use:not()to exclude elements, or any other CSS selector feature - No naming conflicts: The same input can match multiple selectors by having multiple classes
Works with all browsers supported by HTMX (IE11+, modern browsers).
MIT License - see LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.