diff --git a/common/utils.py b/common/utils.py
index 264c9ca..ea2ea39 100644
--- a/common/utils.py
+++ b/common/utils.py
@@ -1,8 +1,9 @@
 from random import choices
 from string import ascii_lowercase
-from typing import Any
+from typing import Any, Callable
 
 from django.template.loader import render_to_string
+from django.urls import NoReverseMatch, reverse
 from django.utils.safestring import mark_safe
 
 
@@ -31,16 +32,68 @@ HTMLAttribute = tuple[str, str]
 HTMLTag = str
 
 
-def A(attributes: list[HTMLAttribute], children: list[HTMLTag] | HTMLTag) -> HTMLTag:
+def Component(
+    attributes: list[HTMLAttribute] = [],
+    children: list[HTMLTag] | HTMLTag = [],
+    template: str = "",
+    tag_name: str = "",
+) -> HTMLTag:
+    if not tag_name and not template:
+        raise ValueError("One of template or tag_name is required.")
     if isinstance(children, str):
         children = [children]
     childrenBlob = "\n".join(children)
     attributesList = [f'{name} = "{value}"' for name, value in attributes]
     attributesBlob = " ".join(attributesList)
-    tag: str = f"{childrenBlob}"
+    tag: str = ""
+    if tag_name != "":
+        tag = f"{childrenBlob}"
+    elif template != "":
+        tag = render_to_string(
+            template,
+            {name: value for name, value in attributes} | {"slot": "\n".join(children)},
+        )
     return mark_safe(tag)
 
 
