diff --git a/docs/architecture.md b/docs/architecture.md
index 6633f1d..0951301 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -1240,6 +1240,24 @@ Example:
}) }}
```
+### Additional rendering CSS classes
+
+Applications can append custom CSS classes to the rendered datatable structure through runtime options.
+
+Supported options:
+
+```twig
+{{ zhortein_datatable('users', {
+ rootClass: 'my-datatable',
+ tableWrapperClass: 'my-table-wrapper',
+ tableClass: 'my-table'
+}) }}
+```
+
+These options append classes and do not replace the bundle's default Bootstrap classes.
+
+This allows host applications to apply project-specific styling without overriding templates.
+
---
## 9. Action rendering layer
diff --git a/docs/basic-usage.md b/docs/basic-usage.md
index 16260a4..06d1bc4 100644
--- a/docs/basic-usage.md
+++ b/docs/basic-usage.md
@@ -636,10 +636,33 @@ For a complete minimal example without Doctrine, see [`examples/array-datatable.
For a complete Doctrine-backed example, see [`examples/doctrine-datatable.md`](examples/doctrine-datatable.md).
+---
+
+## 19. Additional CSS classes
+
+You can append custom CSS classes to the generated datatable markup:
+
+```twig
+{{ zhortein_datatable('users', {
+ rootClass: 'my-datatable',
+ tableWrapperClass: 'my-table-wrapper',
+ tableClass: 'my-table'
+}) }}
+```
+
+Available options:
+
+| Option | Target |
+|---|---|
+| `rootClass` | Root datatable container |
+| `tableWrapperClass` | Table responsive wrapper |
+| `tableClass` | `
` element |
+
+Classes are appended to existing Bootstrap classes.
---
-## 19. Current limitations
+## 20. Current limitations
The bundle is still under active development.
diff --git a/docs/theming.md b/docs/theming.md
index 126739d..0a2d5fb 100644
--- a/docs/theming.md
+++ b/docs/theming.md
@@ -307,6 +307,19 @@ The following controls use Bootstrap dropdown markup:
Without Bootstrap JavaScript, these controls may render but not open.
+## Additional CSS classes
+
+For project-specific styling, applications can append CSS classes without overriding templates.
+
+```twig
+{{ zhortein_datatable('users', {
+ rootClass: 'datatable datatable-users',
+ tableWrapperClass: 'datatable-wrapper',
+ tableClass: 'datatable-table'
+}) }}
+```
+
+The bundle keeps its default Bootstrap classes and appends the provided classes.
## Future direction
diff --git a/templates/bootstrap/datatable.html.twig b/templates/bootstrap/datatable.html.twig
index 788947b..0095b02 100644
--- a/templates/bootstrap/datatable.html.twig
+++ b/templates/bootstrap/datatable.html.twig
@@ -12,6 +12,9 @@
{% set table_small = options.tableSmall is defined ? options.tableSmall : false %}
{% set table_responsive = options.tableResponsive is defined ? options.tableResponsive : true %}
{% set table_classes = ['table', 'align-middle', 'mb-0'] %}
+{% set root_class = options.rootClass is defined ? options.rootClass|trim : '' %}
+{% set table_wrapper_class = options.tableWrapperClass is defined ? options.tableWrapperClass|trim : '' %}
+{% set table_class = options.tableClass is defined ? options.tableClass|trim : '' %}
{% if table_striped %}
{% set table_classes = table_classes|merge(['table-striped']) %}
@@ -33,9 +36,23 @@
{% set table_classes = table_classes|merge(['table-sm']) %}
{% endif %}
+{% if table_class is not empty %}
+ {% set table_classes = table_classes|merge([table_class]) %}
+{% endif %}
+
+{% set table_wrapper_classes = [] %}
+
+{% if table_responsive %}
+ {% set table_wrapper_classes = table_wrapper_classes|merge(['table-responsive']) %}
+{% endif %}
+
+{% if table_wrapper_class is not empty %}
+ {% set table_wrapper_classes = table_wrapper_classes|merge([table_wrapper_class]) %}
+{% endif %}
+
{{ 'zhortein_datatable.loading'|trans({}, 'zhortein_datatable') }}
-
+
{% include '@ZhorteinDatatable/bootstrap/_header.html.twig' %}
diff --git a/tests/Unit/Renderer/DatatableRendererAdditionalCssClassesTest.php b/tests/Unit/Renderer/DatatableRendererAdditionalCssClassesTest.php
new file mode 100644
index 0000000..3877ba3
--- /dev/null
+++ b/tests/Unit/Renderer/DatatableRendererAdditionalCssClassesTest.php
@@ -0,0 +1,110 @@
+createRenderer()->render($this->createDefinition(), [
+ 'rootClass' => 'datatable--compact my-root-class',
+ 'columnVisibility' => false,
+ 'export' => false,
+ ]);
+
+ self::assertStringContainsString('class="zhortein-datatable datatable--compact my-root-class"', $html);
+ }
+
+ public function test_it_appends_additional_table_wrapper_class(): void
+ {
+ $html = $this->createRenderer()->render($this->createDefinition(), [
+ 'tableWrapperClass' => 'my-table-wrapper',
+ 'columnVisibility' => false,
+ 'export' => false,
+ ]);
+
+ self::assertStringContainsString('class="table-responsive my-table-wrapper"', $html);
+ }
+
+ public function test_it_appends_wrapper_class_even_when_responsive_wrapper_is_disabled(): void
+ {
+ $html = $this->createRenderer()->render($this->createDefinition(), [
+ 'tableResponsive' => false,
+ 'tableWrapperClass' => 'my-table-wrapper',
+ 'columnVisibility' => false,
+ 'export' => false,
+ ]);
+
+ self::assertStringContainsString('class="my-table-wrapper"', $html);
+ self::assertStringNotContainsString('table-responsive my-table-wrapper', $html);
+ }
+
+ public function test_it_appends_additional_table_class_without_removing_defaults(): void
+ {
+ $html = $this->createRenderer()->render($this->createDefinition(), [
+ 'tableClass' => 'my-table table-sm-custom',
+ 'columnVisibility' => false,
+ 'export' => false,
+ ]);
+
+ self::assertStringContainsString('table', $html);
+ self::assertStringContainsString('align-middle', $html);
+ self::assertStringContainsString('mb-0', $html);
+ self::assertStringContainsString('table-striped', $html);
+ self::assertStringContainsString('table-hover', $html);
+ self::assertStringContainsString('my-table table-sm-custom', $html);
+ }
+
+ public function test_it_ignores_empty_additional_classes(): void
+ {
+ $html = $this->createRenderer()->render($this->createDefinition(), [
+ 'rootClass' => ' ',
+ 'tableWrapperClass' => '',
+ 'tableClass' => ' ',
+ 'columnVisibility' => false,
+ 'export' => false,
+ ]);
+
+ self::assertStringContainsString('class="zhortein-datatable"', $html);
+ self::assertStringContainsString('class="table-responsive"', $html);
+ self::assertStringContainsString('class="table align-middle mb-0 table-striped table-hover"', $html);
+ }
+
+ private function createDefinition(): DatatableDefinition
+ {
+ $definition = new DatatableDefinition('users');
+ $definition->addColumn('e.email', label: 'Email');
+
+ return $definition;
+ }
+
+ private function createRenderer(): DatatableRenderer
+ {
+ return new DatatableRenderer($this->createTwigEnvironment());
+ }
+
+ private function createTwigEnvironment(): Environment
+ {
+ $loader = new FilesystemLoader();
+ $loader->addPath(__DIR__.'/../../../templates', 'ZhorteinDatatable');
+
+ $twig = new Environment($loader, [
+ 'strict_variables' => true,
+ 'autoescape' => 'html',
+ ]);
+
+ $this->addTranslationExtension($twig);
+
+ return $twig;
+ }
+}