diff --git a/CHANGELOG.md b/CHANGELOG.md
index 60342d5e..8f599da4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
+## v3.3.2
+*Release date: 2026-03-24*
+
+### Added
+- `Element.source_locator` attribute that preserves the original locator before platform-specific transformations
+
+---
+
## v3.3.1
*Release date: 2026-01-05*
diff --git a/docs/source/element_object/key_features.md b/docs/source/element_object/key_features.md
index fe34abe8..9503e0ce 100644
--- a/docs/source/element_object/key_features.md
+++ b/docs/source/element_object/key_features.md
@@ -39,3 +39,56 @@ particularly for negative checks (i.e., when an element is not present on the pa
## 3. Built-in waits
Most methods automatically wait for specific element states.
For example, the framework will wait until a web element becomes clickable before executing `click` method on it.
+
+---
+
+
+
+
+## 4. Original locator preservation
+The `source_locator` attribute stores the original locator exactly as it was provided to `Element.__init__`,
+before any platform-specific resolution or framework-specific transformations.
+
+```{note}
+For **static** sub-elements, consider using the built-in parent mechanism instead —
+`Element` objects defined as class attributes of a `Group` automatically search within
+the Group locator (see {doc}`Group documentation <../group_object/index>`).
+
+`source_locator` is designed for cases where you need the **raw locator string** for
+dynamic XPath construction at runtime — something the parent mechanism cannot do.
+```
+
+**Example — dynamic table parsing:**
+
+```python
+from mops.base.group import Group
+from mops.base.element import Element
+
+
+class DataTable(Group):
+
+ def load(self):
+ row_locator = f'{self.source_locator}//tr'
+ row_elements = Element(row_locator, f'{self.name}: Rows').all_elements
+
+ self.rows = []
+ for index, _ in enumerate(row_elements):
+ cell_locator = f'({row_locator})[{index + 1}]/td'
+ cells = Element(cell_locator, f'{self.name}: Row {index} cells')
+ self.rows.append(cells)
+```
+
+Here `source_locator` is used to dynamically compose new XPath expressions
+via string concatenation. This cannot be achieved with the parent mechanism because:
+
+- The XPath grouping operator `(…)[n]` requires building the full expression as a single string.
+- New `Element` objects are created at runtime, not as class-level attributes.
+- After initialization, `locator` is transformed with platform prefixes
+ (e.g., `xpath=` for Playwright), making it unsuitable for string concatenation.
+
+```{note}
+`source_locator` preserves the exact type passed in: if a `Locator` dataclass was given, it stays a `Locator`;
+if a string was given, it stays the original string.
+The `locator` attribute, by contrast, is resolved to a platform-specific string and may be further modified
+(e.g., prefixed with `xpath=` for Playwright or converted to a CSS selector for ID-based locators in Selenium).
+```
diff --git a/mops/__init__.py b/mops/__init__.py
index e2527452..9553764d 100644
--- a/mops/__init__.py
+++ b/mops/__init__.py
@@ -1,2 +1,2 @@
-__version__ = '3.3.1'
+__version__ = '3.3.2'
__project_name__ = 'mops'
diff --git a/mops/base/element.py b/mops/base/element.py
index 7ecf711b..82ddbaa6 100644
--- a/mops/base/element.py
+++ b/mops/base/element.py
@@ -55,6 +55,8 @@ class Element(DriverMixin, InternalMixin, Logging, ElementABC):
and provides a unified interface for UI interactions.
"""
+ source_locator: Union[Locator, str]
+
_object = 'element'
_base_cls: Type[PlayElement, MobileElement, WebElement]
driver_wrapper: DriverWrapper
@@ -115,6 +117,7 @@ def __init__(
raise ValueError(error)
self.locator = locator
+ self.source_locator = locator
self.name = name if name else locator
self.parent = parent
self.wait = wait
diff --git a/tests/static_tests/integration/test_source_locator.py b/tests/static_tests/integration/test_source_locator.py
new file mode 100644
index 00000000..9e518238
--- /dev/null
+++ b/tests/static_tests/integration/test_source_locator.py
@@ -0,0 +1,83 @@
+import pytest
+
+from mops.base.element import Element
+from mops.base.group import Group
+from mops.mixins.objects.locator import Locator
+from tests.static_tests.conftest import desktop_drivers, desktop_ids, mobile_drivers, mobile_ids
+
+
+xpath_locator = '//div[@class="test"]'
+css_locator = '.test-class'
+id_locator = 'test-id'
+ios_locator = 'ios_locator'
+android_locator = 'android_locator'
+mobile_locator = 'mobile_locator'
+
+multi_platform_locator = Locator(
+ default='default_locator',
+ desktop='desktop_locator',
+ mobile=mobile_locator,
+ ios=ios_locator,
+ android=android_locator,
+)
+
+
+class SourceLocatorGroup(Group):
+ def __init__(self):
+ super().__init__(xpath_locator, name='source locator group')
+
+ sub_element = Element(css_locator, name='sub element')
+ multi_element = Element(multi_platform_locator, name='multi element')
+
+
+@pytest.mark.parametrize('driver', desktop_drivers, ids=desktop_ids)
+def test_source_locator_preserved_for_string_xpath(driver, request):
+ request.getfixturevalue(driver)
+ element = Element(xpath_locator, name='xpath element')
+ assert element.source_locator == xpath_locator
+
+
+@pytest.mark.parametrize('driver', desktop_drivers, ids=desktop_ids)
+def test_source_locator_preserved_for_string_css(driver, request):
+ request.getfixturevalue(driver)
+ element = Element(css_locator, name='css element')
+ assert element.source_locator == css_locator
+
+
+@pytest.mark.parametrize('driver', desktop_drivers, ids=desktop_ids)
+def test_source_locator_preserved_for_string_id(driver, request):
+ request.getfixturevalue(driver)
+ element = Element(id_locator, name='id element')
+ assert element.source_locator == id_locator
+
+
+@pytest.mark.parametrize('driver', desktop_drivers, ids=desktop_ids)
+def test_source_locator_differs_from_transformed_locator(driver, request):
+ request.getfixturevalue(driver)
+ element = Element(xpath_locator, name='xpath element')
+ assert element.source_locator == xpath_locator
+ assert element.locator != xpath_locator or element.source_locator == element.locator
+
+
+@pytest.mark.parametrize('driver', desktop_drivers, ids=desktop_ids)
+def test_source_locator_preserved_for_locator_dataclass(driver, request):
+ request.getfixturevalue(driver)
+ element = Element(multi_platform_locator, name='multi element')
+ assert element.source_locator is multi_platform_locator
+
+
+@pytest.mark.parametrize('driver', mobile_drivers, ids=mobile_ids)
+def test_source_locator_preserved_for_locator_dataclass_mobile(driver, request):
+ request.getfixturevalue(driver)
+ element = Element(multi_platform_locator, name='multi element')
+ assert element.source_locator is multi_platform_locator
+ assert isinstance(element.locator, str)
+
+
+@pytest.mark.parametrize('driver', desktop_drivers, ids=desktop_ids)
+def test_source_locator_preserved_in_sub_elements(driver, request):
+ request.getfixturevalue(driver)
+ group = SourceLocatorGroup()
+ assert group.source_locator == xpath_locator
+ assert group.sub_element.source_locator == css_locator
+ assert group.multi_element.source_locator is multi_platform_locator