diff --git a/src/simudator/gui/module_graphics_item/actions.py b/src/simudator/gui/module_graphics_item/actions.py
index 1747bec59fd1cf99501b600788e452d18bd9797c..c1abb1f5f37d1f974e2816e03fe39a8af5e1a6c8 100644
--- a/src/simudator/gui/module_graphics_item/actions.py
+++ b/src/simudator/gui/module_graphics_item/actions.py
@@ -45,3 +45,23 @@ def edit_state_action(module: Module, module_widget: ModuleWidget) -> QAction:
     action.triggered.connect(action_triggered)
     return action
 
+
+def toggle_ports_action(module_widget: ModuleWidget) -> QAction:
+    """
+    Create a QAction for toggling the visibility of port widgets of a module
+    widget.
+
+    Parameters
+    ----------
+    module_widget : ModuleWidget
+        The module for which its ports should be toggled to visible or invisible
+        using a QAction.
+
+    Returns
+    -------
+        QAction for toggling port visibility of a module widget.
+    """
+    action = QAction("Toggle ports", module_widget)
+    action.triggered.connect(module_widget.toggle_ports)
+    return action
+
diff --git a/src/simudator/gui/module_graphics_item/module_widget.py b/src/simudator/gui/module_graphics_item/module_widget.py
index b1a6a8b286ca0d317d9e03c5f6c47cb3d1d004d5..98716c22ff72295e7eb8b2a0cf365a48bd62dac8 100644
--- a/src/simudator/gui/module_graphics_item/module_widget.py
+++ b/src/simudator/gui/module_graphics_item/module_widget.py
@@ -1,5 +1,3 @@
-from typing import Any
-
 from qtpy.QtCore import Property, QRectF, Qt
 from qtpy.QtCore import Signal as pyqtSignal
 from qtpy.QtCore import Slot
