diff --git a/src/simudator/gui/module_graphics_item/module_widget.py b/src/simudator/gui/module_graphics_item/module_widget.py
new file mode 100644
index 0000000000000000000000000000000000000000..b9cc6d81384e902cd7ed68cf9ee5f817530dcafe
--- /dev/null
+++ b/src/simudator/gui/module_graphics_item/module_widget.py
@@ -0,0 +1,219 @@
+from typing import Any
+from qtpy.QtCore import Property, QPointF, QRectF, Qt, Slot
+from qtpy.QtGui import QBrush, QColor, QFont, QFontMetrics, QPainter, QPainterPath, QPen
+from qtpy.QtCore import Signal as pyqtSignal
+from qtpy.QtCore import Slot
+from qtpy.QtWidgets import (
+    QGraphicsItem,
+    QGraphicsWidget,
+    QStyleOptionGraphicsItem,
+    QWidget,
+    QAction,
+)
+
+from simudator.core.module import Module
+
+class ModuleWidget(QGraphicsWidget):
+    DEFAULT_WIDTH = 50*3
+    DEFAULT_HEIGHT = 50*3
+
+    update = pyqtSignal()
+
+    def __init__(self, module: Module, state_vars: list[str], actions: list[QAction], formatters: dict[str, Any], parent: QGraphicsItem | None = None, flags: Qt.WindowFlags | Qt.WindowType = Qt.WindowFlags()) -> None:
+        super().__init__(parent, flags)
+        super().__init__(parent, flags)
+        self.setFlag(QGraphicsItem.ItemIsMovable)
+
+        self._module = module
+        self._state_vars = state_vars
+        self._actions = actions
+        self._formatters = formatters
+
+        # Default values for properties for appearance
+        # TODO: Put these values in constants?
+        self._outline_width = 1
+        self._outline = self.palette().windowText()
+        self._background = self.palette().window()
+        self._text_color = self.palette().windowText().color()
+        self._text_font = QFont("Sans Serif", 9)
+        self._padding = 5
+
+        width = self.DEFAULT_WIDTH
+        height = self.DEFAULT_HEIGHT
+        if len(self._state_vars) <= 1:
+           height = QFontMetrics(self._text_font).height() + 2*self._padding
+        else:
+            height = (self._padding + QFontMetrics(self._text_font).height()) * (len(state_vars) + 1) + self._padding
+        self.resize(width, height)
+
+    def paint(
+        self,
+        painter: QPainter | None = None,
+        option: QStyleOptionGraphicsItem | None = None,
+        widget: QWidget | None = None,
+    ) -> None:
+        painter.save()
+
+        width = self.size().width()
+        height = self.size().height()
+
+        # Draw the base shape
+        pen = QPen(self.outline, self.outline_width)
+        painter.setPen(pen)
+        painter.setBrush(self.background)
+        painter.drawPath(self.shape())
+
+        # Set text specific painter settings
+        painter.setPen(QPen(self.text_color))
+        painter.setBrush(QBrush(self.text_color))
+        painter.setFont(self.text_font)
+        text_flags = Qt.AlignmentFlag.AlignCenter | Qt.TextFlag.TextWordWrap
+
+        text_rect = self.shape().boundingRect()
+
+        # Draw only name of the module if no state variables are to be shown
+        if len(self._state_vars) == 0:
+            painter.drawText(text_rect, text_flags, self._module.get_state()["name"])
+
+        # Special case: only 1 state variable to show
+        elif len(self._state_vars) == 1:
+            value = self._module.get_state()[self._state_vars[0]]
+            painter.drawText(text_rect, text_flags, f"{self._module.get_state()["name"]}: {value}")
+
+        # Draw the name of the module and each state variable with its value on one row each
+        else:
+            text_rect = QRectF(0,  0, width, height/(len(self._state_vars)+1))
+            painter.drawText(text_rect, text_flags, self._module.get_state()["name"])
+
+            for i, state_var in enumerate(self._state_vars):
+                y_start = height / (len(self._state_vars)+1) * (i+1)
+                y_end = height / (len(self._state_vars) + 1 )
+                text_rect = QRectF(0, y_start, width, y_end)
+                value = self._module.get_state()[state_var]
+                painter.drawText(text_rect, text_flags, f"{state_var}: {value}")
+
+        painter.restore()
+
+    @Slot()
+    def update(self, rect: QRectF | None = None):
+        # This "override" is needed in order to decorate update() as a
+        # pyqt slot
+        super().update()
+
+    def shape(self) -> QPainterPath:
+        path = QPainterPath()
+        path.addRect(0, 0, self.size().width(), self.size().height())
+        return path
+
+    def boundingRect(self) -> QRectF:
+        width = self.size().width()
+        height = self.size().height()
+        margin = self.outline_width / 2
+        return QRectF(0 - margin, 0 - margin, width + margin, height + margin)
+
+    def outline_width(self) -> float:
+        """Return the outline pen width.
+
+        Returns
+        -------
+        float
+            Outline pen width.
+        """
+        return self._outline_width
+
+    def set_outline_width(self, width: float) -> None:
+        """Set the outline pen width.
+
+        Parameters
+        ----------
+        width : float
+            Outline pen width.
+
+        """
+        self._outline_width = width
+
+    def outline(self) -> QBrush:
+        """Return the outline brush used to create the outline pen.
+
+        Returns
+        -------
+        QBrush
+            Outline brush.
+        """
+        return self._outline
+
+    def set_outline(self, brush: QBrush) -> None:
+        """Set the outline brush used to create the outline pen.
+
+        Parameters
+        ----------
+        brush : QBrush
+            Outline brush.
+        """
+        self._outline = brush
+
+    def background(self) -> QBrush:
+        """Return the bursh used for filling the background.
+
+        Returns
+        -------
+        QBrush
+            Background brush.
+        """
+        return self._background
+
+    def set_background(self, brush: QBrush) -> None:
+        """Set the bursh used for filling the background.
+
+        Parameters
+        ----------
+        brush : QBrush
+            Background brush.
+        """
+        self._background = brush
+
+    def text_color(self) -> QColor:
+        """Return the color used for text.
+
+        Returns
+        -------
+        QColor
+            Text color.
+        """
+        return self._text_color
+
+    def set_text_color(self, color: QColor) -> None:
+        """Set the color used for text.
+
+        Parameters
+        ----------
+        color : QColor
+            Text color.
+        """
+        self._text_color = color
+
+    def text_font(self) -> QFont:
+        """Return the font used for text.
+
+        Returns
+        -------
+        QFont
+            Text font.
+        """
+        return self._text_font
+
+    def set_text_font(self, font: QFont) -> None:
+        """Set the font used for text.
+
+        Parameters
+        ----------
+        font : QFont
+            Text font.
+        """
+        self._text_font = font
+
+    outline_width = Property(float, outline_width, set_outline_width)
+    outline = Property(QBrush, outline, set_outline)
+    background = Property(QBrush, background, set_background)
+    text_color = Property(QColor, text_color, set_text_color)
+    text_font = Property(QFont, text_font, set_text_font)
\ No newline at end of file