diff --git a/src/sfg_walberla/api.py b/src/sfg_walberla/api.py
index 482ec88fb4e10bcc732d0e8f04dfd389857d89a0..f5d391a9728ff4a57c7cab5e6a8999bcef0a2c3e 100644
--- a/src/sfg_walberla/api.py
+++ b/src/sfg_walberla/api.py
@@ -15,6 +15,19 @@ from pystencilssfg.ir import SfgHeaderInclude
 real_t = PsCustomType("walberla::real_t")
 
 
+class Vector2(SrcVector):
+    def __init__(self, val_type: UserTypeSpec):
+        self._value_type = create_type(val_type)
+        val_type_str = self._value_type.c_string()
+        super().__init__(PsCustomType(f"walberla::Vector2< {val_type_str} >"))
+
+    def extract_component(self, coordinate: int) -> AugExpr:
+        if coordinate > 1:
+            raise ValueError(f"Cannot extract component {coordinate} from Vector2")
+        
+        return AugExpr(self._value_type).bind("{}[{}]", self, coordinate)
+
+
 class Vector3(SrcVector):
     def __init__(self, val_type: UserTypeSpec):
         self._value_type = create_type(val_type)
@@ -22,6 +35,9 @@ class Vector3(SrcVector):
         super().__init__(PsCustomType(f"walberla::Vector3< {val_type_str} >"))
 
     def extract_component(self, coordinate: int) -> AugExpr:
+        if coordinate > 2:
+            raise ValueError(f"Cannot extract component {coordinate} from Vector3")
+
         return AugExpr(self._value_type).bind("{}[{}]", self, coordinate)
 
 
diff --git a/src/sfg_walberla/boundaries/hbb.py b/src/sfg_walberla/boundaries/hbb.py
index 27626306239007bdb88469094c925474b617c767..97db11db6c82e07111d6e93d6848aa15d5632b0d 100644
--- a/src/sfg_walberla/boundaries/hbb.py
+++ b/src/sfg_walberla/boundaries/hbb.py
@@ -13,7 +13,7 @@ from lbmpy.boundaries.boundaryconditions import LbBoundary
 from lbmpy.boundaries.boundaryhandling import create_lattice_boltzmann_boundary_kernel
 from pystencilssfg.lang.expressions import IFieldExtraction
 
-from ..sweep import FieldInfo, SweepClassProperties
+from ..sweep import FieldInfo, SweepClassProperties, combine_vectors
 from ..api import GhostLayerFieldPtr, BlockDataID, IBlockPtr, StructuredBlockForest
 
 BoundaryIndexType = create_type("int32")
@@ -107,14 +107,15 @@ class SimpleHbbBoundary(CustomGenerator):
             key=lambda f: f.field.name,
         )
 
-        scalar_params = khandle.scalar_parameters
+        parameters = khandle.scalar_parameters
+        vector_groups = combine_vectors(parameters)
 
         props = HbbBoundaryProperties()
 
         for fi in domain_fields:
             props.add_property(fi.data_id, setter=False, getter=True)
 
-        for s in sorted(scalar_params, key=lambda p: p.name):
+        for s in sorted(parameters, key=lambda p: p.name):
             props.add_property(s, setter=True, getter=True)
 
         block = IBlockPtr().var("block")
@@ -142,14 +143,16 @@ class SimpleHbbBoundary(CustomGenerator):
                     ),
                     #   Map GhostLayerFields to pystencils fields
                     *(sfg.map_field(fi.field, fi.glfield_ptr) for fi in domain_fields),
-                    #   Extract scalar parameters
-                    *(sfg.init(param)(props.get(param)) for param in scalar_params),
                     #   Get index vector
                     sfg.init(idx_vector)(
                         block.getData(idx_vector.vector_type, idx_vector_id)
                     ),
                     #   Map index vector to pystencils field
                     sfg.map_field(self._index_field, idx_vector),
+                    #   Extract parameters
+                    *(sfg.init(param)(props.get(param)) for param in parameters),
+                    #   Extract vector components
+                    *(sfg.map_vector(comps, vec) for vec, comps in vector_groups.items()),
                     #   Call kernel
                     sfg.call(khandle),
                 )
diff --git a/src/sfg_walberla/sweep.py b/src/sfg_walberla/sweep.py
index 4e48c515a0f61e8bf44bc98bf95320de35d064d7..59f7b387074921c572e8b6b161aa92a3c25e7a7d 100644
--- a/src/sfg_walberla/sweep.py
+++ b/src/sfg_walberla/sweep.py
@@ -2,6 +2,7 @@ from typing import Sequence, Iterable, Callable
 from dataclasses import dataclass, replace
 from itertools import chain
 from collections import defaultdict
+import warnings
 
 import sympy as sp
 
