diff --git a/responsive.go b/responsive.go
new file mode 100644
index 0000000..25374f4
--- /dev/null
+++ b/responsive.go
@@ -0,0 +1,39 @@
+package html
+
+import "strings"
+
+// Responsive wraps multiple Layout variants for breakpoint-aware rendering.
+// Each variant is rendered inside a container with data-variant for CSS targeting.
+type Responsive struct {
+ variants []responsiveVariant
+}
+
+type responsiveVariant struct {
+ name string
+ layout *Layout
+}
+
+// NewResponsive creates a new multi-variant responsive compositor.
+func NewResponsive() *Responsive {
+ return &Responsive{}
+}
+
+// Variant adds a named layout variant (e.g., "desktop", "tablet", "mobile").
+// Variants render in insertion order.
+func (r *Responsive) Variant(name string, layout *Layout) *Responsive {
+ r.variants = append(r.variants, responsiveVariant{name: name, layout: layout})
+ return r
+}
+
+// Render produces HTML with each variant in a data-variant container.
+func (r *Responsive) Render(ctx *Context) string {
+ var b strings.Builder
+ for _, v := range r.variants {
+ b.WriteString(`
`)
+ b.WriteString(v.layout.Render(ctx))
+ b.WriteString(`
`)
+ }
+ return b.String()
+}
diff --git a/responsive_test.go b/responsive_test.go
new file mode 100644
index 0000000..483d018
--- /dev/null
+++ b/responsive_test.go
@@ -0,0 +1,59 @@
+package html
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestResponsive_SingleVariant(t *testing.T) {
+ ctx := NewContext()
+ r := NewResponsive().
+ Variant("desktop", NewLayout("HLCRF").
+ H(Raw("header")).L(Raw("nav")).C(Raw("main")).R(Raw("aside")).F(Raw("footer")))
+ got := r.Render(ctx)
+
+ if !strings.Contains(got, `data-variant="desktop"`) {
+ t.Errorf("responsive should contain data-variant, got:\n%s", got)
+ }
+ if !strings.Contains(got, `data-block="H-0"`) {
+ t.Errorf("responsive should contain layout content, got:\n%s", got)
+ }
+}
+
+func TestResponsive_MultiVariant(t *testing.T) {
+ ctx := NewContext()
+ r := NewResponsive().
+ Variant("desktop", NewLayout("HLCRF").H(Raw("h")).L(Raw("l")).C(Raw("c")).R(Raw("r")).F(Raw("f"))).
+ Variant("tablet", NewLayout("HCF").H(Raw("h")).C(Raw("c")).F(Raw("f"))).
+ Variant("mobile", NewLayout("C").C(Raw("c")))
+
+ got := r.Render(ctx)
+
+ for _, v := range []string{"desktop", "tablet", "mobile"} {
+ if !strings.Contains(got, `data-variant="`+v+`"`) {
+ t.Errorf("responsive missing variant %q in:\n%s", v, got)
+ }
+ }
+}
+
+func TestResponsive_VariantOrder(t *testing.T) {
+ ctx := NewContext()
+ r := NewResponsive().
+ Variant("desktop", NewLayout("HLCRF").C(Raw("d"))).
+ Variant("mobile", NewLayout("C").C(Raw("m")))
+
+ got := r.Render(ctx)
+
+ di := strings.Index(got, `data-variant="desktop"`)
+ mi := strings.Index(got, `data-variant="mobile"`)
+ if di < 0 || mi < 0 {
+ t.Fatalf("missing variants in:\n%s", got)
+ }
+ if di >= mi {
+ t.Errorf("desktop should appear before mobile (insertion order), desktop=%d mobile=%d", di, mi)
+ }
+}
+
+func TestResponsive_ImplementsNode(t *testing.T) {
+ var _ Node = NewResponsive()
+}