diff --git a/sas/forms.py b/sas/forms.py
index d987aaf1..71dedd7d 100644
--- a/sas/forms.py
+++ b/sas/forms.py
@@ -1,6 +1,7 @@
 from typing import Any
 
 from django import forms
+from django.core.exceptions import ValidationError
 from django.utils.translation import gettext_lazy as _
 
 from core.models import User
@@ -11,55 +12,28 @@ from sas.models import Album, Picture, PictureModerationRequest
 from sas.widgets.ajax_select import AutoCompleteSelectAlbum
 
 
-class SASForm(forms.Form):
-    album_name = forms.CharField(
-        label=_("Add a new album"), max_length=Album.NAME_MAX_LENGTH, required=False
-    )
-    images = MultipleImageField(
-        label=_("Upload images"),
-        required=False,
-    )
+class AlbumCreateForm(forms.ModelForm):
+    class Meta:
+        model = Album
+        fields = ["name", "parent"]
+        labels = {"name": _("Add a new album")}
+        widgets = {"parent": forms.HiddenInput}
 
-    def process(self, parent, owner, files, *, automodere=False):
-        try:
-            if self.cleaned_data["album_name"] != "":
-                album = Album(
-                    parent=parent,
-                    name=self.cleaned_data["album_name"],
-                    owner=owner,
-                    is_moderated=automodere,
-                )
-                album.clean()
-                album.save()
-        except Exception as e:
-            self.add_error(
-                None,
-                _("Error creating album %(album)s: %(msg)s")
-                % {"album": self.cleaned_data["album_name"], "msg": repr(e)},
-            )
-        for f in files:
-            new_file = Picture(
-                parent=parent,
-                name=f.name,
-                file=f,
-                owner=owner,
-                mime_type=f.content_type,
-                size=f.size,
-                is_folder=False,
-                is_moderated=automodere,
-            )
-            if automodere:
-                new_file.moderator = owner
-            try:
-                new_file.clean()
-                new_file.generate_thumbnails()
-                new_file.save()
-            except Exception as e:
-                self.add_error(
-                    None,
-                    _("Error uploading file %(file_name)s: %(msg)s")
-                    % {"file_name": f, "msg": repr(e)},
-                )
+    def __init__(self, *args, owner: User, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.instance.owner = owner
+        if owner.has_perm("sas.moderate_sasfile"):
+            self.instance.is_moderated = True
+            self.instance.moderator = owner
+
+    def clean(self):
+        if not self.instance.owner.can_edit(self.instance.parent):
+            raise ValidationError(_("You do not have the permission to do that"))
+        return super().clean()
+
+
+class PictureUploadForm(forms.Form):
+    images = MultipleImageField(label=_("Upload images"), required=False)
 
 
 class PictureEditForm(forms.ModelForm):
diff --git a/sas/templates/sas/album.jinja b/sas/templates/sas/album.jinja
index 6c2cbcf7..677552c1 100644
--- a/sas/templates/sas/album.jinja
+++ b/sas/templates/sas/album.jinja
@@ -110,13 +110,28 @@
 
   {% if is_sas_admin %}
     </form>
-    <form class="add-files" id="upload_form" action="" method="post" enctype="multipart/form-data">
+    {{ album_create_fragment }}
+    <form
+      class="add-files"
+      id="upload_form"
+      x-data="pictureUpload({{ album.id }})"
+      @submit.prevent="sendPictures()"
+    >
       {% csrf_token %}
       <div class="inputs">
-        {{ form.as_p() }}
-
+        <p>
+          <label for="{{ upload_form.images.id_for_label }}">{{ upload_form.images.label }} :</label>
+          {{ upload_form.images|add_attr("x-ref=pictures") }}
+          <span class="helptext">{{ upload_form.images.help_text }}</span>
+        </p>
         <input type="submit" value="{% trans %}Upload{% endtrans %}" />
+        <progress x-ref="progress" x-show="sending"></progress>
       </div>
+      <ul class="errorlist">
+        <template x-for="error in errors">
+          <li class="error" x-text="error"></li>
+        </template>
+      </ul>
     </form>
   {% endif %}
 
diff --git a/sas/templates/sas/fragments/album_create_form.jinja b/sas/templates/sas/fragments/album_create_form.jinja
new file mode 100644
index 00000000..4369bff6
--- /dev/null
+++ b/sas/templates/sas/fragments/album_create_form.jinja
@@ -0,0 +1,18 @@
+<form
+  class="add-files"
+  hx-post="{{ url("sas:album_create") }}"
+  hx-disabled-elt="find input[type='submit']"
+  hx-swap="outerHTML"
+>
+  {% csrf_token %}
+  <div class="inputs">
+    <div>
+      <label for="{{ form.name.id_for_label }}">{{ form.name.label }}</label>
+      {{ form.name }}
+    </div>
+    {{ form.parent }}
+    <input type="submit" value="{% trans %}Create{% endtrans %}" />
+  </div>
+  {{ form.non_field_errors() }}
+  {{ form.name.errors }}
+</form>
\ No newline at end of file
diff --git a/sas/templates/sas/main.jinja b/sas/templates/sas/main.jinja
index 98dc9f87..5fa82a45 100644
--- a/sas/templates/sas/main.jinja
+++ b/sas/templates/sas/main.jinja
@@ -61,23 +61,8 @@
 
       {% if is_sas_admin %}
         </form>
-
         <br>
-
-        <form class="add-files" action="" method="post" enctype="multipart/form-data">
-          {% csrf_token %}
-
-          <div class="inputs">
-            <div>
-              <label for="{{ form.album_name.name }}">{{ form.album_name.label }}</label>
-              {{ form.album_name }}
-            </div>
-            <input type="submit" value="{% trans %}Create{% endtrans %}" />
-          </div>
-
-          {{ form.non_field_errors() }}
-          {{ form.album_name.errors }}
-        </form>
+        {{ album_create_fragment }}
       {% endif %}
     {% endif %}
   </main>
diff --git a/sas/urls.py b/sas/urls.py
index 5fb57ccf..9a5435d2 100644
--- a/sas/urls.py
+++ b/sas/urls.py
@@ -16,6 +16,7 @@
 from django.urls import path
 
 from sas.views import (
+    AlbumCreateFragment,
     AlbumEditView,
     AlbumUploadView,
     AlbumView,
@@ -59,4 +60,5 @@ urlpatterns = [
     path(
         "user/<int:user_id>/pictures/", UserPicturesView.as_view(), name="user_pictures"
     ),
+    path("fragment/album-create", AlbumCreateFragment.as_view(), name="album_create"),
 ]
diff --git a/sas/views.py b/sas/views.py
index 74a34816..799258a8 100644
--- a/sas/views.py
+++ b/sas/views.py
@@ -36,28 +36,40 @@ from sas.forms import (
 from sas.models import Album, Picture
 
 
-class SASMainView(FormView):
-    form_class = SASForm
-    template_name = "sas/main.jinja"
-    success_url = reverse_lazy("sas:main")
+class AlbumCreateFragment(FragmentMixin, CreateView):
+    model = Album
+    form_class = AlbumCreateForm
+    template_name = "sas/fragments/album_create_form.jinja"
+    reload_on_redirect = True
 
-    def post(self, request, *args, **kwargs):
-        self.form = self.get_form()
-        parent = SithFile.objects.filter(id=settings.SITH_SAS_ROOT_DIR_ID).first()
-        files = request.FILES.getlist("images")
-        root = User.objects.filter(username="root").first()
-        if request.user.is_authenticated and request.user.is_in_group(
-            pk=settings.SITH_GROUP_SAS_ADMIN_ID
-        ):
-            if self.form.is_valid():
-                self.form.process(
-                    parent=parent, owner=root, files=files, automodere=True
-                )
-                if self.form.is_valid():
-                    return super().form_valid(self.form)
-        else:
-            self.form.add_error(None, _("You do not have the permission to do that"))
-        return self.form_invalid(self.form)
+    def get_form_kwargs(self):
+        return super().get_form_kwargs() | {"owner": self.request.user}
+
+    def render_fragment(
+        self, request, owner: User | None = None, **kwargs
+    ) -> SafeString:
+        self.object = None
+        self.owner = owner or self.request.user
+        return super().render_fragment(request, **kwargs)
+
+    def get_success_url(self):
+        parent = self.object.parent
+        parent.__class__ = Album
+        return parent.get_absolute_url()
+
+
+class SASMainView(UseFragmentsMixin, TemplateView):
+    template_name = "sas/main.jinja"
+
+    def get_fragments(self) -> dict[str, FragmentRenderer]:
+        form_init = {"parent": SithFile.objects.get(id=settings.SITH_SAS_ROOT_DIR_ID)}
+        return {
+            "album_create_fragment": AlbumCreateFragment.as_fragment(initial=form_init)
+        }
+
+    def get_fragment_data(self) -> dict[str, dict[str, Any]]:
+        root_user = User.objects.get(pk=settings.SITH_ROOT_USER_ID)
+        return {"album_create_fragment": {"owner": root_user}}
 
     def get_context_data(self, **kwargs):
         kwargs = super().get_context_data(**kwargs)
@@ -130,62 +142,45 @@ class AlbumUploadView(CanViewMixin, DetailView, FormMixin):
                 return HttpResponse(str(self.form.errors), status=200)
         return HttpResponse(str(self.form.errors), status=500)
 
-
-class AlbumView(CanViewMixin, DetailView, FormMixin):
+class AlbumView(CanViewMixin, UseFragmentsMixin, DetailView):
     model = Album
-    form_class = SASForm
     pk_url_kwarg = "album_id"
     template_name = "sas/album.jinja"
 
+    def get_fragments(self) -> dict[str, FragmentRenderer]:
+        return {
+            "album_create_fragment": AlbumCreateFragment.as_fragment(
+                initial={"parent": self.object}
+            )
+        }
+
     def dispatch(self, request, *args, **kwargs):
         try:
             self.asked_page = int(request.GET.get("page", 1))
         except ValueError as e:
             raise Http404 from e
-        return super().dispatch(request, *args, **kwargs)
-
-    def get(self, request, *args, **kwargs):
-        self.form = self.get_form()
         if "clipboard" not in request.session:
             request.session["clipboard"] = []
-        return super().get(request, *args, **kwargs)
+        return super().dispatch(request, *args, **kwargs)
 
     def post(self, request, *args, **kwargs):
         self.object = self.get_object()
         if not self.object.file:
             self.object.generate_thumbnail()
-        self.form = self.get_form()
-        if "clipboard" not in request.session:
-            request.session["clipboard"] = []
         if request.user.can_edit(self.object):  # Handle the copy-paste functions
             FileView.handle_clipboard(request, self.object)
-        parent = SithFile.objects.filter(id=self.object.id).first()
-        files = request.FILES.getlist("images")
-        if request.user.is_authenticated and request.user.is_subscribed:
-            if self.form.is_valid():
-                self.form.process(
-                    parent=parent,
-                    owner=request.user,
-                    files=files,
-                    automodere=request.user.is_in_group(
-                        pk=settings.SITH_GROUP_SAS_ADMIN_ID
-                    ),
-                )
-                if self.form.is_valid():
-                    return super().form_valid(self.form)
-        else:
-            self.form.add_error(None, _("You do not have the permission to do that"))
-        return self.form_invalid(self.form)
+        return HttpResponseRedirect(self.request.path)
 
-    def get_success_url(self):
-        return reverse("sas:album", kwargs={"album_id": self.object.id})
+    def get_fragment_data(self) -> dict[str, dict[str, Any]]:
+        return {"album_create_fragment": {"owner": self.request.user}}
 
     def get_context_data(self, **kwargs):
         kwargs = super().get_context_data(**kwargs)
-        kwargs["form"] = self.form
-        kwargs["clipboard"] = SithFile.objects.filter(
-            id__in=self.request.session["clipboard"]
-        )
+        if ids := self.request.session.get("clipboard", None):
+            kwargs["clipboard"] = SithFile.objects.filter(id__in=ids)
+        kwargs["upload_form"] = PictureUploadForm()
+        # if True, the albums will be fetched with a request to the API
+        # if False, the section won't be displayed at all
         kwargs["show_albums"] = (
             Album.objects.viewable_by(self.request.user)
             .filter(parent_id=self.object.id)