@@ -19,8 +20,8 @@ from pystencils import (
 from pystencils.types import PsType, constify, deconstify, PsCustomType
 
 from pystencilssfg import SfgComposer
-from pystencilssfg.lang import VarLike, asvar, SfgVar, AugExpr, Ref
-from .api import GhostLayerFieldPtr, IBlockPtr, BlockDataID, Vector3
+from pystencilssfg.lang import VarLike, asvar, SfgVar, AugExpr, Ref, SrcVector
+from .api import GhostLayerFieldPtr, IBlockPtr, BlockDataID, Vector2, Vector3
 
 
 class SweepClassProperties:
@@ -188,18 +189,68 @@ class ShadowFieldCache:
         return f"shadow_{str(orig)}_"
 
 
-#   TODO: Happens outside of Properties class
-# def combine_vectors():
-#     vector_components: defaultdict[str, list[SweepClassProperties.Property]] = (
-#         defaultdict(lambda _: [None, None, None])
-#     )
+def combine_vectors(scalars: set[SfgVar]) -> dict[SrcVector, tuple[SfgVar, ...]]:
+    """Attempt to combine vector component symbols into vectors.
 
-#     for prop_name, prop in self._properties.items():
-#         tokens = prop_name.split("_")
-#         if tokens[-1] in ("0", "1", "2"):
-#             vec_name = "_".join(tokens[:-1])
-#             vec_comp = int(tokens[-1])
-#             vector_components[vec_name][vec_comp] = prop
+    This function modifies the `scalars` parameter in-place by removing grouped scalar
+    components and replacing them by their vectors.
+    """
+    potential_vectors: defaultdict[str, list[tuple[int, SfgVar]]] = defaultdict(list)
+
+    for s in scalars:
+        tokens = s.name.split("_")
+        try:
+            coord = int(tokens[-1])
+            vec_name = "_".join(tokens[:-1])
+            potential_vectors[vec_name].append((coord, s))
+        except ValueError:
+            pass
+
+    all_vectors: dict[SrcVector, tuple[SfgVar, ...]] = dict()
+
+    for name, entries in potential_vectors.items():
+        entries = sorted(entries, key=lambda t: t[0])
+        coords = [e[0] for e in entries]
+        components = tuple(e[1] for e in entries)
+        scalar_types = set(e[1].dtype for e in entries)
+
+        if set(coords) == {0, 1}:
+            vec_type = Vector2
+        elif set(coords) == {0, 1, 2}:
+            vec_type = Vector3
+        else:
+            continue
+
+        if len(scalar_types) > 1:
+            warnings.warn(
+                f"Skipping vector grouping for vector {name}: "
+                f"Encountered vector components {components} with conflicting data types {scalar_types}.",
+                UserWarning,
+            )
+            continue
+
+        name_conflict = False
+        for s in scalars:
+            if s.name == name:
+                warnings.warn(
+                    f"Skipping vector grouping for vector {name}: "
+                    f"Vector name conflicts with existing scalar parameter {s}.",
+                    UserWarning,
+                )
+                name_conflict = True
+
+        if name_conflict:
+            continue
+
+        scalar_type = deconstify(scalar_types.pop())
+        vector = vec_type(scalar_type).var(name)
+        all_vectors[vector] = components
+
+        for c in components:
+            scalars.remove(c)
+        scalars.add(asvar(vector))
+
+    return all_vectors
 
 
 class Sweep(CustomGenerator):
@@ -225,25 +276,9 @@ class Sweep(CustomGenerator):
         self._kernel = kernel
         self._gen_config = config
 
-        #   Vectors
-        self._vectors: dict[Vector3, tuple[sp.Symbol, ...]] = dict()
-
         #   Map from shadow field to shadowed field
         self._shadow_fields: dict[Field, Field] = dict()
 
-    def vector_param(self, vector: Vector3, vector_entries: tuple[sp.Symbol]):
-        if len(vector_entries) not in (2, 3):
-            raise ValueError("Can only combine 2D or 3D vectors")
-
-        if vector in self._vectors:
-            raise KeyError(f"Duplicate vector parameter: {vector}")
-
-        if not vector.is_bound():
-            v_name = vector_entries[0].name.split("_")[0]
-            vector = vector.var(v_name)
-
-        self._vectors[vector] = vector_entries
-
     def swap_fields(self, field: Field, shadow_field: Field):
         if field in self._shadow_fields:
             raise ValueError(f"Field swap for {field} was already registered.")
@@ -286,14 +321,7 @@ class Sweep(CustomGenerator):
 
         parameters = khandle.scalar_parameters
 
-        #   Remove vector entries from parameter list, add vectors instead
-        vector_entry_names: set[str] = (
-            set.union(*(set(e.name for e in vec) for vec in self._vectors.values()))
-            if self._vectors
-            else set()
-        )
-        parameters = set(filter(lambda p: p.name not in vector_entry_names, parameters))
-        parameters |= set(asvar(vec) for vec in self._vectors)
+        vector_groups = combine_vectors(parameters)
 
         props = SweepClassProperties()
 
@@ -336,7 +364,7 @@ class Sweep(CustomGenerator):
                     #   Extract scalars from vectors
                     *(
                         sfg.map_vector(components, vector)
-                        for vector, components in self._vectors.items()
+                        for vector, components in vector_groups.items()
                     ),
                     sfg.call(khandle),
                     #   Perform field swaps