@@ -16,6 +14,7 @@ from qtpy.QtWidgets import (
 
 from simudator.core.module import Module
 from simudator.gui.formatting import Format, FormatInfo, format_to_str
+from simudator.gui.port_widget import PortWidget
 
 
 class ModuleWidget(QGraphicsWidget):
@@ -31,14 +30,17 @@ class ModuleWidget(QGraphicsWidget):
     state_vars : dict[str, FormatInfo]
         The state variables of the module to display and in what formats to
         display them.
+    ports: list[str]
+        List of names for signal port widgets to create and display for this
+        module widget.
     parent : QGraphicsItem | None
         Optional parent item of this module widget.
     flags : Qt.WindowFlags | Qt.WindowType
         Optional window flags for the window of this widget.
     """
 
-    DEFAULT_WIDTH = 50 * 3
-    DEFAULT_HEIGHT = 50 * 3
+    _DEFAULT_WIDTH = 50 * 3
+    _DEFAULT_HEIGHT = 50 * 3
 
     state_changed = pyqtSignal()
 
@@ -46,6 +48,7 @@ class ModuleWidget(QGraphicsWidget):
         self,
         module: Module,
         state_vars: dict[str, FormatInfo],
+        ports: list[str],
         parent: QGraphicsItem | None = None,
         flags: Qt.WindowFlags | Qt.WindowType = Qt.WindowFlags(),
     ) -> None:
@@ -55,6 +58,7 @@ class ModuleWidget(QGraphicsWidget):
 
         self._module = module
         self._state_vars = state_vars
+        self._port_widgets: list[PortWidget] = []
 
         # Default values for properties for appearance
         # TODO: Put these values in constants?
@@ -65,8 +69,9 @@ class ModuleWidget(QGraphicsWidget):
         self._text_font = QFont("Sans Serif", 9)
         self._padding = 5
 
-        width = self.DEFAULT_WIDTH
-        height = self.DEFAULT_HEIGHT
+        # Set the size of the module depending on the number of state variables
+        width = self._DEFAULT_WIDTH
+        height = self._DEFAULT_HEIGHT
         if len(self._state_vars) <= 1:
             height = QFontMetrics(self._text_font).height() + 2 * self._padding
         else:
@@ -75,6 +80,8 @@ class ModuleWidget(QGraphicsWidget):
             ) + self._padding
         self.resize(width, height)
 
+        self._create_ports(ports)
+
     def paint(
         self,
         painter: QPainter | None = None,
@@ -146,19 +153,32 @@ class ModuleWidget(QGraphicsWidget):
         width = self.size().width()
         height = self.size().height()
         margin = self.outline_width / 2
-        return QRectF(0 - margin, 0 - margin, width + margin, height + margin)
+        return QRectF(0 - margin, 0 - margin, width + 2 * margin, height + 2 * margin)
 
-    @Slot(bool)
-    def show_ports(self, value: bool) -> None:
+    def _create_ports(self, ports: list[str]) -> None:
         """
-        Toggle the visibility of the signal ports of the displayed module.
+        Instantiate and add port widgets to this module widget.
 
         Parameters
         ----------
-        value : bool
-            `True` to show all ports, `False` to hide them.
+        ports : list[str]
+            List of names for the ports to create. One widget is created per
+            each name.
         """
-        pass
+        for port_name in ports:
+            port = PortWidget(port_name, parent=self)
+            port.setPos(0, 0)
+            self._port_widgets.append(port)
+
+    @Slot()
+    def toggle_ports(self) -> None:
+        """
+        Toggle the visibility of the signal ports of the displayed module.
+        """
+        # TODO: Set the visibility to the same value for all ports rather than
+        # switching the visibility for each port
+        for port in self._port_widgets:
+            port.setVisible(not port.isVisible())
 
     def contextMenuEvent(self, event: 'QGraphicsSceneContextMenuEvent') -> None:
         """
@@ -308,4 +328,3 @@ class ModuleWidget(QGraphicsWidget):
     background = Property(QBrush, background, set_background)
     text_color = Property(QColor, text_color, set_text_color)
     text_font = Property(QFont, text_font, set_text_font)
-
diff --git a/src/simudator/gui/orientation.py b/src/simudator/gui/orientation.py
index 6d3adfe80bf67dbc4b230128b90677109d7bd017..f9d49487870b74746956e9da662304f415700c52 100644
--- a/src/simudator/gui/orientation.py
+++ b/src/simudator/gui/orientation.py
@@ -1,6 +1,7 @@
 from enum import IntEnum
 
 
+# TODO: Remove this file
 class Orientation(IntEnum):
     """
     Used to give and track ports orientations.
diff --git a/src/simudator/gui/port_widget.py b/src/simudator/gui/port_widget.py
new file mode 100644
index 0000000000000000000000000000000000000000..e78ed6c60fd0fb51636ec48a1638556e5d4cd135
--- /dev/null
+++ b/src/simudator/gui/port_widget.py
@@ -0,0 +1,394 @@
+from enum import IntEnum
+
+from qtpy.QtCore import Property, QPointF, QRectF, Qt
+from qtpy.QtCore import Signal as pyqtSignal
+from qtpy.QtCore import Slot
+from qtpy.QtGui import QBrush, QColor, QFont, QFontMetrics, QPainter, QPainterPath, QPen
+from qtpy.QtWidgets import (
+    QAction,
+    QGraphicsItem,
+    QGraphicsSceneContextMenuEvent,
+    QGraphicsSceneMouseEvent,
+    QGraphicsWidget,
+    QMenu,
+    QStyleOptionGraphicsItem,
+    QWidget,
+)
+
+
+class Orientation(IntEnum):
+    """
+    Used to give and track ports orientations.
+    """
+
+    UP = 0
+    LEFT = 1
+    DOWN = 2
+    RIGHT = 3
+
+
+class PortWidget(QGraphicsWidget):
+    """
+    A graphics widget for displaying a signal port of a module widget.
+
+    The port is drawn as a line at the edge of the shape of a module widget.
+    A name of the port can also be displayed at the end of the port.
+
+    The default appearance of the port has line length 12, line width 1 and
+    uses the font Sans Serif 9pt.
+
+    Parameters
+    ----------
+    name : str
+        The name to display on the label for the port widget.
+    parent : QGraphicsItem | None
+        Optional parent item of this module widget.
+    flags : Qt.WindowFlags | Qt.WindowType
+        Optional window flags for the window of this widget.
+    """
+
+    _DEFAULT_LINE_LENGTH = 12
+    """
+    Default length of the line that represents this signal port.
+    """
+    _DEFAULT_LINE_WIDTH = 1
+    """
+    Default width of the line that represents this signal port.
+    """
+    _DEFAULT_TEXT_FONT = QFont("Sans Serif", 10)
+    """
+    Default text font used to display the name label of this signal port.
+    """
+
+    signal_toggled = pyqtSignal()
+
+    def __init__(
+        self,
+        name: str,
+        parent: QGraphicsItem | None = None,
+        flags: Qt.WindowFlags | Qt.WindowType = Qt.WindowFlags(),
+    ) -> None:
+        super().__init__(parent, flags)
+        self._name = name
+        # TODO: Change this? Use shape() instead?
+        self._attached_shape = parent.boundingRect()
+
+        self.set_orientation(Orientation.LEFT)
+        self.set_line_length(self._DEFAULT_LINE_LENGTH)
+        self.set_line_width(self._DEFAULT_LINE_WIDTH)
+        self.set_line_brush(self.palette().text())
+        self.set_label_visible(True)
+        self.set_text_font(self._DEFAULT_TEXT_FONT)
+
+        self.setFlag(QGraphicsItem.ItemIsMovable)
+
+    def paint(
+        self,
+        painter: QPainter | None,
+        option: QStyleOptionGraphicsItem | None = None,
+        widget: QWidget | None = None,
+    ) -> None:
+        painter.save()
+
+        # Set style and draw a line to represent the port
+        painter.setPen(QPen(self.line_brush, self.line_width))
+        painter.drawLine(QPointF(0, 0), self.get_end_point())
+
+        # Draw the port label if it should be visible
+        if self.label_visible:
+            # Set text style
+            painter.setFont(self.text_font)
+
+            # Draw text
+            text_rect = self._get_label_rect()
+            painter.drawText(text_rect, Qt.AlignmentFlag.AlignCenter, self._name)
+
+        painter.restore()
+
+    def get_name(self) -> str:
+        """
+        Return the displayed name of this port.
+
+        Returns
+        -------
+        str
+            Name of this port.
+        """
+        return self._name
+
+    def get_end_point(self) -> QPointF:
+        """
+        Return the point at the end of the line of this port. The end is the one
+        that is not attached to the shape of the module widget that the port
+        is attached to.
+
+        Returns
+        -------
+        QPointF
+            The end point of the port line.
+        """
+        match self.orientation:
+            case Orientation.UP:
+                return QPointF(0, -self.line_length)
+            case Orientation.DOWN:
+                return QPointF(0, self.line_length)
+            case Orientation.LEFT:
+                return QPointF(-self.line_length, 0)
+            case Orientation.RIGHT:
+                return QPointF(self.line_length, 0)
+
+    def _get_label_rect(self) -> QRectF:
+        """
+        Calculate and return a rectangle in which the name label of this port
+        should be placed.
+
+        Returns
+        -------
+        QRectF
+            Rectangle to put the port name label in.
+        """
+        end_point = self.get_end_point()
+        font_metrics = QFontMetrics(self.text_font)
+        text_width = font_metrics.width(self._name)
+        text_height = font_metrics.height()
+
+        match self.orientation:
+            case Orientation.UP:
+                x = end_point.x() - text_width / 2
+                y = end_point.y() - text_height
+            case Orientation.DOWN:
+                x = end_point.x() - text_width / 2
+                y = end_point.y()
+            case Orientation.LEFT:
+                x = end_point.x() - text_width
+                y = end_point.y() - text_height
+            case Orientation.RIGHT:
+                x = end_point.x()
+                y = end_point.y() - text_height
+
+        return QRectF(x, y, text_width, text_height)
+
+    def shape(self) -> QPainterPath:
+        path = QPainterPath()
+        path.addRect(self.boundingRect())
+        return path
+
+    def boundingRect(self) -> QRectF:
+        label_rect = self._get_label_rect()
+        match self.orientation:
+            case Orientation.UP:
+                top_left = label_rect.topLeft()
+                width = label_rect.width()
+                height = label_rect.height() + self.line_length
+
+            case Orientation.DOWN:
+                top_left = label_rect.topLeft() - QPointF(0, self.line_length)
+                width = label_rect.width()
+                height = label_rect.height() + self.line_length
+
+            case Orientation.LEFT:
+                top_left = label_rect.topLeft()
+                width = label_rect.width() + self.line_length
+                height = label_rect.height()
+
+            case Orientation.RIGHT:
+                top_left = QPointF(0, label_rect.top())
+                width = label_rect.width() + self.line_length
+                height = label_rect.height()
+
+        # Add small margin since the line is drawn on the edge of the bounding box.
+        # This removes any possibilities of artifacts appearing.
+        margin = self.line_width / 2
+        return QRectF(
+            top_left.x() - margin,
+            top_left.y() - margin,
+            width + 2 * margin,
+            height + 2 * margin,
+        )
+
+    def contextMenuEvent(
+        self, event: QGraphicsSceneContextMenuEvent | None = None
+    ) -> None:
+        menu = QMenu()
+        hide_signal_action = QAction("Toggle signal visibility", menu)
+        hide_signal_action.triggered.connect(self.signal_toggled)
+        # hide_signal_action.setEnabled(not self.isEnabled)
+
+        hide_port_action = QAction("Toggle port visibility", menu)
+        hide_port_action.triggered.connect(
+            lambda: self.setVisible(not self.isVisible())
+        )
+        # hide_port_action.setEnabled(not self.isLocked)
+
+        menu.addAction(hide_signal_action)
+        menu.addAction(hide_port_action)
+        menu.exec_(event.screenPos())
+        event.accept()
+
+    def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None:
+        if event.buttons() != Qt.MouseButton.LeftButton:
+            return
+
+        rel_x = event.scenePos().x() - self.parentItem().x()
+        rel_y = event.scenePos().y() - self.parentItem().y()
+
+        min_x = self._attached_shape.topLeft().x()
+        min_y = self._attached_shape.topLeft().y()
+        max_x = self._attached_shape.bottomRight().x()
+        max_y = self._attached_shape.bottomRight().y()
+
+        # clamp position between the min and max allowed values
+        x_pos = min(max(rel_x, min_x), max_x)
+        y_pos = min(max(rel_y, min_y), max_y)
+
+        # Make sure the port cannot be placed inside of the module that it
+        # is attached to
+        if not (min_x < x_pos < max_x) or not (min_y < y_pos < max_y):
+            self.setX(x_pos)
+            self.setY(y_pos)
+
+            if x_pos == max_x:
+                self.set_orientation(Orientation.RIGHT)
+            elif x_pos == min_x:
+                self.set_orientation(Orientation.LEFT)
+            elif y_pos == max_y:
+                self.set_orientation(Orientation.DOWN)
+            elif y_pos == min_y:
+                self.set_orientation(Orientation.UP)
+            self.update(self.boundingRect())
+
+    def line_length(self) -> float:
+        """
+        Return the length of the port line.
+
+        Returns
+        -------
+        float
+            The length of the port line as a float.
+        """
+        return self._line_length
+
+    def set_line_length(self, length: float) -> None:
+        """
+        Set the length of the port line.
+
+        Parameters
+        ----------
+        length: float
+            The desired port length.
+        """
+        self._line_length = length
+
+    def line_width(self) -> float:
+        """
+        Return the width of the port line length.
+        """
+        return self._line_width
+
+    def set_line_width(self, width: float) -> None:
+        """
+        Set the width of the port line.
+
+        Parameters
+        ----------
+        length: float
+            The desired port line width.
+        """
+        self._line_width = width
+
+    def line_brush(self) -> QBrush:
+        """
+        Return the brush used to draw the port widget.
+
+        Returns
+        -------
+        QBrush
+            The brush used to draw the port widget.
+        """
+        return self._line_brush
+
+    def set_line_brush(self, brush: QBrush) -> None:
+        """
+        Set the brush used to draw the port widget.
+
+        Parameters
+        ----------
+        brush: QBrush
+            The brush to use when drawing the port widget.
+        """
+        self._line_brush = brush
+
+    def label_visible(self) -> bool:
+        """
+        Return a bool representing the visible state of the ports label.
+
+        Returns
+        -------
+        bool
+        `True` if the label is visible, `False` otherwise.
+        """
+        return self._label_visible
+
+    @Slot(bool)
+    def set_label_visible(self, value: bool) -> None:
+        """
+        Set the visibility of the label.
+
+        Parameter
+        ---------
+        value: bool
+            Sets the label as visible if value is `True`, `False` otherwise.
+        """
+        self._label_visible = value
+
+    def orientation(self) -> Orientation:
+        """
+        Return the orientation of the port widget.
+
+        Returns
+        -------
+        Orientation
+            The current orientation of the port widget.
+        """
+        return self._orientation
+
+    def set_orientation(self, orientation: Orientation) -> None:
+        """
+        Set the orientation of the port widget.
+
+        Parameters
+        ----------
+        orientation: Orientation
+            Sets the orientation of the port widget to one of the alternatives of
+            UP, DOWN, LEFT, RIGHT.
+        """
+        self._orientation = orientation
+
+    def text_font(self) -> QFont:
+        """
+        Return the font of the port widgets label.
+
+        Returns
+        -------
+        QFont
+            The font used for the text in the port widgets label.
+        """
+        return self._text_font
+
+    def set_text_font(self, font: QFont) -> None:
+        """
+        Set the font for the port widgets label.
+
+        Parameters
+        ----------
+        font: QFont
+            Sets the font used in the port widgets label.
+        """
+        self._text_font = font
+
+    line_length = Property(float, line_length, set_line_length)
+    line_width = Property(float, line_width, set_line_width)
+    line_brush = Property(QBrush, line_brush, set_line_brush)
+    label_visible = Property(bool, label_visible, set_label_visible)
+    orientation = Property(Orientation, orientation, set_orientation)
+    text_font = Property(QFont, text_font, set_text_font)