+def A(
+    attributes: list[HTMLAttribute] = [],
+    children: list[HTMLTag] | HTMLTag = [],
+    url: str | Callable[..., Any] = "",
+):
+    """
+    Returns the HTML tag "a".
+    "url" can either be:
+        - URL (string)
+        - path name passed to reverse() (string)
+        - function
+    """
+    additional_attributes = []
+    if url:
+        if type(url) is str:
+            try:
+                url_result = reverse(url)
+            except NoReverseMatch:
+                url_result = url
+        elif callable(url):
+            url_result = url()
+        else:
+            raise TypeError("'url' is neither str nor function.")
+        additional_attributes = [("href", url_result)]
+    return Component(
+        tag_name="a", attributes=attributes + additional_attributes, children=children
+    )
+
+
+def Button(
+    attributes: list[HTMLAttribute] = [],
+    children: list[HTMLTag] | HTMLTag = [],
+):
+    return Component(
+        template="cotton/button.html", attributes=attributes, children=children
+    )
+
+
 def safe_division(numerator: int | float, denominator: int | float) -> int | float:
     """
     Divides without triggering division by zero exception.
diff --git a/games/templates/cotton/button.html b/games/templates/cotton/button.html
index 83f0695..3cc1f1f 100644
--- a/games/templates/cotton/button.html
+++ b/games/templates/cotton/button.html
@@ -1,4 +1,5 @@
 
diff --git a/games/templates/cotton/simple_table.html b/games/templates/cotton/simple_table.html
index 62c413f..8f08b99 100644
--- a/games/templates/cotton/simple_table.html
+++ b/games/templates/cotton/simple_table.html
@@ -2,6 +2,11 @@
 
     
         
+            {% if header_action %}
+                
+                {{ header_action }}
+                
+            {% endif %}
             
                 
                     {% for column in columns %}| {{ column }} | {% endfor %}
diff --git a/games/templates/cotton/table_header.html b/games/templates/cotton/table_header.html
new file mode 100644
index 0000000..2fa28cc
--- /dev/null
+++ b/games/templates/cotton/table_header.html
@@ -0,0 +1,3 @@
+
+    {{ slot }}
+
diff --git a/games/templates/list_purchases.html b/games/templates/list_purchases.html
index ac553d9..f1e45dc 100644
--- a/games/templates/list_purchases.html
+++ b/games/templates/list_purchases.html
@@ -5,6 +5,6 @@
 {% endblock title %}
 {% block content %}
     
-        
+        
     
 {% endblock content %}
diff --git a/games/views/device.py b/games/views/device.py
index 7842a40..3fc1a2b 100644
--- a/games/views/device.py
+++ b/games/views/device.py
@@ -7,6 +7,7 @@ from django.shortcuts import get_object_or_404, redirect, render
 from django.template.loader import render_to_string
 from django.urls import reverse
 
+from common.utils import A, Button
 from games.forms import DeviceForm
 from games.models import Device
 from games.views.general import dateformat
@@ -35,6 +36,7 @@ def list_devices(request: HttpRequest) -> HttpResponse:
             else None
         ),
         "data": {
+            "header_action": A([], Button([], "Add device"), url="add_device"),
             "columns": [
                 "Name",
                 "Type",
diff --git a/games/views/edition.py b/games/views/edition.py
index f569390..a2568cf 100644
--- a/games/views/edition.py
+++ b/games/views/edition.py
@@ -7,7 +7,7 @@ from django.shortcuts import get_object_or_404, redirect, render
 from django.template.loader import render_to_string
 from django.urls import reverse
 
-from common.utils import A, truncate_with_popover
+from common.utils import A, Button, truncate_with_popover
 from games.forms import EditionForm
 from games.models import Edition, Game
 from games.views.general import dateformat
@@ -36,6 +36,7 @@ def list_editions(request: HttpRequest) -> HttpResponse:
             else None
         ),
         "data": {
+            "header_action": A([], Button([], "Add edition"), url="add_edition"),
             "columns": [
                 "Game",
                 "Name",
diff --git a/games/views/game.py b/games/views/game.py
index 51132e7..db12442 100644
--- a/games/views/game.py
+++ b/games/views/game.py
@@ -9,7 +9,7 @@ from django.template.loader import render_to_string
 from django.urls import reverse
 
 from common.time import format_duration
-from common.utils import A, safe_division, truncate_with_popover
+from common.utils import A, Button, safe_division, truncate_with_popover
 from games.forms import GameForm
 from games.models import Edition, Game, Purchase, Session
 from games.views.general import (
@@ -45,6 +45,7 @@ def list_games(request: HttpRequest) -> HttpResponse:
             else None
         ),
         "data": {
+            "header_action": A([], Button([], "Add game"), url="add_game"),
             "columns": [
                 "Name",
                 "Sort Name",
diff --git a/games/views/platform.py b/games/views/platform.py
index 49995b1..3906510 100644
--- a/games/views/platform.py
+++ b/games/views/platform.py
@@ -7,6 +7,7 @@ from django.shortcuts import get_object_or_404, redirect, render
 from django.template.loader import render_to_string
 from django.urls import reverse
 
+from common.utils import A, Button
 from games.forms import PlatformForm
 from games.models import Platform
 from games.views.general import dateformat, use_custom_redirect
@@ -35,6 +36,7 @@ def list_platforms(request: HttpRequest) -> HttpResponse:
             else None
         ),
         "data": {
+            "header_action": A([], Button([], "Add platform"), url="add_platform"),
             "columns": [
                 "Name",
                 "Group",
diff --git a/games/views/purchase.py b/games/views/purchase.py
index e75d338..f977e4c 100644
--- a/games/views/purchase.py
+++ b/games/views/purchase.py
@@ -13,7 +13,7 @@ from django.template.loader import render_to_string
 from django.urls import reverse
 from django.utils import timezone
 
-from common.utils import A, truncate_with_popover
+from common.utils import A, Button, truncate_with_popover
 from games.forms import PurchaseForm
 from games.models import Edition, Purchase
 from games.views.general import dateformat, use_custom_redirect
@@ -42,6 +42,7 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
             else None
         ),
         "data": {
+            "header_action": A([], Button([], "Add purchase"), url="add_purchase"),
             "columns": [
                 "Name",
                 "Platform",
diff --git a/games/views/session.py b/games/views/session.py
index 9022a32..0e85639 100644
--- a/games/views/session.py
+++ b/games/views/session.py
@@ -9,7 +9,7 @@ from django.urls import reverse
 from django.utils import timezone
 
 from common.time import format_duration
-from common.utils import A, truncate_with_popover
+from common.utils import A, Button, truncate_with_popover
 from games.forms import SessionForm
 from games.models import Purchase, Session
 from games.views.general import (
@@ -45,6 +45,7 @@ def list_sessions(request: HttpRequest) -> HttpResponse:
             else None
         ),
         "data": {
+            "header_action": A([], Button([], "Add session"), url="add_session"),
             "columns": [
                 "Name",
                 "Date",
@@ -57,16 +58,11 @@ def list_sessions(request: HttpRequest) -> HttpResponse:
             "rows": [
                 [
                     A(
-                        [
-                            (
-                                "href",
-                                reverse(
-                                    "view_game",
-                                    args=[session.purchase.edition.game.pk],
-                                ),
-                            )
-                        ],
-                        truncate_with_popover(session.purchase.edition.name),
+                        children=truncate_with_popover(session.purchase.edition.name),
+                        url=reverse(
+                            "view_game",
+                            args=[session.purchase.edition.game.pk],
+                        ),
                     ),
                     f"{session.timestamp_start.strftime(datetimeformat)}{f" — {session.timestamp_end.strftime(timeformat)}" if session.timestamp_end else ""}",
                     (