diff --git a/INTEGRATIONS.md b/INTEGRATIONS.md
new file mode 100644
index 0000000..5984ef1
--- /dev/null
+++ b/INTEGRATIONS.md
@@ -0,0 +1,445 @@
+# Supported Integrations (connectors)
+
+`go-partial` currently supports the following connectors:
+
+- HTMX
+- Turbo
+- Unpoly
+- Alpine.js
+- Stimulus
+- Partial (Custom Connector)
+
+## HTMX
+
+### Description:
+[HTMX](https://htmx.org/) allows you to use AJAX, WebSockets, and Server-Sent Events directly in HTML using attributes.
+
+### Server-Side Setup:
+```go
+import (
+    "github.com/donseba/go-partial"
+    "github.com/donseba/go-partial/connector"
+)
+
+// Create a new partial
+contentPartial := partial.New("templates/content.gohtml").ID("content")
+
+// Set the HTMX connector
+contentPartial.SetConnector(connector.NewHTMX(&connector.Config{
+    UseURLQuery: true, // Enable fallback to URL query parameters
+}))
+
+// Optionally add actions or selections
+contentPartial.WithAction(func(ctx context.Context, p *partial.Partial, data *partial.Data) (*partial.Partial, error) {
+    // Action logic here
+    return p, nil
+})
+
+// Handler function
+func contentHandler(w http.ResponseWriter, r *http.Request) {
+    ctx := r.Context()
+    err := contentPartial.WriteWithRequest(ctx, w, r)
+    if err != nil {
+        http.Error(w, err.Error(), http.StatusInternalServerError)
+    }
+}
+```
+
+### Client-Side Setup:
+```html
+<!-- Load content into #content div when clicked -->
+<button hx-get="/content" hx-target="#content" hx-headers='{"HX-Select": "tab1"}'>Tab 1</button>
+<button hx-get="/content" hx-target="#content" hx-headers='{"HX-Select": "tab2"}'>Tab 2</button>
+
+<!-- Content area -->
+<div id="content">
+    <!-- Dynamic content will be loaded here -->
+</div>
+```
+
+### alternative: 
+```html
+<button hx-get="/content" hx-target="#content" hx-headers='{"X-Select": "tab1"}'>Tab 1</button>
+<button hx-get="/content" hx-target="#content" hx-headers='{"X-Select": "tab2"}'>Tab 2</button>
+```
+
+### alternative 2:
+```html
+<button hx-get="/content" hx-target="#content" hx-params="select=tab1">Tab 1</button>
+<button hx-get="/content" hx-target="#content" hx-params="select=tab2">Tab 2</button>
+```
+
+## Turbo
+### Description:
+[Turbo](https://turbo.hotwired.dev/) speeds up web applications by reducing the amount of custom JavaScript needed to provide rich, modern user experiences.
+
+### Server-Side Setup:
+```go
+import (
+    "github.com/donseba/go-partial"
+    "github.com/donseba/go-partial/connector"
+)
+
+// Create a new partial
+contentPartial := partial.New("templates/content.gohtml").ID("content")
+
+// Set the Turbo connector
+contentPartial.SetConnector(connector.NewTurbo(&connector.Config{
+    UseURLQuery: true,
+}))
+
+// Handler function
+func contentHandler(w http.ResponseWriter, r *http.Request) {
+    ctx := r.Context()
+    err := contentPartial.WriteWithRequest(ctx, w, r)
+    if err != nil {
+        http.Error(w, err.Error(), http.StatusInternalServerError)
+    }
+}
+```
+
+### Client-Side Setup:
+```html
+<turbo-frame id="content">
+    <!-- Dynamic content will be loaded here -->
+</turbo-frame>
+
+<!-- Links to load content -->
+<a href="/content?select=tab1" data-turbo-frame="content">Tab 1</a>
+<a href="/content?select=tab2" data-turbo-frame="content">Tab 2</a>
+```
+
+## Unpoly
+### Description:
+[Unpoly](https://unpoly.com/) enables fast and flexible server-side rendering with minimal custom JavaScript.
+
+### Server-Side Setup:
+```go
+import (
+    "github.com/donseba/go-partial"
+    "github.com/donseba/go-partial/connector"
+)
+
+// Create a new partial
+contentPartial := partial.New("templates/content.gohtml").ID("content")
+
+// Set the Unpoly connector
+contentPartial.SetConnector(connector.NewUnpoly(&connector.Config{
+    UseURLQuery: true,
+}))
+
+// Handler function
+func contentHandler(w http.ResponseWriter, r *http.Request) {
+    ctx := r.Context()
+    err := contentPartial.WriteWithRequest(ctx, w, r)
+    if err != nil {
+        http.Error(w, err.Error(), http.StatusInternalServerError)
+    }
+}
+```
+
+### Client-Side Setup:
+```html
+<!-- Links to load content -->
+<a href="/content?select=tab1" up-target="#content" up-headers='{"X-Up-Select": "tab1"}'>Tab 1</a>
+<a href="/content?select=tab2" up-target="#content" up-headers='{"X-Up-Select": "tab2"}'>Tab 2</a>
+
+<!-- Content area -->
+<div id="content">
+    <!-- Dynamic content will be loaded here -->
+</div>
+```
+
+### Alternative:
+```html
+<a href="/content?select=tab1" up-target="#content">Tab 1</a>
+<a href="/content?select=tab2" up-target="#content">Tab 2</a>
+```
+
+## Alpine.js
+### Description:
+[Alpine.js](https://alpinejs.dev/) offers a minimal and declarative way to render reactive components in the browser.
+
+### Server-Side Setup:
+```go
+import (
+    "github.com/donseba/go-partial"
+    "github.com/donseba/go-partial/connector"
+)
+
+// Create a new partial
+contentPartial := partial.New("templates/content.gohtml").ID("content")
+
+// Set the Alpine.js connector
+contentPartial.SetConnector(connector.NewAlpine(&connector.Config{
+    UseURLQuery: true,
+}))
+
+// Handler function
+func contentHandler(w http.ResponseWriter, r *http.Request) {
+    ctx := r.Context()
+    err := contentPartial.WriteWithRequest(ctx, w, r)
+    if err != nil {
+        http.Error(w, err.Error(), http.StatusInternalServerError)
+    }
+}
+```
+
+### Client-Side Setup:
+```html
+<div>
+    <!-- Buttons to load content -->
+    <form method="get" action="/page" x-target="content" x-headers="{'X-Alpine-Select': 'tab1'}">
+        <button type="submit">Tab 1</button>
+    </form>    
+    
+    <form method="get" action="/page" x-target="content" x-headers="{'X-Alpine-Select': 'tab2'}">
+        <button type="submit">Tab 2</button>
+    </form>
+
+    <!-- Content area -->
+    <div id="content">
+        <!-- Dynamic content will be loaded here -->
+    </div>
+</div>
+```
+
+## Alpine Ajax
+### Description:
+[Alpine Ajax](https://alpine-ajax.js.org) is an Alpine.js plugin that enables your HTML elements to request remote content from your server.
+### Server-Side Setup:
+```go
+import (
+    "github.com/donseba/go-partial"
+    "github.com/donseba/go-partial/connector"
+)
+
+// Create a new partial
+contentPartial := partial.New("templates/content.gohtml").ID("content")
+
+// Set the Alpine-AJAX connector
+contentPartial.SetConnector(connector.NewAlpineAjax(&connector.Config{
+UseURLQuery: true, // Enable fallback to URL query parameters
+}))
+
+// Handler function
+func contentHandler(w http.ResponseWriter, r *http.Request) {
+    ctx := r.Context()
+    err := contentPartial.WriteWithRequest(ctx, w, r)
+    if err != nil {
+        http.Error(w, err.Error(), http.StatusInternalServerError)
+    }
+}
+```
+
+### Client-Side Setup:
+```html
+<!-- Include Alpine.js and Alpine-AJAX -->
+<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
+<script src="https://unpkg.com/alpine-ajax@latest/dist/alpine-ajax.min.js" defer></script>
+
+<!-- Initialize Alpine -->
+<div x-data>
+
+    <!-- Buttons to load content -->
+    <button x-on:click="click" x-get="/content" x-target="#content" x-headers='{"X-Alpine-Select": "tab1"}'>Tab 1</button>
+    <button x-on:click="click" x-get="/content" x-target="#content" x-headers='{"X-Alpine-Select": "tab1"}'>Tab 2</button>
+
+    <!-- Content area -->
+    <div id="content">
+        <!-- Dynamic content will be loaded here -->
+    </div>
+</div>
+
+```
+
+## Stimulus
+### Description:
+[Stimulus](https://stimulus.hotwired.dev/) is a JavaScript framework that enhances static or server-rendered HTML with just enough behavior.
+
+### Server-Side Setup:
+```go
+import (
+    "github.com/donseba/go-partial"
+    "github.com/donseba/go-partial/connector"
+)
+
+// Create a new partial
+contentPartial := partial.New("templates/content.gohtml").ID("content")
+
+// Set the Stimulus connector
+contentPartial.SetConnector(connector.NewStimulus(&connector.Config{
+    UseURLQuery: true,
+}))
+
+// Handler function
+func contentHandler(w http.ResponseWriter, r *http.Request) {
+    ctx := r.Context()
+    err := contentPartial.WriteWithRequest(ctx, w, r)
+    if err != nil {
+        http.Error(w, err.Error(), http.StatusInternalServerError)
+    }
+}
+```
+
+### Client-Side Setup:
+```html
+<div data-controller="ajax">
+    <!-- Buttons to load content -->
+    <button data-action="click->ajax#load" data-select="tab1">Tab 1</button>
+    <button data-action="click->ajax#load" data-select="tab2">Tab 2</button>
+
+    <!-- Content area -->
+    <div id="content">
+        <!-- Dynamic content will be loaded here -->
+    </div>
+</div>
+
+<script>
+import { Controller } from "stimulus"
+
+export default class extends Controller {
+    load(event) {
+        event.preventDefault()
+        const select = event.target.dataset.select
+        fetch('/content', {
+            headers: {
+                'X-Stimulus-Target': 'content',
+                'X-Stimulus-Select': select
+            }
+        })
+        .then(response => response.text())
+        .then(html => {
+            document.getElementById('content').innerHTML = html
+        })
+    }
+}
+</script>
+```
+
+## Partial (Custom Connector)
+### Description:
+The Partial connector is a simple, custom connector provided by go-partial. It can be used when you don't rely on any specific front-end library.
+
+### Server-Side Setup:
+```go
+import (
+    "github.com/donseba/go-partial"
+    "github.com/donseba/go-partial/connector"
+)
+
+// Create a new partial
+contentPartial := partial.New("templates/content.gohtml").ID("content")
+
+// Set the custom Partial connector
+contentPartial.SetConnector(connector.NewPartial(&connector.Config{
+    UseURLQuery: true,
+}))
+
+// Handler function
+func contentHandler(w http.ResponseWriter, r *http.Request) {
+    ctx := r.Context()
+    err := contentPartial.WriteWithRequest(ctx, w, r)
+    if err != nil {
+        http.Error(w, err.Error(), http.StatusInternalServerError)
+    }
+}
+
+```
+### Client-Side Usage:
+```html
+<button x-get="/" x-target="#content" x-select="tab1">Tab 1</button>
+<button x-get="/" x-target="#content" x-select="tab2 ">Tab 2</button>
+
+<!-- Content area -->
+<div id="content">
+    <!-- Dynamic content will be loaded here -->
+</div>
+```
+
+## Vue.js
+### Description:
+[Vue.js](https://vuejs.org/) is a progressive JavaScript framework for building user interfaces.
+
+### Note:
+Integrating go-partial with Vue.js for partial HTML updates is possible but comes with limitations. For small sections of the page or simple content updates, it can work. For larger applications, consider whether server-rendered partials align with your architecture.
+
+### Server-Side Setup:
+```go
+import (
+    "github.com/donseba/go-partial"
+    "github.com/donseba/go-partial/connector"
+)
+
+// Create a new partial
+contentPartial := partial.New("templates/content.gohtml").ID("content")
+
+// Set the Vue connector
+contentPartial.SetConnector(connector.NewVue(&connector.Config{
+    UseURLQuery: true,
+}))
+
+// Handler function
+func contentHandler(w http.ResponseWriter, r *http.Request) {
+    ctx := r.Context()
+    err := contentPartial.WriteWithRequest(ctx, w, r)
+    if err != nil {
+        http.Error(w, err.Error(), http.StatusInternalServerError)
+    }
+}
+```
+
+### Client-Side Setup:
+```html
+<template>
+  <div>
+    <!-- Buttons to load content -->
+    <button @click="loadContent('tab1')">Tab 1</button>
+    <button @click="loadContent('tab2')">Tab 2</button>
+
+    <!-- Content area -->
+    <div v-html="content"></div>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      content: ''
+    }
+  },
+  methods: {
+    loadContent(select) {
+      fetch('/content', {
+        headers: {
+          'X-Vue-Target': 'content',
+          'X-Vue-Select': select
+        }
+      })
+      .then(response => response.text())
+      .then(html => {
+        this.content = html;
+      });
+    }
+  }
+}
+</script>
+```
+
+### using axios:
+```javascript
+methods: {
+  loadContent(select) {
+    axios.get('/content', {
+      headers: {
+        'X-Vue-Target': 'content',
+        'X-Vue-Select': select
+      }
+    })
+    .then(response => {
+      this.content = response.data;
+    });
+  }
+}
+```
\ No newline at end of file
diff --git a/README.md b/README.md
index 14c1e00..a021d93 100644
--- a/README.md
+++ b/README.md
@@ -20,12 +20,20 @@ go get github.com/donseba/go-partial
 ## Advanced use cases 
 Advanced usecases are documented in the [ADVANCED.md](ADVANCED.md) file
 
-## Basic Usage
+## Integrations
+Several integrations are available, detailed information can be found in the [INTEGRATIONS.md](INTEGRATIONS.md) file
+- htmx
+- Turbo
+- Stimulus
+- Unpoly
+- Alpine.js / Alpine Ajax (not great)
+- Vue.js (not great)
+- Standalone
 
+## Basic Usage
 Here's a simple example of how to use the package to render a template.
 
 ### 1. Create a Service
-
 The `Service` holds global configurations and data.
 
 ```go 
@@ -44,7 +52,6 @@ service.SetData(map[string]any{
 ```
 
 ## 2. Create a Layout
-
 The `Layout` manages the overall structure of your templates.
 ```go
 layout := service.NewLayout()
@@ -54,7 +61,6 @@ layout.SetData(map[string]any{
 ```
 
 ### 3. Define Partials
-
 Create `Partial` instances for the content and any other components.
 
 ```go 
@@ -81,7 +87,6 @@ func handler(w http.ResponseWriter, r *http.Request) {
 ```
 
 ## Template Files
-
 templates/layout.html
 ```html
 <!DOCTYPE html>
@@ -103,7 +108,6 @@ Note: In the layout template, we use {{ child "content" }} to render the content
 
 
 ### Using Global and Layout Data
-
 - **Global Data (ServiceData)**: Set on the Service, accessible via {{.Service}} in templates.
 - **Layout Data (LayoutData)**: Set on the Layout, accessible via {{.Layout}} in templates.
 - **Partial Data (Data)**: Set on individual Partial instances, accessible via {{.Data}} in templates.
diff --git a/connector/alpine-ajax.go b/connector/alpine-ajax.go
new file mode 100644
index 0000000..d6a8577
--- /dev/null
+++ b/connector/alpine-ajax.go
@@ -0,0 +1,22 @@
+package connector
+
+import "net/http"
+
+type AlpineAjax struct {
+	base
+}
+
+func NewAlpineAjax(c *Config) Connector {
+	return &AlpineAjax{
+		base: base{
+			config:       c,
+			targetHeader: "X-Alpine-Target",
+			selectHeader: "X-Alpine-Select",
+			actionHeader: "X-Alpine-Action",
+		},
+	}
+}
+
+func (a *AlpineAjax) RenderPartial(r *http.Request) bool {
+	return r.Header.Get(a.targetHeader) != ""
+}
diff --git a/connector/alpine.go b/connector/alpine.go
new file mode 100644
index 0000000..624f321
--- /dev/null
+++ b/connector/alpine.go
@@ -0,0 +1,22 @@
+package connector
+
+import "net/http"
+
+type Alpine struct {
+	base
+}
+
+func NewAlpine(c *Config) Connector {
+	return &Alpine{
+		base: base{
+			config:       c,
+			targetHeader: "X-Alpine-Target",
+			selectHeader: "X-Alpine-Select",
+			actionHeader: "X-Alpine-Action",
+		},
+	}
+}
+
+func (a *Alpine) RenderPartial(r *http.Request) bool {
+	return r.Header.Get(a.targetHeader) != ""
+}
diff --git a/connector/connector.go b/connector/connector.go
new file mode 100644
index 0000000..0b8c007
--- /dev/null
+++ b/connector/connector.go
@@ -0,0 +1,87 @@
+package connector
+
+import "net/http"
+
+type (
+	Connector interface {
+		RenderPartial(r *http.Request) bool
+		GetTargetValue(r *http.Request) string
+		GetSelectValue(r *http.Request) string
+		GetActionValue(r *http.Request) string
+
+		GetTargetHeader() string
+		GetSelectHeader() string
+		GetActionHeader() string
+	}
+
+	Config struct {
+		UseURLQuery bool
+	}
+
+	base struct {
+		config       *Config
+		targetHeader string
+		selectHeader string
+		actionHeader string
+	}
+)
+
+func (x *base) RenderPartial(r *http.Request) bool {
+	return r.Header.Get(x.targetHeader) != ""
+}
+
+func (x *base) GetTargetHeader() string {
+	return x.targetHeader
+}
+
+func (x *base) GetSelectHeader() string {
+	return x.selectHeader
+}
+
+func (x *base) GetActionHeader() string {
+	return x.actionHeader
+}
+
+func (x *base) GetTargetValue(r *http.Request) string {
+	if targetValue := r.Header.Get(x.targetHeader); targetValue != "" {
+		return targetValue
+	}
+
+	if x.config.useURLQuery() {
+		return r.URL.Query().Get("target")
+	}
+
+	return ""
+}
+
+func (x *base) GetSelectValue(r *http.Request) string {
+	if selectValue := r.Header.Get(x.selectHeader); selectValue != "" {
+		return selectValue
+	}
+
+	if x.config.useURLQuery() {
+		return r.URL.Query().Get("select")
+	}
+
+	return ""
+}
+
+func (x *base) GetActionValue(r *http.Request) string {
+	if actionValue := r.Header.Get(x.actionHeader); actionValue != "" {
+		return actionValue
+	}
+
+	if x.config.useURLQuery() {
+		return r.URL.Query().Get("action")
+	}
+
+	return ""
+}
+
+func (c *Config) useURLQuery() bool {
+	if c == nil {
+		return false
+	}
+
+	return c.UseURLQuery
+}
diff --git a/connector/htmx.go b/connector/htmx.go
new file mode 100644
index 0000000..66838e1
--- /dev/null
+++ b/connector/htmx.go
@@ -0,0 +1,35 @@
+package connector
+
+import (
+	"net/http"
+)
+
+type HTMX struct {
+	base
+
+	requestHeader               string
+	boostedHeader               string
+	historyRestoreRequestHeader string
+}
+
+func NewHTMX(c *Config) Connector {
+	return &HTMX{
+		base: base{
+			config:       c,
+			targetHeader: "HX-Target",
+			selectHeader: "X-Select",
+			actionHeader: "X-Action",
+		},
+		requestHeader:               "HX-Request",
+		boostedHeader:               "HX-Boosted",
+		historyRestoreRequestHeader: "HX-History-Restore-Request",
+	}
+}
+
+func (h *HTMX) RenderPartial(r *http.Request) bool {
+	hxRequest := r.Header.Get(h.requestHeader)
+	hxBoosted := r.Header.Get(h.boostedHeader)
+	hxHistoryRestoreRequest := r.Header.Get(h.historyRestoreRequestHeader)
+
+	return (hxRequest == "true" || hxBoosted == "true") && hxHistoryRestoreRequest != "true"
+}
diff --git a/connector/partial.go b/connector/partial.go
new file mode 100644
index 0000000..e9874a1
--- /dev/null
+++ b/connector/partial.go
@@ -0,0 +1,16 @@
+package connector
+
+type Partial struct {
+	base
+}
+
+func NewPartial(c *Config) Connector {
+	return &Partial{
+		base: base{
+			config:       c,
+			targetHeader: "X-Target",
+			selectHeader: "X-Select",
+			actionHeader: "X-Action",
+		},
+	}
+}
diff --git a/connector/stimulus.go b/connector/stimulus.go
new file mode 100644
index 0000000..3e9851d
--- /dev/null
+++ b/connector/stimulus.go
@@ -0,0 +1,22 @@
+package connector
+
+import "net/http"
+
+type Stimulus struct {
+	base
+}
+
+func NewStimulus(c *Config) Connector {
+	return &Stimulus{
+		base: base{
+			config:       c,
+			targetHeader: "X-Stimulus-Target",
+			selectHeader: "X-Stimulus-Select",
+			actionHeader: "X-Stimulus-Action",
+		},
+	}
+}
+
+func (s *Stimulus) RenderPartial(r *http.Request) bool {
+	return r.Header.Get(s.targetHeader) != ""
+}
diff --git a/connector/turbo.go b/connector/turbo.go
new file mode 100644
index 0000000..f17c999
--- /dev/null
+++ b/connector/turbo.go
@@ -0,0 +1,16 @@
+package connector
+
+type Turbo struct {
+	base
+}
+
+func NewTurbo(c *Config) Connector {
+	return &Turbo{
+		base: base{
+			config:       c,
+			targetHeader: "Turbo-Frame",
+			selectHeader: "Turbo-Select",
+			actionHeader: "Turbo-Action",
+		},
+	}
+}
diff --git a/connector/unpoly.go b/connector/unpoly.go
new file mode 100644
index 0000000..0d7e254
--- /dev/null
+++ b/connector/unpoly.go
@@ -0,0 +1,22 @@
+package connector
+
+import "net/http"
+
+type Unpoly struct {
+	base
+}
+
+func NewUnpoly(c *Config) Connector {
+	return &Unpoly{
+		base: base{
+			config:       c,
+			targetHeader: "X-Up-Target",
+			selectHeader: "X-Up-Select",
+			actionHeader: "X-Up-Action",
+		},
+	}
+}
+
+func (u *Unpoly) RenderPartial(r *http.Request) bool {
+	return r.Header.Get(u.targetHeader) != ""
+}
diff --git a/connector/vuejs.go b/connector/vuejs.go
new file mode 100644
index 0000000..fff0e78
--- /dev/null
+++ b/connector/vuejs.go
@@ -0,0 +1,22 @@
+package connector
+
+import "net/http"
+
+type Vue struct {
+	base
+}
+
+func NewVue(c *Config) Connector {
+	return &Vue{
+		base: base{
+			config:       c,
+			targetHeader: "X-Vue-Target",
+			selectHeader: "X-Vue-Select",
+			actionHeader: "X-Vue-Action",
+		},
+	}
+}
+
+func (v *Vue) RenderPartial(r *http.Request) bool {
+	return r.Header.Get(v.targetHeader) != ""
+}
diff --git a/examples/infinitescroll/main.go b/examples/infinitescroll/main.go
new file mode 100644
index 0000000..bf00513
--- /dev/null
+++ b/examples/infinitescroll/main.go
@@ -0,0 +1,117 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"log/slog"
+	"net/http"
+	"path/filepath"
+	"strconv"
+
+	"github.com/donseba/go-partial"
+	"github.com/donseba/go-partial/connector"
+)
+
+type (
+	App struct {
+		PartialService *partial.Service
+	}
+)
+
+func main() {
+	logger := slog.Default()
+
+	app := &App{
+		PartialService: partial.NewService(&partial.Config{
+			Logger: logger,
+			Connector: connector.NewPartial(&connector.Config{
+				UseURLQuery: true,
+			}),
+		}),
+	}
+
+	mux := http.NewServeMux()
+
+	mux.Handle("GET /js/", http.StripPrefix("/js/", http.FileServer(http.Dir("../../js"))))
+
+	mux.HandleFunc("GET /", app.home)
+
+	server := &http.Server{
+		Addr:    ":8080",
+		Handler: mux,
+	}
+
+	logger.Info("starting server on :8080")
+	err := server.ListenAndServe()
+	if err != nil {
+		logger.Error("error starting server", "error", err)
+	}
+}
+
+// super simple example of how to use the partial service to render a layout with a content partial
+func (a *App) home(w http.ResponseWriter, r *http.Request) {
+	// layout, footer, index could be abstracted away and shared over multiple handlers within the same module, for instance.
+	layout := a.PartialService.NewLayout()
+	footer := partial.NewID("footer", filepath.Join("templates", "footer.gohtml"))
+	index := partial.NewID("index", filepath.Join("templates", "index.gohtml")).WithOOB(footer)
+
+	content := partial.NewID("content", filepath.Join("templates", "content.gohtml")).WithAction(func(ctx context.Context, p *partial.Partial, data *partial.Data) (*partial.Partial, error) {
+		switch p.GetRequestedAction() {
+		case "infinite-scroll":
+			return handleInfiniteScroll(p, data)
+		default:
+			return p, nil
+		}
+	})
+
+	// set the layout content and wrap it with the main template
+	layout.Set(content).Wrap(index)
+
+	err := layout.WriteWithRequest(r.Context(), w, r)
+	if err != nil {
+		http.Error(w, fmt.Errorf("error rendering layout: %w", err).Error(), http.StatusInternalServerError)
+	}
+}
+
+type (
+	Row struct {
+		ID   int
+		Name string
+		Desc string
+	}
+)
+
+func handleInfiniteScroll(p *partial.Partial, data *partial.Data) (*partial.Partial, error) {
+	startID := 0
+	if p.GetRequest().URL.Query().Get("ID") != "" {
+		startID, _ = strconv.Atoi(p.GetRequest().URL.Query().Get("ID"))
+	}
+
+	if startID >= 100 {
+		p.SetResponseHeaders(map[string]string{
+			"X-Swap":            "innerHTML",
+			"X-Infinite-Scroll": "stop",
+		})
+		p = partial.NewID("rickrolled", filepath.Join("templates", "rickrolled.gohtml"))
+	} else {
+		data.Data["Rows"] = generateNextRows(startID, 10)
+	}
+
+	return p, nil
+}
+
+func generateNextRows(lastID int, count int) []Row {
+	var out []Row
+	start := lastID + 1
+	end := lastID + count
+
+	for i := start; i <= end; i++ {
+		out = append(out, Row{
+			ID:   i,
+			Name: fmt.Sprintf("Name %d", i),
+			Desc: fmt.Sprintf("Description %d", i),
+		})
+	}
+
+	return out
+}
diff --git a/examples/infinitescroll/templates/content.gohtml b/examples/infinitescroll/templates/content.gohtml
new file mode 100644
index 0000000..04cce16
--- /dev/null
+++ b/examples/infinitescroll/templates/content.gohtml
@@ -0,0 +1,18 @@
+{{ with .Data.rickrolled }}
+    {{ . }}
+{{ else }}
+    {{ range $k, $v := .Data.Rows }}
+        <div class="row" x-params='{"ID": {{ $v.ID }}}'>
+            <div class="col-sm">
+                {{ $v.ID }}
+            </div>
+            <div class="col-sm">
+                {{ $v.Name }}
+            </div>
+            <div class="col-sm">
+                {{ $v.Desc }}
+            </div>
+        </div>
+    {{ end}}
+{{ end }}
+
diff --git a/examples/infinitescroll/templates/footer.gohtml b/examples/infinitescroll/templates/footer.gohtml
new file mode 100644
index 0000000..2fdbef2
--- /dev/null
+++ b/examples/infinitescroll/templates/footer.gohtml
@@ -0,0 +1 @@
+<div id="footer" {{ if swapOOB }}x-swap-oob="outerHTML"{{end}} class="container">footer time : {{ formatDate now "15:04:05" }} - {{ if swapOOB }}swapped with OOB{{else}}rendered on load{{end}}</div>
\ No newline at end of file
diff --git a/examples/infinitescroll/templates/index.gohtml b/examples/infinitescroll/templates/index.gohtml
new file mode 100644
index 0000000..a692d67
--- /dev/null
+++ b/examples/infinitescroll/templates/index.gohtml
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <title>Infinite Scroll</title>
+        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css">
+		<script type="application/javascript" src="/js/partial.js"></script>
+    </head>
+
+    <body>
+
+
+        <div class="mt-3 container">
+            <div>(rendered on load at : {{ formatDate now "15:04:05" }})</div>
+        <div class="mt-3">What the handler looks like: </div>
+
+        <pre class="mt-3 p-1" style="background-color: gray"><small>func (a *App) home(w http.ResponseWriter, r *http.Request) {
+	// layout, footer, index could be abstracted away and shared over multiple handlers within the same module, for instance.
+	layout := a.PartialService.NewLayout()
+	footer := partial.NewID("footer", filepath.Join("templates", "footer.gohtml"))
+	index := partial.NewID("index", filepath.Join("templates", "index.gohtml")).WithOOB(footer)
+
+	content := partial.NewID("content", filepath.Join("templates", "content.gohtml")).WithAction(func(ctx context.Context, p *partial.Partial, data *partial.Data) (*partial.Partial, error) {
+		switch p.GetRequestedAction() {
+		case "infinite-scroll":
+			return handleInfiniteScroll(p, data)
+		default:
+			return p, nil
+		}
+	})
+
+	// set the layout content and wrap it with the main template
+	layout.Set(content).Wrap(index)
+
+	err := layout.WriteWithRequest(r.Context(), w, r)
+	if err != nil {
+		http.Error(w, fmt.Errorf("error rendering layout: %w", err).Error(), http.StatusInternalServerError)
+	}
+}</small></pre>
+        </div>
+
+		{{ child "footer" }}
+
+		<div class="mt-3 container" id="content" x-infinite-scroll="true" x-get="/" x-swap="beforeend">
+			{{ child "content" }}
+		</div>
+
+    <script>
+        // Initialize the handler with optional configuration
+        const partial = new Partial();
+    </script>
+    </body>
+</html>
\ No newline at end of file
diff --git a/examples/infinitescroll/templates/rickrolled.gohtml b/examples/infinitescroll/templates/rickrolled.gohtml
new file mode 100644
index 0000000..171c96f
--- /dev/null
+++ b/examples/infinitescroll/templates/rickrolled.gohtml
@@ -0,0 +1,5 @@
+<div>
+    <h1>Don’t scroll too far, there are consequences</h1>
+    <div>That's enough scrolling for you today.</div>
+    <iframe id="rick" width="560" height="315" src="https://www.youtube.com/embed/dQw4w9WgXcQ?autoplay=1" allow='autoplay'></iframe>
+</div>
\ No newline at end of file
diff --git a/examples/tabs-htmx/main.go b/examples/tabs-htmx/main.go
index 59e9dc4..cd4622a 100644
--- a/examples/tabs-htmx/main.go
+++ b/examples/tabs-htmx/main.go
@@ -2,9 +2,11 @@ package main
 
 import (
 	"fmt"
-	"github.com/donseba/go-partial"
 	"log/slog"
 	"net/http"
+
+	"github.com/donseba/go-partial"
+	"github.com/donseba/go-partial/connector"
 )
 
 type (
@@ -18,14 +20,14 @@ func main() {
 
 	app := &App{
 		PartialService: partial.NewService(&partial.Config{
-			PartialHeader: "HX-Target",
-			Logger:        logger,
+			Logger:    logger,
+			Connector: connector.NewHTMX(nil),
 		}),
 	}
 
 	mux := http.NewServeMux()
 
-	mux.Handle("GET /files/", http.StripPrefix("/files/", http.FileServer(http.Dir("./files"))))
+	mux.Handle("GET /js/", http.StripPrefix("/js/", http.FileServer(http.Dir("../../js"))))
 
 	mux.HandleFunc("GET /", app.home)
 
diff --git a/examples/tabs/content.gohtml b/examples/tabs/content.gohtml
index 56d298a..eaf3473 100644
--- a/examples/tabs/content.gohtml
+++ b/examples/tabs/content.gohtml
@@ -2,13 +2,13 @@
     <!-- Tab Navigation -->
     <ul class="nav nav-tabs" role="tablist">
         <li class="nav-item">
-            <span class="nav-link {{ ifRequestedSelect "active" "tab1" ""}}" style="cursor:pointer;" x-get="/" x-partial="content" x-select="tab1">Tab 1</span>
+            <span class="nav-link {{ ifRequestedSelect "active" "tab1" ""}}" style="cursor:pointer;" x-get="/" x-target="#content" x-select="tab1">Tab 1</span>
         </li>
         <li class="nav-item">
-            <span class="nav-link {{ ifRequestedSelect "active" "tab2"}}" style="cursor:pointer;" x-get="/" x-partial="content" x-select="tab2">Tab 2</span>
+            <span class="nav-link {{ ifRequestedSelect "active" "tab2"}}" style="cursor:pointer;" x-get="/" x-target="#content" x-select="tab2">Tab 2</span>
         </li>
         <li class="nav-item">
-            <span class="nav-link {{ ifRequestedSelect "active" "tab3"}}" style="cursor:pointer;" x-get="/" x-partial="content" x-select="tab3">Tab 3</span>
+            <span class="nav-link {{ ifRequestedSelect "active" "tab3"}}" style="cursor:pointer;" x-get="/" x-target="#content" x-select="tab3">Tab 3</span>
         </li>
     </ul>
 
diff --git a/examples/tabs/index.gohtml b/examples/tabs/index.gohtml
index e601f2c..18c3dbc 100644
--- a/examples/tabs/index.gohtml
+++ b/examples/tabs/index.gohtml
@@ -3,7 +3,7 @@
     <head>
         <title>Tab Example</title>
         <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css">
-        <script type="application/javascript" src="/js/standalone.js"></script>
+        <script type="application/javascript" src="/js/partial.js"></script>
     </head>
 
     <body>
diff --git a/examples/tabs/main.go b/examples/tabs/main.go
index 263019f..e830223 100644
--- a/examples/tabs/main.go
+++ b/examples/tabs/main.go
@@ -2,10 +2,12 @@ package main
 
 import (
 	"fmt"
-	"github.com/donseba/go-partial"
 	"log/slog"
 	"net/http"
 	"path/filepath"
+
+	"github.com/donseba/go-partial"
+	"github.com/donseba/go-partial/connector"
 )
 
 type (
@@ -20,6 +22,9 @@ func main() {
 	app := &App{
 		PartialService: partial.NewService(&partial.Config{
 			Logger: logger,
+			Connector: connector.NewPartial(&connector.Config{
+				UseURLQuery: true,
+			}),
 		}),
 	}
 
diff --git a/examples/tabs/templates/content.gohtml b/examples/tabs/templates/content.gohtml
index 617ccea..70cdc84 100644
--- a/examples/tabs/templates/content.gohtml
+++ b/examples/tabs/templates/content.gohtml
@@ -1,14 +1,14 @@
 <div class="container mt-5">
     <!-- Tab Navigation -->
-    <ul class="nav nav-tabs" role="tablist">
+    <ul class="nav nav-tabs" role="tablist" x-target="#content" x-swap="innerHTML" >
         <li class="nav-item">
-            <span class="nav-link {{ ifRequestedSelect "active" "tab1" ""}}" style="cursor:pointer;" x-get="/" x-target="#content" x-select="tab1">Tab 1</span>
+            <span class="nav-link {{ ifRequestedSelect "active" "tab1" ""}}" style="cursor:pointer;" x-get="/" x-select="tab1">Tab 1</span>
         </li>
         <li class="nav-item">
-            <span class="nav-link {{ ifRequestedSelect "active" "tab2"}}" style="cursor:pointer;" x-get="/" x-target="#content" x-select="tab2">Tab 2</span>
+            <span class="nav-link {{ ifRequestedSelect "active" "tab2"}}" style="cursor:pointer;" x-get="/" x-select="tab2" >Tab 2</span>
         </li>
         <li class="nav-item">
-            <span class="nav-link {{ ifRequestedSelect "active" "tab3"}}" style="cursor:pointer;" x-get="/" x-target="#content" x-select="tab3" x-debounce="1000">Tab 3 (debounce 1000ms)</span>
+            <span class="nav-link {{ ifRequestedSelect "active" "tab3"}}" style="cursor:pointer;" x-get="/" x-select="tab3" x-debounce="1000" x-loading-class="bg-warning" >Tab 3 (debounce 1000ms)</span>
         </li>
     </ul>
 
diff --git a/js/partial.js b/js/partial.js
index 7dab576..a2a0008 100644
--- a/js/partial.js
+++ b/js/partial.js
@@ -7,7 +7,7 @@
  * @property {Function|string} [csrfToken] - CSRF token value or function returning the token.
  * @property {Function} [beforeRequest] - Hook before the request is sent.
  * @property {Function} [afterResponse] - Hook after the response is received.
- * @property {boolean} [autoFocus=true] - Whether to auto-focus the target element after content update.
+ * @property {boolean} [autoFocus=false] - Whether to auto-focus the target element after content update.
  * @property {number} [debounceTime=0] - Debounce time in milliseconds for event handlers.
  */
 
@@ -36,33 +36,49 @@ class Partial {
                 POST:   'x-post',
                 PUT:    'x-put',
                 DELETE: 'x-delete',
+                PATCH:  'x-patch',
             },
-            TARGET:        'x-target',
-            TRIGGER:       'x-trigger',
-            SERIALIZE:     'x-serialize',
-            JSON:          'x-json',
-            PARAMS:        'x-params',
-            SWAP_OOB:      'x-swap-oob',
-            PUSH_STATE:    'x-push-state',
-            FOCUS:         'x-focus',
-            DEBOUNCE:      'x-debounce',
-            BEFORE:        'x-before',
-            AFTER:         'x-after',
-            SSE:           'x-sse',
-            INDICATOR:     'x-indicator',
-            CONFIRM:       'x-confirm',
-            TIMEOUT:       'x-timeout',
-            RETRY:         'x-retry',
-            ON_ERROR:      'x-on-error',
-            LOADING_CLASS: 'x-loading-class',
+            TARGET:          'x-target',
+            TRIGGER:         'x-trigger',
+            SERIALIZE:       'x-serialize',
+            JSON:            'x-json',
+            PARAMS:          'x-params',
+            SWAP_OOB:        'x-swap-oob',
+            PUSH_STATE:      'x-push-state',
+            FOCUS:           'x-focus',
+            DEBOUNCE:        'x-debounce',
+            BEFORE:          'x-before',
+            AFTER:           'x-after',
+            SSE:             'x-sse',
+            INDICATOR:       'x-indicator',
+            CONFIRM:         'x-confirm',
+            TIMEOUT:         'x-timeout',
+            RETRY:           'x-retry',
+            ON_ERROR:        'x-on-error',
+            LOADING_CLASS:   'x-loading-class',
+            SWAP:            'x-swap',
+            INFINITE_SCROLL: 'x-infinite-scroll',
         };
 
         this.SERIALIZE_TYPES = {
-            JSON: 'json',
+            JSON:        'json',
             NESTED_JSON: 'nested-json',
-            XML: 'xml',
+            XML:         'xml',
         };
 
+        this.INHERITABLE_ATTRIBUTES = [
+            this.ATTRIBUTES.TARGET,
+            this.ATTRIBUTES.SWAP,
+            this.ATTRIBUTES.SERIALIZE,
+            this.ATTRIBUTES.TRIGGER,
+            this.ATTRIBUTES.LOADING_CLASS,
+            this.ATTRIBUTES.INDICATOR,
+            this.ATTRIBUTES.RETRY,
+            this.ATTRIBUTES.TIMEOUT,
+            this.ATTRIBUTES.FOCUS,
+            this.ATTRIBUTES.DEBOUNCE,
+        ];
+
         // Store options with default values
         this.onError           = options.onError || null;
         this.csrfToken         = options.csrfToken || null;
@@ -79,12 +95,15 @@ class Partial {
         this.sseConnections = new Map();
 
         // Bind methods to ensure correct 'this' context
-        this.scanForElements   = this.scanForElements.bind(this);
-        this.setupElement      = this.setupElement.bind(this);
-        this.setupSSEElement   = this.setupSSEElement.bind(this);
-        this.handleAction      = this.handleAction.bind(this);
-        this.handleOobSwapping = this.handleOobSwapping.bind(this);
-        this.handlePopState    = this.handlePopState.bind(this);
+        this.scanForElements            = this.scanForElements.bind(this);
+        this.setupElement               = this.setupElement.bind(this);
+        this.setupSSEElement            = this.setupSSEElement.bind(this);
+        this.setupInfiniteScroll        = this.setupInfiniteScroll.bind(this);
+        this.stopInfiniteScroll         = this.stopInfiniteScroll.bind(this);
+        this.handleAction               = this.handleAction.bind(this);
+        this.handleOobSwapping          = this.handleOobSwapping.bind(this);
+        this.handlePopState             = this.handlePopState.bind(this);
+        this.handleInfiniteScrollAction = this.handleInfiniteScrollAction.bind(this);
 
         // Initialize the handler on DOMContentLoaded
         document.addEventListener('DOMContentLoaded', () => this.scanForElements());
@@ -93,6 +112,9 @@ class Partial {
         window.addEventListener('popstate', this.handlePopState);
     }
 
+    // Initialization Methods
+    // ----------------------
+
     /**
      * Scans the entire document or a specific container for elements with defined action attributes.
      * @param {HTMLElement | Document} [container=document]
@@ -112,6 +134,9 @@ class Partial {
         });
     }
 
+    // SSE Methods
+    // -----------
+
     /**
      * Sets up an element with x-sse attribute to handle SSE connections.
      * @param {HTMLElement} element
@@ -129,14 +154,13 @@ class Partial {
         const eventSource = new EventSource(sseUrl);
 
         eventSource.onmessage = (event) => {
-            this.handleSSEMessage(event, element);
+            this.handleSSEMessage(event, element).catch(error => {
+                this.handleError(error, element);
+            });
         };
 
         eventSource.onerror = (error) => {
-            console.error('SSE connection error on element:', element, error);
-            if (typeof this.onError === 'function') {
-                this.onError(error, element);
-            }
+            this.handleError(error, element);
         };
 
         // Store the connection to manage it later if needed
@@ -167,14 +191,7 @@ class Partial {
             // Decide swap method
             const swapOption = data.xSwap || this.defaultSwapOption;
 
-            if (swapOption === 'outerHTML') {
-                targetElement.outerHTML = data.content;
-            } else if (swapOption === 'innerHTML') {
-                targetElement.innerHTML = data.content;
-            } else {
-                console.error(`Invalid x-swap option '${swapOption}' in SSE message. Use 'outerHTML' or 'innerHTML'.`);
-                return;
-            }
+            this.performSwap(targetElement, data.content, swapOption);
 
             // Optionally focus the target element
             const focusEnabled = data.xFocus !== 'false';
@@ -200,13 +217,13 @@ class Partial {
             this.dispatchEvent('sseContentReplaced', { targetElement, data, element });
 
         } catch (error) {
-            console.error('Error processing SSE message:', error);
-            if (typeof this.onError === 'function') {
-                this.onError(error, element);
-            }
+            this.handleError(error, element);
         }
     }
 
+    // Element Setup Methods
+    // ---------------------
+
     /**
      * Sets up an individual element by attaching the appropriate event listener.
      * @param {HTMLElement} element
@@ -215,17 +232,25 @@ class Partial {
         // Avoid attaching multiple listeners
         if (element.__xRequestHandlerInitialized) return;
 
+        // Check for x-infinite-scroll attribute
+        if (element.hasAttribute(this.ATTRIBUTES.INFINITE_SCROLL)) {
+            this.setupInfiniteScroll(element);
+            // Mark the element as initialized
+            element.__xRequestHandlerInitialized = true;
+            return;
+        }
+
         // Set a default trigger based on the element type
         let trigger;
         if (element.tagName === 'FORM') {
             trigger = element.getAttribute(this.ATTRIBUTES.TRIGGER) || 'submit';
         } else {
-            trigger = element.getAttribute(this.ATTRIBUTES.TRIGGER) || 'click';
+            trigger = this.getAttributeWithInheritance(element, this.ATTRIBUTES.TRIGGER) || 'click';
         }
 
         // Get custom debounce time from x-debounce attribute
         let elementDebounceTime = this.debounceTime; // Default to global debounce time
-        const xDebounce = element.getAttribute(this.ATTRIBUTES.DEBOUNCE);
+        const xDebounce = this.getAttributeWithInheritance(element, this.ATTRIBUTES.DEBOUNCE);
         if (xDebounce !== null) {
             const parsedDebounce = parseInt(xDebounce, 10);
             if (!isNaN(parsedDebounce) && parsedDebounce >= 0) {
@@ -237,7 +262,9 @@ class Partial {
 
         // Debounce only the handleAction function
         const debouncedHandleAction = this.debounce((event) => {
-            this.handleAction(event, element);
+            this.handleAction(event, element).catch(error => {
+                this.handleError(error, element);
+            });
         }, elementDebounceTime);
 
         // Event handler that calls preventDefault immediately
@@ -252,79 +279,184 @@ class Partial {
         element.__xRequestHandlerInitialized = true;
     }
 
+    // Infinite Scroll Methods
+    // -----------------------
+
     /**
-     * Handles the action when an element is triggered.
-     * @param {Event} event
-     * @param {HTMLElement} element
+     * Sets up infinite scroll on an element.
+     * @param {HTMLElement} parentElement
      */
-    async handleAction(event, element) {
-        // Get confirmation message from x-confirm
-        const confirmMessage = element.getAttribute(this.ATTRIBUTES.CONFIRM);
-        if (confirmMessage) {
-            const confirmed = window.confirm(confirmMessage);
-            if (!confirmed) {
-                return; // Abort the action
-            }
+    setupInfiniteScroll(parentElement) {
+        // Check if infinite scroll has been stopped
+        if (parentElement.__infiniteScrollStopped) {
+            return;
         }
 
-        const requestParams = this.extractRequestParams(element);
-        requestParams.element = element;
+        // Create or find the sentinel element
+        let sentinel = parentElement.__sentinelElement;
+        if (!sentinel) {
+            sentinel = document.createElement('div');
+            sentinel.classList.add('infinite-scroll-sentinel');
+            parentElement.parentNode.insertBefore(sentinel, parentElement.nextSibling);
+            parentElement.__sentinelElement = sentinel;
+        }
 
-        if (!requestParams.url) {
-            const error = new Error(`No URL specified for method ${requestParams.method} on element.`);
-            console.error(error.message, element);
-            if (typeof this.onError === 'function') {
-                this.onError(error, element);
-            }
+        // Set up Intersection Observer on the sentinel
+        const observer = new IntersectionObserver((entries) => {
+            entries.forEach(entry => {
+                if (entry.isIntersecting) {
+                    // Unobserve to prevent multiple triggers
+                    observer.unobserve(sentinel);
+                    // Execute the action
+                    this.handleInfiniteScrollAction(parentElement).catch(error => {
+                        this.handleError(error, parentElement);
+                    });
+                }
+            });
+        });
+
+        observer.observe(sentinel);
+
+        // Store the observer reference
+        parentElement.__infiniteScrollObserver = observer;
+    }
+
+    /**
+     * Stops the infinite scroll by removing the sentinel and disconnecting the observer.
+     * @param {HTMLElement} parentElement
+     */
+    stopInfiniteScroll(parentElement) {
+        // Remove the sentinel element
+        if (parentElement.__sentinelElement) {
+            parentElement.__sentinelElement.remove();
+            delete parentElement.__sentinelElement;
+        }
+
+        // Set a flag to indicate infinite scroll has stopped
+        parentElement.__infiniteScrollStopped = true;
+
+        // Disconnect the observer
+        if (parentElement.__infiniteScrollObserver) {
+            parentElement.__infiniteScrollObserver.disconnect();
+            delete parentElement.__infiniteScrollObserver;
+        }
+    }
+
+    /**
+     * Handles the action for infinite scroll.
+     * @param {HTMLElement} parentElement
+     */
+    async handleInfiniteScrollAction(parentElement) {
+        const url = parentElement.getAttribute(this.ATTRIBUTES.ACTIONS.GET);
+        if (!url) {
+            console.error('No URL specified for infinite scroll.');
             return;
         }
 
-        const targetElement = document.querySelector(requestParams.targetSelector);
-        if (!targetElement) {
-            const error = new Error(`No element found with selector '${requestParams.targetSelector}' for 'x-target' targeting.`);
-            console.error(error.message);
-            if (typeof this.onError === 'function') {
-                this.onError(error, element);
+        const requestParams = this.prepareRequestParams(parentElement, { maxRetries: 2 });
+
+        // Set X-Action header if not already set
+        if (!requestParams.headers["X-Action"]) {
+            requestParams.headers["X-Action"] = "infinite-scroll";
+        }
+
+        // Get the params from the last child
+        requestParams.paramsObject = this.getChildParamsObject(parentElement);
+        if (requestParams.paramsObject && Object.keys(requestParams.paramsObject).length > 0) {
+            requestParams.headers["X-Params"] = JSON.stringify(requestParams.paramsObject);
+        }
+
+        try {
+            const responseText = await this.performRequest(requestParams);
+            const targetElement = document.querySelector(requestParams.targetSelector);
+            if (!targetElement) {
+                console.error(`No element found with selector '${requestParams.targetSelector}' for infinite scroll.`);
+                return;
             }
-            return;
+
+            await this.processResponse(responseText, targetElement, parentElement);
+
+            // Re-attach the observer to continue loading
+            this.setupInfiniteScroll(parentElement);
+        } catch (error) {
+            this.handleError(error, parentElement, parentElement);
         }
+    }
 
-        if (!requestParams.partialId) {
-            const error = new Error(`Target element does not have an 'id' attribute.`);
-            console.error(error.message, targetElement);
-            if (typeof this.onError === 'function') {
-                this.onError(error, element);
+    /**
+     * Retrieves parameters from the last child element.
+     * @param {HTMLElement} parentElement
+     * @returns {Object}
+     */
+    getChildParamsObject(parentElement) {
+        // Get x-params from the last child
+        const lastChild = parentElement.lastElementChild;
+        let paramsObject = {};
+        if (lastChild) {
+            const xParamsAttr = lastChild.getAttribute(this.ATTRIBUTES.PARAMS);
+            if (xParamsAttr) {
+                try {
+                    paramsObject = JSON.parse(xParamsAttr);
+                } catch (e) {
+                    console.error('Invalid JSON in x-params attribute of last child:', e);
+                }
             }
-            return;
         }
 
-        // Set the X-Target header to the request
-        requestParams.headers["X-Target"] = requestParams.partialId;
+        return paramsObject;
+    }
+
+    // Action Handling Methods
+    // -----------------------
+
+    /**
+     * Handles the action when an element is triggered.
+     * @param {Event} event
+     * @param {HTMLElement} element
+     */
+    async handleAction(event, element) {
+        // Get a confirmation message from x-confirm
+        const confirmMessage = element.getAttribute(this.ATTRIBUTES.CONFIRM);
+        if (confirmMessage) {
+            const confirmed = window.confirm(confirmMessage);
+            if (!confirmed) {
+                return; // Abort the action
+            }
+        }
 
         // Get the indicator selector from x-indicator
-        const indicatorSelector = element.getAttribute(this.ATTRIBUTES.INDICATOR);
+        const indicatorSelector = this.getAttributeWithInheritance(element, this.ATTRIBUTES.INDICATOR);
         let indicatorElement = null;
         if (indicatorSelector) {
             indicatorElement = document.querySelector(indicatorSelector);
         }
 
         // Get loading class from x-loading-class
-        const loadingClass = element.getAttribute(this.ATTRIBUTES.LOADING_CLASS);
+        const loadingClass = this.getAttributeWithInheritance(element, this.ATTRIBUTES.LOADING_CLASS);
 
         // Handle x-focus
-        const focusEnabled = element.getAttribute(this.ATTRIBUTES.FOCUS) !== 'false';
+        const focusEnabled = this.getAttributeWithInheritance(element, this.ATTRIBUTES.FOCUS) !== 'false';
 
         // Handle x-push-state
-        const shouldPushState = element.getAttribute(this.ATTRIBUTES.PUSH_STATE) !== 'false';
+        const shouldPushState = this.getAttributeWithInheritance(element, this.ATTRIBUTES.PUSH_STATE) !== 'false';
 
         // Handle x-timeout
-        const timeoutValue = element.getAttribute(this.ATTRIBUTES.TIMEOUT);
+        const timeoutValue = this.getAttributeWithInheritance(element, this.ATTRIBUTES.TIMEOUT);
         const timeout = parseInt(timeoutValue, 10);
 
         // Handle x-retry
-        const retryValue = element.getAttribute(this.ATTRIBUTES.RETRY);
+        const retryValue = this.getAttributeWithInheritance(element, this.ATTRIBUTES.RETRY);
         const maxRetries = parseInt(retryValue, 10) || 0;
 
+        const requestParams = this.prepareRequestParams(element);
+
+        const targetElement = document.querySelector(requestParams.targetSelector);
+        if (!targetElement) {
+            const error = new Error(`No element found with selector '${requestParams.targetSelector}' for 'x-target' targeting.`);
+            this.handleError(error, element, targetElement);
+            return;
+        }
+
         try {
             // Show the indicator before the request
             if (indicatorElement) {
@@ -412,6 +544,41 @@ class Partial {
         }
     }
 
+    // Request Preparation Methods
+    // ---------------------------
+
+    /**
+     * Prepares the request parameters for the Fetch API.
+     * @param {HTMLElement} element
+     * @param {Object} [additionalParams={}]
+     * @returns {Object} Request parameters
+     */
+    prepareRequestParams(element, additionalParams = {}) {
+        const requestParams = this.extractRequestParams(element);
+        requestParams.element = element;
+
+        if (!requestParams.url) {
+            throw new Error(`No URL specified for method ${requestParams.method} on element.`);
+        }
+
+        const targetElement = document.querySelector(requestParams.targetSelector);
+        if (!targetElement) {
+            throw new Error(`No element found with selector '${requestParams.targetSelector}' for 'x-target' targeting.`);
+        }
+
+        if (!requestParams.partialId) {
+            throw new Error(`Target element does not have an 'id' attribute.`);
+        }
+
+        // Set the X-Target header
+        requestParams.headers["X-Target"] = requestParams.partialId;
+
+        // Merge additional parameters
+        Object.assign(requestParams, additionalParams);
+
+        return requestParams;
+    }
+
     /**
      * Extracts request parameters from the element.
      * @param {HTMLElement} element
@@ -419,30 +586,28 @@ class Partial {
      */
     extractRequestParams(element) {
         const method = this.getMethod(element);
-        let url = element.getAttribute(`x-${method.toLowerCase()}`);
+        const actionAttr = `x-${method.toLowerCase()}`;
+        let url = this.getAttributeWithInheritance(element, actionAttr);
 
         const headers = this.getHeaders(element);
 
-        let targetSelector = element.getAttribute(this.ATTRIBUTES.TARGET);
+        let targetSelector = this.getAttributeWithInheritance(element, this.ATTRIBUTES.TARGET);
         if (!targetSelector) {
-            targetSelector = 'body';
+            targetSelector = element.id ? `#${element.id}` : "body";
         }
 
         const targetElement = document.querySelector(targetSelector);
         const partialId = targetElement ? targetElement.getAttribute('id') : null;
 
-        const xParams = element.getAttribute(this.ATTRIBUTES.PARAMS);
+        const xParams = this.getAttributeWithInheritance(element, this.ATTRIBUTES.PARAMS);
         let paramsObject = {};
 
         if (xParams) {
             try {
                 paramsObject = JSON.parse(xParams);
             } catch (e) {
-                console.error('Invalid JSON in x-params attribute:', e);
                 const error = new Error('Invalid JSON in x-params attribute');
-                if (typeof this.onError === 'function') {
-                    this.onError(error, element);
-                }
+                this.handleError(error, element, targetElement);
             }
         }
 
@@ -456,7 +621,7 @@ class Partial {
      */
     getMethod(element) {
         for (const attr of Object.values(this.ATTRIBUTES.ACTIONS)) {
-            if (element.hasAttribute(attr)) {
+            if (this.hasAttributeWithInheritance(element, attr)) {
                 return attr.replace('x-', '').toUpperCase();
             }
         }
@@ -479,10 +644,22 @@ class Partial {
             }
         }
 
-        // Collect all x-* attributes that are not actionAttributes
+        // List of attributes to exclude from headers
+        const excludedAttributes = [
+            ...Object.values(this.ATTRIBUTES.ACTIONS),
+            this.ATTRIBUTES.TARGET,
+            this.ATTRIBUTES.TRIGGER,
+            this.ATTRIBUTES.SWAP,
+            this.ATTRIBUTES.SWAP_OOB,
+            this.ATTRIBUTES.PUSH_STATE,
+            this.ATTRIBUTES.INFINITE_SCROLL,
+            this.ATTRIBUTES.DEBOUNCE
+        ];
+
+        // Collect x-* attributes to include as headers
         for (const attr of element.attributes) {
             const name = attr.name;
-            if (name.startsWith('x-') && !Object.values(this.ATTRIBUTES.ACTIONS).includes(name)) {
+            if (name.startsWith('x-') && !excludedAttributes.includes(name)) {
                 const headerName = 'X-' + this.capitalize(name.substring(2)); // Remove 'x-' prefix and capitalize
                 headers[headerName] = attr.value;
             }
@@ -491,6 +668,40 @@ class Partial {
         return headers;
     }
 
+    // Utility Methods
+    // ---------------
+
+    /**
+     * Retrieves the value of an attribute from the element or its ancestors.
+     * @param {HTMLElement} element
+     * @param {string} attributeName
+     * @returns {string|null}
+     */
+    getAttributeWithInheritance(element, attributeName) {
+        if (!this.INHERITABLE_ATTRIBUTES.includes(attributeName)) {
+            return element.getAttribute(attributeName);
+        }
+
+        let currentElement = element;
+        while (currentElement) {
+            if (currentElement.hasAttribute(attributeName)) {
+                return currentElement.getAttribute(attributeName);
+            }
+            currentElement = currentElement.parentElement;
+        }
+        return null;
+    }
+
+    /**
+     * Checks if an attribute exists on the element or its ancestors.
+     * @param {HTMLElement} element
+     * @param {string} attributeName
+     * @returns {boolean}
+     */
+    hasAttributeWithInheritance(element, attributeName) {
+        return this.getAttributeWithInheritance(element, attributeName) !== null;
+    }
+
     /**
      * Capitalizes the first letter of the string.
      * @param {string} str
@@ -500,6 +711,27 @@ class Partial {
         return str.charAt(0).toUpperCase() + str.slice(1);
     }
 
+    /**
+     * Debounce function to limit the rate at which a function can fire.
+     * @param {Function} func - The function to debounce.
+     * @param {number} wait - The number of milliseconds to wait.
+     * @returns {Function}
+     */
+    debounce(func, wait) {
+        let timeout;
+        return (...args) => {
+            const later = () => {
+                clearTimeout(timeout);
+                func.apply(this, args);
+            };
+            clearTimeout(timeout);
+            timeout = setTimeout(later, wait);
+        };
+    }
+
+    // Request Execution Methods
+    // -------------------------
+
     /**
      * Performs the HTTP request using Fetch API.
      * @param {Object} requestParams - Parameters including method, url, headers, body, etc.
@@ -542,13 +774,13 @@ class Partial {
                 const form = element.tagName === 'FORM' ? element : element.closest('form');
                 if (serializeType === this.SERIALIZE_TYPES.JSON) {
                     // Serialize form data as flat JSON
-                    bodyData = JSON.parse(this.serializeFormToJson(form));
+                    bodyData = JSON.parse(Serializer.serializeFormToJson(form));
                 } else if (serializeType === this.SERIALIZE_TYPES.NESTED_JSON) {
                     // Serialize form data as nested JSON
-                    bodyData = JSON.parse(this.serializeFormToNestedJson(form));
+                    bodyData = JSON.parse(Serializer.serializeFormToNestedJson(form));
                 } else if (serializeType === this.SERIALIZE_TYPES.XML) {
                     // Serialize form data as XML
-                    bodyData = this.serializeFormToXml(form);
+                    bodyData = Serializer.serializeFormToXml(form);
                     headers['Content-Type'] = 'application/xml';
                 } else {
                     // Use FormData
@@ -557,7 +789,6 @@ class Partial {
             }
 
             // Merge paramsObject with bodyData
-            console.log(paramsObject)
             if (paramsObject && Object.keys(paramsObject).length > 0) {
                 if (bodyData instanceof FormData) {
                     // Append params to FormData
@@ -605,7 +836,9 @@ class Partial {
             attempts++;
             try {
                 const response = await fetch(requestUrl, options);
-                clearTimeout(timeoutId);
+                if (timeoutId) {
+                    clearTimeout(timeoutId);
+                }
 
                 this.lastResponse = response;
 
@@ -615,6 +848,10 @@ class Partial {
                 }
                 return response.text();
             } catch (error) {
+                if (timeoutId) {
+                    clearTimeout(timeoutId);
+                }
+
                 if (error.name === 'AbortError') {
                     throw new Error('Request timed out');
                 }
@@ -622,132 +859,13 @@ class Partial {
                 if (attempts >= maxAttempts) {
                     throw error;
                 }
-                // Optionally, implement a delay before retrying
+                // TODO, implement a delay before retrying
             }
         }
     }
 
-    /**
-     * Serializes form data to a flat JSON string.
-     * @param {HTMLFormElement} form
-     * @returns {string} JSON string
-     */
-    serializeFormToJson(form) {
-        const formData = new FormData(form);
-        const jsonObject = {};
-        formData.forEach((value, key) => {
-            if (jsonObject[key]) {
-                if (Array.isArray(jsonObject[key])) {
-                    jsonObject[key].push(value);
-                } else {
-                    jsonObject[key] = [jsonObject[key], value];
-                }
-            } else {
-                jsonObject[key] = value;
-            }
-        });
-        return JSON.stringify(jsonObject);
-    }
-
-    /**
-     * Serializes form data to a nested JSON string.
-     * @param {HTMLFormElement} form
-     * @returns {string} Nested JSON string
-     */
-    serializeFormToNestedJson(form) {
-        const formData = new FormData(form);
-        const serializedData = {};
-
-        for (let [name, value] of formData) {
-            const inputElement = form.querySelector(`[name="${name}"]`);
-            const checkBoxCustom = form.querySelector(`[data-custom="true"]`);
-            const inputType = inputElement ? inputElement.type : null;
-            const inputStep = inputElement ? inputElement.step : null;
-
-            // Check if the input type is number and convert the value if so
-            if (inputType === 'number') {
-                if (inputStep && inputStep !== "any" && Number(inputStep) % 1 === 0) {
-                    value = parseInt(value, 10);
-                } else if (inputStep === "any") {
-                    value = value.includes('.') ? parseFloat(value) : parseInt(value, 10);
-                } else {
-                    value = parseFloat(value);
-                }
-            }
-
-            // Check if the input type is checkbox and convert the value to boolean
-            if (inputType === 'checkbox' && !checkBoxCustom) {
-                value = inputElement.checked; // value will be true if checked, false otherwise
-            }
-
-            // Check if the input type is select-one and has data-bool attribute
-            if (inputType === 'select-one' && inputElement.getAttribute('data-bool') === 'true') {
-                value = value === "true"; // Value will be true if selected, false otherwise
-            }
-
-            // Attempt to parse JSON strings
-            try {
-                value = JSON.parse(value);
-            } catch (e) {
-                // If parsing fails, treat as a simple string
-            }
-
-            const keys = name.split(/[.[\]]+/).filter(Boolean); // split by dot or bracket notation
-            let obj = serializedData;
-
-            for (let i = 0; i < keys.length - 1; i++) {
-                if (!obj[keys[i]]) {
-                    obj[keys[i]] = /^\d+$/.test(keys[i + 1]) ? [] : {}; // create an array if the next key is an index
-                }
-                obj = obj[keys[i]];
-            }
-
-            const lastKey = keys[keys.length - 1];
-            if (lastKey in obj && Array.isArray(obj[lastKey])) {
-                obj[lastKey].push(value); // add to array if the key already exists
-            } else if (lastKey in obj) {
-                obj[lastKey] = [obj[lastKey], value];
-            } else {
-                obj[lastKey] = value; // set value for key
-            }
-        }
-
-        return JSON.stringify(serializedData);
-    }
-
-    /**
-     * Serializes form data to an XML string.
-     * @param {HTMLFormElement} form
-     * @returns {string} XML string
-     */
-    serializeFormToXml(form) {
-        const formData = new FormData(form);
-        let xmlString = '<?xml version="1.0" encoding="UTF-8"?><form>';
-
-        formData.forEach((value, key) => {
-            xmlString += `<${key}>${this.escapeXml(value)}</${key}>`;
-        });
-
-        xmlString += '</form>';
-        return xmlString;
-    }
-
-    /**
-     * Escapes XML special characters.
-     * @param {string} unsafe
-     * @returns {string}
-     */
-    escapeXml(unsafe) {
-        return unsafe.replace(/[<>&'"]/g, function (c) {
-            switch (c) {
-                case '<': return '&lt;';
-                case '>': return '&gt;';
-                case '&': return '&amp;';
-                case '\'': return '&apos;';
-                case '"': return '&quot;';
-            }
-        });
-    }
+    // Response Processing Methods
+    // ---------------------------
 
     /**
      * Processes the response text and updates the DOM accordingly.
@@ -767,29 +885,50 @@ class Partial {
         const oobElements = Array.from(doc.querySelectorAll(`[${this.ATTRIBUTES.SWAP_OOB}]`));
         oobElements.forEach(el => el.parentNode.removeChild(el));
 
+        // Handle backend instructions
+        const backendTargetSelector = this.lastResponse.headers.get('X-Target');
+        const backendSwapOption = this.lastResponse.headers.get('X-Swap');
+        const infiniteScrollAction = this.lastResponse.headers.get('X-Infinite-Scroll');
+
+        // Determine the target element
+        let finalTargetElement = targetElement;
+        if (backendTargetSelector) {
+            const backendTargetElement = document.querySelector(backendTargetSelector);
+            if (backendTargetElement) {
+                finalTargetElement = backendTargetElement;
+            } else {
+                console.error(`No element found with selector '${backendTargetSelector}' specified in X-Target header.`);
+            }
+        }
+
+        // Determine the swap option
+        let swapOption = this.getAttributeWithInheritance(element, this.ATTRIBUTES.SWAP) || this.defaultSwapOption;
+        if (backendSwapOption) {
+            swapOption = backendSwapOption;
+        }
+
+        // Get the content from the response
+        const newContent = doc.body.innerHTML;
+
         // Replace the target's content
-        this.updateTargetElement(targetElement, doc);
+        this.performSwap(finalTargetElement, newContent, swapOption);
 
         // Dispatch afterUpdate event
-        this.dispatchEvent('afterUpdate', { targetElement, element });
+        this.dispatchEvent('afterUpdate', { targetElement: finalTargetElement, element });
 
         // Re-scan the newly added content for Partial elements
-        this.scanForElements(targetElement);
+        this.scanForElements(finalTargetElement);
 
         // Handle OOB swapping with the extracted OOB elements
         this.handleOobSwapping(oobElements);
 
         // Handle any x-event-* headers from the response
         await this.handleResponseEvents();
-    }
 
-    /**
-     * Updates the target element with new content.
-     * @param {HTMLElement} targetElement
-     * @param {Document} doc
-     */
-    updateTargetElement(targetElement, doc) {
-        targetElement.innerHTML = doc.body.innerHTML;
+        // Stop infinite scroll if instructed by backend
+        if (infiniteScrollAction === 'stop' && element.hasAttribute(this.ATTRIBUTES.INFINITE_SCROLL)) {
+            this.stopInfiniteScroll(element);
+        }
     }
 
     /**
@@ -813,14 +952,9 @@ class Partial {
                 return;
             }
 
-            if (swapOption === 'outerHTML' || swapOption === true) {
-                existingElement.outerHTML = oobElement.outerHTML;
-            } else if (swapOption === 'innerHTML') {
-                existingElement.innerHTML = oobElement.innerHTML;
-            } else {
-                console.error(`Invalid x-swap-oob option '${swapOption}' on element with ID '${targetId}'. Use 'outerHTML' or 'innerHTML'.`);
-                return;
-            }
+            const newContent = oobElement.outerHTML;
+
+            this.performSwap(existingElement, newContent, swapOption);
 
             // After swapping, initialize any new elements within the replaced content
             const newElement = document.getElementById(targetId);
@@ -830,6 +964,33 @@ class Partial {
         });
     }
 
+    /**
+     * Performs the swap operation on the target element based on the swap option.
+     * @param {HTMLElement} targetElement
+     * @param {string} newContent
+     * @param {string} swapOption
+     */
+    performSwap(targetElement, newContent, swapOption) {
+        switch (swapOption) {
+            case 'innerHTML':
+                targetElement.innerHTML = newContent;
+                break;
+            case 'outerHTML':
+                targetElement.outerHTML = newContent;
+                break;
+            case 'beforebegin':
+            case 'afterbegin':
+            case 'beforeend':
+            case 'afterend':
+                targetElement.insertAdjacentHTML(swapOption, newContent);
+                break;
+            default:
+                console.error(`Invalid swap option '${swapOption}'. Using 'innerHTML' as default.`);
+                targetElement.innerHTML = newContent;
+                break;
+        }
+    }
+
     /**
      * Handles any x-event-* headers from the response and dispatches events accordingly.
      */
@@ -839,7 +1000,8 @@ class Partial {
         }
 
         this.lastResponse.headers.forEach((value, name) => {
-            if (name.toLowerCase().startsWith('x-event-')) {
+            const lowerName = name.toLowerCase();
+            if (lowerName.startsWith('x-event-')) {
                 const eventName = name.substring(8); // Remove 'x-event-' prefix
                 let eventData = value;
                 try {
@@ -852,6 +1014,9 @@ class Partial {
         });
     }
 
+    // Event Handling Methods
+    // ----------------------
+
     /**
      * Dispatches custom events specified in a comma-separated string.
      * @param {string} events - Comma-separated event names.
@@ -891,32 +1056,11 @@ class Partial {
                 }
 
             } catch (error) {
-                console.error('PopState request failed:', error);
-                if (typeof this.onError === 'function') {
-                    this.onError(error, document.body);
-                }
+                this.handleError(error, document.body);
             }
         }
     }
 
-    /**
-     * Debounce function to limit the rate at which a function can fire.
-     * @param {Function} func - The function to debounce.
-     * @param {number} wait - The number of milliseconds to wait.
-     * @returns {Function}
-     */
-    debounce(func, wait) {
-        let timeout;
-        return (...args) => {
-            const later = () => {
-                clearTimeout(timeout);
-                func.apply(this, args);
-            };
-            clearTimeout(timeout);
-            timeout = setTimeout(later, wait);
-        };
-    }
-
     /**
      * Listens for a custom event and executes the callback when the event is dispatched.
      * @param {string} eventName - The name of the event to listen for
@@ -979,6 +1123,9 @@ class Partial {
         this.eventTarget.dispatchEvent(event);
     }
 
+    // Cleanup Methods
+    // ---------------
+
     /**
      * Allows manually re-scanning a specific container for Partial elements.
      * Useful when dynamically adding content to the DOM.
@@ -1000,4 +1147,149 @@ class Partial {
             element.__xSSEInitialized = false;
         }
     }
-}
\ No newline at end of file
+
+    // Error Handling Methods
+    // ----------------------
+
+    /**
+     * Handles errors by calling the provided error callback or logging to the console.
+     * @param {Error} error
+     * @param {HTMLElement} element
+     * @param {HTMLElement} [targetElement]
+     */
+    handleError(error, element, targetElement = null) {
+        if (typeof this.onError === 'function') {
+            this.onError(error, element);
+        } else {
+            console.error('Error:', error);
+            if (targetElement) {
+                targetElement.innerHTML = `<div class="error">An error occurred: ${error.message}</div>`;
+            }
+        }
+    }
+}
+
+class Serializer {
+    /**
+     * Serializes form data to a flat JSON string.
+     * @param {HTMLFormElement} form
+     * @returns {string} JSON string
+     */
+    static serializeFormToJson(form) {
+        const formData = new FormData(form);
+        const jsonObject = {};
+        formData.forEach((value, key) => {
+            if (jsonObject[key]) {
+                if (Array.isArray(jsonObject[key])) {
+                    jsonObject[key].push(value);
+                } else {
+                    jsonObject[key] = [jsonObject[key], value];
+                }
+            } else {
+                jsonObject[key] = value;
+            }
+        });
+        return JSON.stringify(jsonObject);
+    }
+
+    /**
+     * Serializes form data to a nested JSON string.
+     * @param {HTMLFormElement} form
+     * @returns {string} Nested JSON string
+     */
+    serializeFormToNestedJson(form) {
+        const formData = new FormData(form);
+        const serializedData = {};
+
+        for (let [name, value] of formData) {
+            const inputElement = form.querySelector(`[name="${name}"]`);
+            const checkBoxCustom = form.querySelector(`[data-custom="true"]`);
+            const inputType = inputElement ? inputElement.type : null;
+            const inputStep = inputElement ? inputElement.step : null;
+
+            // Check if the input type is number and convert the value if so
+            if (inputType === 'number') {
+                if (inputStep && inputStep !== "any" && Number(inputStep) % 1 === 0) {
+                    value = parseInt(value, 10);
+                } else if (inputStep === "any") {
+                    value = value.includes('.') ? parseFloat(value) : parseInt(value, 10);
+                } else {
+                    value = parseFloat(value);
+                }
+            }
+
+            // Check if the input type is checkbox and convert the value to boolean
+            if (inputType === 'checkbox' && !checkBoxCustom) {
+                value = inputElement.checked; // value will be true if checked, false otherwise
+            }
+
+            // Check if the input type is select-one and has data-bool attribute
+            if (inputType === 'select-one' && inputElement.getAttribute('data-bool') === 'true') {
+                value = value === "true"; // Value will be true if selected, false otherwise
+            }
+
+            // Attempt to parse JSON strings
+            try {
+                value = JSON.parse(value);
+            } catch (e) {
+                // If parsing fails, treat as a simple string
+            }
+
+            const keys = name.split(/[.[\]]+/).filter(Boolean); // split by dot or bracket notation
+            let obj = serializedData;
+
+            for (let i = 0; i < keys.length - 1; i++) {
+                if (!obj[keys[i]]) {
+                    obj[keys[i]] = /^\d+$/.test(keys[i + 1]) ? [] : {}; // create an array if the next key is an index
+                }
+                obj = obj[keys[i]];
+            }
+
+            const lastKey = keys[keys.length - 1];
+            if (lastKey in obj && Array.isArray(obj[lastKey])) {
+                obj[lastKey].push(value); // add to array if the key already exists
+            } else if (lastKey in obj) {
+                obj[lastKey] = [obj[lastKey], value];
+            } else {
+                obj[lastKey] = value; // set value for key
+            }
+        }
+
+        return JSON.stringify(serializedData);
+    }
+
+    /**
+     * Serializes form data to an XML string.
+     * @param {HTMLFormElement} form
+     * @returns {string} XML string
+     */
+    static serializeFormToXml(form) {
+        const formData = new FormData(form);
+        let xmlString = '<?xml version="1.0" encoding="UTF-8"?><form>';
+
+        formData.forEach((value, key) => {
+            xmlString += `<${key}>${this.escapeXml(value)}</${key}>`;
+        });
+
+        xmlString += '</form>';
+        return xmlString;
+    }
+
+    /**
+     * Escapes XML special characters.
+     * @param {string} unsafe
+     * @returns {string}
+     */
+    static escapeXml(unsafe) {
+        return unsafe.replace(/[<>&'"]/g, function (c) {
+            switch (c) {
+                case '<': return '&lt;';
+                case '>': return '&gt;';
+                case '&': return '&amp;';
+                case '\'': return '&apos;';
+                case '"': return '&quot;';
+                default: return c;
+            }
+        });
+    }
+}
diff --git a/partial.go b/partial.go
index 77f2054..0607a55 100644
--- a/partial.go
+++ b/partial.go
@@ -10,10 +10,13 @@ import (
 	"log/slog"
 	"net/http"
 	"net/url"
+	"os"
 	"path"
 	"reflect"
 	"strings"
 	"sync"
+
+	"github.com/donseba/go-partial/connector"
 )
 
 var (
@@ -51,18 +54,14 @@ type (
 		swapOOB           bool
 		fs                fs.FS
 		logger            Logger
-		partialHeader     string
-		selectHeader      string
-		actionHeader      string
-		requestedPartial  string
-		requestedAction   string
-		requestedSelect   string
+		connector         connector.Connector
 		useCache          bool
 		templates         []string
 		combinedFunctions template.FuncMap
 		data              map[string]any
 		layoutData        map[string]any
 		globalData        map[string]any
+		responseHeaders   map[string]string
 		mu                sync.RWMutex
 		children          map[string]*Partial
 		oobChildren       map[string]struct{}
@@ -107,6 +106,7 @@ func New(templates ...string) *Partial {
 		globalData:        make(map[string]any),
 		children:          make(map[string]*Partial),
 		oobChildren:       make(map[string]struct{}),
+		fs:                os.DirFS("./"),
 	}
 }
 
@@ -150,6 +150,29 @@ func (p *Partial) AddData(key string, value any) *Partial {
 	return p
 }
 
+func (p *Partial) SetResponseHeaders(headers map[string]string) *Partial {
+	p.responseHeaders = headers
+	return p
+}
+
+func (p *Partial) GetResponseHeaders() map[string]string {
+	if p == nil {
+		return nil
+	}
+
+	if p.responseHeaders == nil {
+		return p.parent.GetResponseHeaders()
+	}
+
+	return p.responseHeaders
+}
+
+// SetConnector sets the connector for the partial.
+func (p *Partial) SetConnector(connector connector.Connector) *Partial {
+	p.connector = connector
+	return p
+}
+
 // MergeData merges the data into the partial.
 func (p *Partial) MergeData(data map[string]any, override bool) *Partial {
 	for k, v := range data {
@@ -286,11 +309,15 @@ func (p *Partial) RenderWithRequest(ctx context.Context, r *http.Request) (templ
 	}
 
 	p.request = r
-	p.requestedPartial = r.Header.Get(p.getPartialHeader())
-	p.requestedAction = r.Header.Get(p.getActionHeader())
-	p.requestedSelect = r.Header.Get(p.getSelectHeader())
+	if p.connector == nil {
+		p.connector = connector.NewPartial(nil)
+	}
+
+	if p.connector.RenderPartial(r) {
+		return p.renderWithTarget(ctx, r)
+	}
 
-	return p.renderWithTarget(ctx, r)
+	return p.renderSelf(ctx, r)
 }
 
 // WriteWithRequest writes the partial to the http.ResponseWriter.
@@ -306,6 +333,12 @@ func (p *Partial) WriteWithRequest(ctx context.Context, w http.ResponseWriter, r
 		return err
 	}
 
+	// get headers
+	headers := p.GetResponseHeaders()
+	for k, v := range headers {
+		w.Header().Set(k, v)
+	}
+
 	_, err = w.Write([]byte(out))
 	if err != nil {
 		p.getLogger().Error("error writing partial to response", "error", err)
@@ -367,16 +400,17 @@ func (p *Partial) getFuncs(data *Data) template.FuncMap {
 	}
 
 	funcs["partialHeader"] = func() string {
-		return p.getPartialHeader()
+		return p.getConnector().GetTargetHeader()
 	}
 
 	funcs["requestedPartial"] = func() string {
-		return p.getRequestedPartial()
+		return p.getConnector().GetTargetValue(p.GetRequest())
 	}
 
 	funcs["ifRequestedPartial"] = func(out any, in ...string) any {
+		target := p.getConnector().GetTargetValue(p.GetRequest())
 		for _, v := range in {
-			if v == p.getRequestedPartial() {
+			if v == target {
 				return out
 			}
 		}
@@ -384,19 +418,22 @@ func (p *Partial) getFuncs(data *Data) template.FuncMap {
 	}
 
 	funcs["selectHeader"] = func() string {
-		return p.getSelectHeader()
+		return p.getConnector().GetSelectHeader()
 	}
 
 	funcs["requestedSelect"] = func() string {
-		if p.getRequestedSelect() == "" {
+		requestedSelect := p.getConnector().GetSelectValue(p.GetRequest())
+
+		if requestedSelect == "" {
 			return p.selection.Default
 		}
-		return p.getRequestedSelect()
+		return requestedSelect
 	}
 
 	funcs["ifRequestedSelect"] = func(out any, in ...string) any {
+		selected := p.getConnector().GetSelectValue(p.GetRequest())
 		for _, v := range in {
-			if v == p.getRequestedSelect() {
+			if v == selected {
 				return out
 			}
 		}
@@ -404,16 +441,17 @@ func (p *Partial) getFuncs(data *Data) template.FuncMap {
 	}
 
 	funcs["actionHeader"] = func() string {
-		return p.getActionHeader()
+		return p.getConnector().GetActionHeader()
 	}
 
 	funcs["requestedAction"] = func() string {
-		return p.GetRequestedAction()
+		return p.getConnector().GetActionValue(p.GetRequest())
 	}
 
 	funcs["ifRequestedAction"] = func(out any, in ...string) any {
+		action := p.getConnector().GetActionValue(p.GetRequest())
 		for _, v := range in {
-			if v == p.GetRequestedAction() {
+			if v == action {
 				return out
 			}
 		}
@@ -457,24 +495,14 @@ func (p *Partial) getLayoutData() map[string]any {
 	return p.layoutData
 }
 
-func (p *Partial) getPartialHeader() string {
-	if p.partialHeader != "" {
-		return p.partialHeader
+func (p *Partial) getConnector() connector.Connector {
+	if p.connector != nil {
+		return p.connector
 	}
 	if p.parent != nil {
-		return p.parent.getPartialHeader()
-	}
-	return defaultTargetHeader
-}
-
-func (p *Partial) getSelectHeader() string {
-	if p.selectHeader != "" {
-		return p.selectHeader
+		return p.parent.getConnector()
 	}
-	if p.parent != nil {
-		return p.parent.getSelectHeader()
-	}
-	return defaultSelectHeader
+	return nil
 }
 
 func (p *Partial) getSelectionPartials() map[string]*Partial {
@@ -484,22 +512,12 @@ func (p *Partial) getSelectionPartials() map[string]*Partial {
 	return nil
 }
 
-func (p *Partial) getActionHeader() string {
-	if p.actionHeader != "" {
-		return p.actionHeader
-	}
-	if p.parent != nil {
-		return p.parent.getActionHeader()
-	}
-	return defaultActionHeader
-}
-
-func (p *Partial) getRequest() *http.Request {
+func (p *Partial) GetRequest() *http.Request {
 	if p.request != nil {
 		return p.request
 	}
 	if p.parent != nil {
-		return p.parent.getRequest()
+		return p.parent.GetRequest()
 	}
 	return &http.Request{}
 }
@@ -511,7 +529,7 @@ func (p *Partial) getFS() fs.FS {
 	if p.parent != nil {
 		return p.parent.getFS()
 	}
-	return nil
+	return os.DirFS("./")
 }
 
 func (p *Partial) getLogger() Logger {
@@ -533,19 +551,21 @@ func (p *Partial) getLogger() Logger {
 	return p.logger
 }
 
-func (p *Partial) getRequestedPartial() string {
-	if p.requestedPartial != "" {
-		return p.requestedPartial
+func (p *Partial) GetRequestedPartial() string {
+	th := p.getConnector().GetTargetValue(p.GetRequest())
+	if th != "" {
+		return th
 	}
 	if p.parent != nil {
-		return p.parent.getRequestedPartial()
+		return p.parent.GetRequestedPartial()
 	}
 	return ""
 }
 
 func (p *Partial) GetRequestedAction() string {
-	if p.requestedAction != "" {
-		return p.requestedAction
+	ah := p.getConnector().GetActionValue(p.GetRequest())
+	if ah != "" {
+		return ah
 	}
 	if p.parent != nil {
 		return p.parent.GetRequestedAction()
@@ -553,18 +573,20 @@ func (p *Partial) GetRequestedAction() string {
 	return ""
 }
 
-func (p *Partial) getRequestedSelect() string {
-	if p.requestedSelect != "" {
-		return p.requestedSelect
+func (p *Partial) GetRequestedSelect() string {
+	as := p.getConnector().GetSelectValue(p.GetRequest())
+	if as != "" {
+		return as
 	}
 	if p.parent != nil {
-		return p.parent.getRequestedSelect()
+		return p.parent.GetRequestedSelect()
 	}
 	return ""
 }
 
 func (p *Partial) renderWithTarget(ctx context.Context, r *http.Request) (template.HTML, error) {
-	if p.getRequestedPartial() == "" || p.getRequestedPartial() == p.id {
+	requestedTarget := p.getConnector().GetTargetValue(p.GetRequest())
+	if requestedTarget == "" || requestedTarget == p.id {
 		out, err := p.renderSelf(ctx, r)
 		if err != nil {
 			return "", err
@@ -581,10 +603,10 @@ func (p *Partial) renderWithTarget(ctx context.Context, r *http.Request) (templa
 		}
 		return out, nil
 	} else {
-		c := p.recursiveChildLookup(p.getRequestedPartial(), make(map[string]bool))
+		c := p.recursiveChildLookup(requestedTarget, make(map[string]bool))
 		if c == nil {
-			p.getLogger().Error("requested partial not found in parent", "id", p.getRequestedPartial(), "parent", p.id)
-			return "", fmt.Errorf("requested partial %s not found in parent %s", p.getRequestedPartial(), p.id)
+			p.getLogger().Error("requested partial not found in parent", "id", requestedTarget, "parent", p.id)
+			return "", fmt.Errorf("requested partial %s not found in parent %s", requestedTarget, p.id)
 		}
 		return c.renderWithTarget(ctx, r)
 	}
@@ -634,7 +656,7 @@ func (p *Partial) renderChildPartial(ctx context.Context, id string, data map[st
 	}
 
 	// Render the cloned child partial
-	return childClone.renderSelf(ctx, p.getRequest())
+	return childClone.renderSelf(ctx, p.GetRequest())
 }
 
 // renderNamed renders the partial with the given name and templates.
@@ -665,7 +687,6 @@ func (p *Partial) renderSelf(ctx context.Context, r *http.Request) (template.HTM
 			p.getLogger().Error("error in action function", "error", err)
 			return "", fmt.Errorf("error in action function: %w", err)
 		}
-		//return actionPartial.renderSelf(ctx, r)
 	}
 
 	functions := p.getFuncs(data)
@@ -725,15 +746,7 @@ func (p *Partial) getOrParseTemplate(cacheKey string, functions template.FuncMap
 	}
 
 	t := template.New(path.Base(p.templates[0])).Funcs(functions)
-	var tmpl *template.Template
-	var err error
-
-	if fsys := p.getFS(); fsys != nil {
-		tmpl, err = t.ParseFS(fsys, p.templates...)
-	} else {
-		tmpl, err = t.ParseFiles(p.templates...)
-	}
-
+	tmpl, err := t.ParseFS(p.getFS(), p.templates...)
 	if err != nil {
 		return nil, fmt.Errorf("error parsing templates: %w", err)
 	}
@@ -757,10 +770,7 @@ func (p *Partial) clone() *Partial {
 		swapOOB:           p.swapOOB,
 		fs:                p.fs,
 		logger:            p.logger,
-		partialHeader:     p.partialHeader,
-		selectHeader:      p.selectHeader,
-		actionHeader:      p.actionHeader,
-		requestedPartial:  p.requestedPartial,
+		connector:         p.connector,
 		useCache:          p.useCache,
 		selection:         p.selection,
 		templates:         append([]string{}, p.templates...), // Copy the slice
diff --git a/partial_test.go b/partial_test.go
index 642a7b7..4cf4c2f 100644
--- a/partial_test.go
+++ b/partial_test.go
@@ -8,6 +8,8 @@ import (
 	"net/http/httptest"
 	"strings"
 	"testing"
+
+	"github.com/donseba/go-partial/connector"
 )
 
 func TestNewRoot(t *testing.T) {
@@ -539,8 +541,8 @@ func BenchmarkWithSelectMap(b *testing.B) {
 	}
 
 	service := NewService(&Config{
-		PartialHeader: "X-Target",
-		UseCache:      false,
+		Connector: connector.NewPartial(nil),
+		UseCache:  false,
 	})
 	layout := service.NewLayout().FS(fsys)
 
@@ -572,8 +574,8 @@ func BenchmarkWithSelectMap(b *testing.B) {
 func BenchmarkRenderWithRequest(b *testing.B) {
 	// Setup configuration and service
 	cfg := &Config{
-		PartialHeader: "X-Target",
-		UseCache:      false,
+		Connector: connector.NewPartial(nil),
+		UseCache:  false,
 	}
 
 	service := NewService(cfg)
diff --git a/service.go b/service.go
index 1005764..147b85f 100644
--- a/service.go
+++ b/service.go
@@ -7,15 +7,8 @@ import (
 	"log/slog"
 	"net/http"
 	"sync"
-)
 
-var (
-	// defaultTargetHeader is the default header used to determine which partial to render.
-	defaultTargetHeader = "X-Target"
-	// defaultSelectHeader is the default header used to determine which partial to select.
-	defaultSelectHeader = "X-Select"
-	// defaultActionHeader is the default header used to determine which action to take.
-	defaultActionHeader = "X-Action"
+	"github.com/donseba/go-partial/connector"
 )
 
 type (
@@ -25,19 +18,18 @@ type (
 	}
 
 	Config struct {
-		PartialHeader string
-		SelectHeader  string
-		ActionHeader  string
-		UseCache      bool
-		FuncMap       template.FuncMap
-		Logger        Logger
-		fs            fs.FS
+		Connector connector.Connector
+		UseCache  bool
+		FuncMap   template.FuncMap
+		Logger    Logger
+		fs        fs.FS
 	}
 
 	Service struct {
 		config            *Config
 		data              map[string]any
 		combinedFunctions template.FuncMap
+		connector         connector.Connector
 		funcMapLock       sync.RWMutex // Add a read-write mutex
 	}
 
@@ -47,11 +39,9 @@ type (
 		content           *Partial
 		wrapper           *Partial
 		data              map[string]any
-		requestedPartial  string
-		requestedAction   string
-		requestedSelect   string
 		request           *http.Request
 		combinedFunctions template.FuncMap
+		connector         connector.Connector
 		funcMapLock       sync.RWMutex // Add a read-write mutex
 	}
 )
@@ -62,18 +52,6 @@ func NewService(cfg *Config) *Service {
 		cfg.FuncMap = DefaultTemplateFuncMap
 	}
 
-	if cfg.PartialHeader == "" {
-		cfg.PartialHeader = defaultTargetHeader
-	}
-
-	if cfg.SelectHeader == "" {
-		cfg.SelectHeader = defaultSelectHeader
-	}
-
-	if cfg.ActionHeader == "" {
-		cfg.ActionHeader = defaultActionHeader
-	}
-
 	if cfg.Logger == nil {
 		cfg.Logger = slog.Default().WithGroup("partial")
 	}
@@ -83,6 +61,7 @@ func NewService(cfg *Config) *Service {
 		data:              make(map[string]any),
 		funcMapLock:       sync.RWMutex{},
 		combinedFunctions: cfg.FuncMap,
+		connector:         cfg.Connector,
 	}
 }
 
@@ -92,6 +71,7 @@ func (svc *Service) NewLayout() *Layout {
 		service:           svc,
 		data:              make(map[string]any),
 		filesystem:        svc.config.fs,
+		connector:         svc.connector,
 		combinedFunctions: svc.getFuncMap(),
 	}
 }
@@ -108,6 +88,11 @@ func (svc *Service) AddData(key string, value any) *Service {
 	return svc
 }
 
+func (svc *Service) SetConnector(conn connector.Connector) *Service {
+	svc.connector = conn
+	return svc
+}
+
 // MergeFuncMap merges the given FuncMap with the existing FuncMap.
 func (svc *Service) MergeFuncMap(funcMap template.FuncMap) {
 	svc.funcMapLock.Lock()
@@ -185,9 +170,6 @@ func (l *Layout) getFuncMap() template.FuncMap {
 
 // RenderWithRequest renders the partial with the given http.Request.
 func (l *Layout) RenderWithRequest(ctx context.Context, r *http.Request) (template.HTML, error) {
-	l.requestedPartial = r.Header.Get(l.service.config.PartialHeader)
-	l.requestedAction = r.Header.Get(l.service.config.ActionHeader)
-	l.requestedSelect = r.Header.Get(l.service.config.SelectHeader)
 	l.request = r
 
 	if l.wrapper != nil {
@@ -202,20 +184,32 @@ func (l *Layout) RenderWithRequest(ctx context.Context, r *http.Request) (templa
 
 // WriteWithRequest writes the layout to the response writer.
 func (l *Layout) WriteWithRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
-	out, err := l.RenderWithRequest(ctx, r)
-	if err != nil {
-		if l.service.config.Logger != nil {
-			l.service.config.Logger.Error("error rendering layout", "error", err)
+	l.request = r
+
+	if l.connector.RenderPartial(r) {
+		if l.wrapper != nil {
+			l.content.parent = l.wrapper
 		}
-		return err
+		err := l.content.WriteWithRequest(ctx, w, r)
+		if err != nil {
+			if l.service.config.Logger != nil {
+				l.service.config.Logger.Error("error rendering layout", "error", err)
+			}
+			return err
+		}
+		return nil
 	}
 
-	_, err = w.Write([]byte(out))
-	if err != nil {
-		if l.service.config.Logger != nil {
-			l.service.config.Logger.Error("error writing layout to response", "error", err)
+	if l.wrapper != nil {
+		l.wrapper.With(l.content)
+
+		err := l.wrapper.WriteWithRequest(ctx, w, r)
+		if err != nil {
+			if l.service.config.Logger != nil {
+				l.service.config.Logger.Error("error rendering layout", "error", err)
+			}
+			return err
 		}
-		return err
 	}
 
 	return nil
@@ -231,14 +225,15 @@ func (l *Layout) applyConfigToPartial(p *Partial) {
 
 	p.mergeFuncMapInternal(combinedFunctions)
 
-	p.fs = l.filesystem
-	p.logger = l.service.config.Logger
+	p.connector = l.service.connector
+	if l.filesystem != nil {
+		p.fs = l.filesystem
+	}
+	if l.service.config.Logger != nil {
+		p.logger = l.service.config.Logger
+	}
 	p.useCache = l.service.config.UseCache
 	p.globalData = l.service.data
 	p.layoutData = l.data
 	p.request = l.request
-	p.partialHeader = l.service.config.PartialHeader
-	p.selectHeader = l.service.config.SelectHeader
-	p.actionHeader = l.service.config.ActionHeader
-	p.requestedPartial = l.requestedPartial
 }
diff --git a/template_functions.go b/template_functions.go
index e281ca5..735f437 100644
--- a/template_functions.go
+++ b/template_functions.go
@@ -160,7 +160,7 @@ func selectionFunc(p *Partial, data *Data) func() template.HTML {
 			return template.HTML(fmt.Sprintf("no selection partials found in parent '%s'", p.id))
 		}
 
-		requestedSelect := p.getRequestedSelect()
+		requestedSelect := p.getConnector().GetSelectValue(p.GetRequest())
 		if requestedSelect != "" {
 			selectedPartial = partials[requestedSelect]
 		} else {
@@ -173,9 +173,8 @@ func selectionFunc(p *Partial, data *Data) func() template.HTML {
 		}
 
 		selectedPartial.fs = p.fs
-		//selectedPartial.parent = p
 
-		html, err := selectedPartial.renderSelf(data.Ctx, p.getRequest())
+		html, err := selectedPartial.renderSelf(data.Ctx, p.GetRequest())
 		if err != nil {
 			p.getLogger().Error("error rendering selected partial", "id", requestedSelect, "parent", p.id, "error", err)
 			return template.HTML(fmt.Sprintf("error rendering selected partial '%s'", requestedSelect))
@@ -228,7 +227,7 @@ func actionFunc(p *Partial, data *Data) func() template.HTML {
 		}
 
 		// Render the selected partial instead
-		html, err := actionPartial.renderSelf(data.Ctx, p.getRequest())
+		html, err := actionPartial.renderSelf(data.Ctx, p.GetRequest())
 		if err != nil {
 			p.getLogger().Error("error rendering action partial", "error", err)
 			return template.HTML(fmt.Sprintf("error rendering action partial: %v", err))