diff --git a/src/panels/PanelTile.jsx b/src/panels/PanelTile.jsx
index 8a031ef..deffbcd 100644
--- a/src/panels/PanelTile.jsx
+++ b/src/panels/PanelTile.jsx
@@ -134,7 +134,7 @@ export const PanelTile = injectMuiTheme(PanelTileBase);
* @behaviour Uses a CSS grid with `gap: 2` (theme spacing).
* @behaviour Children position themselves via the `wide` prop of each tile.
*/
-export function PanelTileGrid({ children, sx }) {
+export function PanelTileGrid({ children, sx, icon, text, label, badge, ...rest }) {
return (
{children}
diff --git a/src/panels/TabbedPanelTileGrid.jsx b/src/panels/TabbedPanelTileGrid.jsx
new file mode 100644
index 0000000..b72d218
--- /dev/null
+++ b/src/panels/TabbedPanelTileGrid.jsx
@@ -0,0 +1,204 @@
+import React, { Component } from 'react';
+import Box from '@mui/material/Box';
+import Chip from '@mui/material/Chip';
+import Tab from '@mui/material/Tab';
+import Tabs from '@mui/material/Tabs';
+
+/**
+ * Tabbed wrapper for multiple `PanelTileGrid` layouts.
+ *
+ * Each child `PanelTileGrid` defines its tab metadata via props:
+ * - `text` (or `label`): Tab label string
+ * - `icon`: Optional icon ReactNode
+ * - `value`: Optional unique tab value (defaults to child index)
+ * - `disabled`: Optional boolean
+ * - `badge`: Optional string/number shown as a pill chip next to the label
+ *
+ * Example:
+ * ```jsx
+ *
+ * } text="Auswahl-Merkmale">
+ * ...
+ * ...
+ *
+ * } text="Text-Merkmale">
+ * ...
+ *
+ *
+ * ```
+ *
+ * @component TabbedPanelTileGrid
+ * @import dashgui/panels
+ *
+ * @prop {ReactNode} children - One or more PanelTileGrid elements.
+ * @prop {any} [value] - Controlled active tab value/index.
+ * @prop {any} [defaultValue] - Initial active tab value/index for uncontrolled mode.
+ * @prop {any} [defaultTab] - Alias for defaultValue.
+ * @prop {(nextValue: any, event: React.SyntheticEvent) => void} [onChange] - Called on tab switch.
+ * @prop {boolean} [keepMounted=true] - When true, inactive grids remain mounted in DOM with `display: none` to preserve scroll, refs, and sortable states.
+ * @prop {object} [sx] - Extra styles merged into root container Box.
+ * @prop {object} [tabsWrapperSx] - Extra styles for the container around Tabs.
+ * @prop {object} [tabsSx] - Extra styles merged into MUI Tabs.
+ * @prop {object} [tabSx] - Extra styles merged into MUI Tab.
+ */
+export class TabbedPanelTileGrid extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ internalTab: props.defaultValue ?? props.defaultTab ?? 0,
+ };
+ this.handleTabChange = this.handleTabChange.bind(this);
+ }
+
+ getTabs() {
+ const children = React.Children.toArray(this.props.children).filter(Boolean);
+ return children.map((child, index) => {
+ const value = child.props.value !== undefined ? child.props.value : index;
+ const text = child.props.text ?? child.props.label ?? child.props.title ?? `Tab ${index + 1}`;
+ const icon = child.props.icon ?? null;
+ const disabled = Boolean(child.props.disabled);
+ const badge = child.props.badge;
+ const color = child.props.color;
+ return {
+ value,
+ text,
+ icon,
+ disabled,
+ badge,
+ color,
+ child,
+ index,
+ };
+ });
+ }
+
+ getActiveValue(tabs) {
+ if (this.props.value !== undefined) {
+ return this.props.value;
+ }
+ const { internalTab } = this.state;
+ if (tabs.some((t) => t.value === internalTab)) {
+ return internalTab;
+ }
+ const firstEnabled = tabs.find((t) => !t.disabled) ?? tabs[0];
+ return firstEnabled ? firstEnabled.value : 0;
+ }
+
+ handleTabChange(event, nextValue) {
+ if (this.props.value === undefined) {
+ this.setState({ internalTab: nextValue }, () => {
+ if (typeof window !== 'undefined') {
+ window.dispatchEvent(new Event('resize'));
+ }
+ });
+ } else if (typeof window !== 'undefined') {
+ window.dispatchEvent(new Event('resize'));
+ }
+ this.props.onChange?.(nextValue, event);
+ }
+
+ render() {
+ const {
+ keepMounted = true,
+ sx,
+ tabsWrapperSx,
+ tabsSx,
+ tabSx,
+ } = this.props;
+
+ const tabs = this.getTabs();
+ if (tabs.length === 0) return null;
+
+ const activeValue = this.getActiveValue(tabs);
+
+ return (
+
+
+
+ {tabs.map((tab) => (
+
+ {tab.text}
+
+
+ ) : (
+ tab.text
+ )
+ }
+ />
+ ))}
+
+
+
+ {tabs.map((tab) => {
+ const isSelected = tab.value === activeValue;
+ if (!isSelected && !keepMounted) {
+ return null;
+ }
+ return React.cloneElement(tab.child, {
+ key: String(tab.value),
+ sx: {
+ ...tab.child.props.sx,
+ ...(isSelected ? {} : { display: 'none !important' }),
+ },
+ });
+ })}
+
+ );
+ }
+}
+
+export default TabbedPanelTileGrid;
diff --git a/src/panels/index.js b/src/panels/index.js
index f24644f..7fc1bcb 100644
--- a/src/panels/index.js
+++ b/src/panels/index.js
@@ -1,4 +1,5 @@
export { PanelTile, PanelTileGrid, GRID_COLUMNS } from './PanelTile';
+export { TabbedPanelTileGrid } from './TabbedPanelTileGrid';
export { EditablePanelTile } from './EditablePanelTile';
export { injectMuiTheme } from './injectMuiTheme';
export { PENDING_EDIT_BORDER } from './panelTileLayout';