diff --git a/app/Config/Autoload.php b/app/Config/Autoload.php
index 1bc7dd61589f8134c67e3afb7434bb7ad63c751b..9436638053425efc6f1bf2a919fc19c9a3148547 100644
--- a/app/Config/Autoload.php
+++ b/app/Config/Autoload.php
@@ -46,6 +46,7 @@ class Autoload extends AutoloadConfig
         'Config' => APPPATH . 'Config',
         'ActivityPub' => APPPATH . 'Libraries/ActivityPub',
         'Analytics' => APPPATH . 'Libraries/Analytics',
+        'ViewComponents' => APPPATH . 'Libraries/ViewComponents',
     ];
 
     /**
@@ -84,5 +85,5 @@ class Autoload extends AutoloadConfig
      * ```
      * @var array<int, string>
      */
-    public $files = [];
+    public $files = [APPPATH . 'Libraries/ViewComponents/Helpers/view_components_helper.php'];
 }
diff --git a/app/Helpers/components_helper.php b/app/Helpers/components_helper.php
index 649114ce6a4345bec2e722fa93bb469d869885ee..a15db5112d1e28acd63d3931418a418b430903ab 100644
--- a/app/Helpers/components_helper.php
+++ b/app/Helpers/components_helper.php
@@ -110,7 +110,9 @@ if (! function_exists('button')) {
         CODE_SAMPLE;
     }
 }
+
 // ------------------------------------------------------------------------
+
 if (! function_exists('icon_button')) {
     /**
      * Icon Button component
@@ -145,6 +147,7 @@ if (! function_exists('icon_button')) {
     }
 }
 // ------------------------------------------------------------------------
+
 if (! function_exists('hint_tooltip')) {
     /**
      * Hint component
@@ -167,7 +170,9 @@ if (! function_exists('hint_tooltip')) {
         return $tooltip . '">' . icon('question') . '</span>';
     }
 }
+
 // ------------------------------------------------------------------------
+
 if (! function_exists('data_table')) {
     /**
      * Data table component
@@ -223,7 +228,9 @@ if (! function_exists('data_table')) {
             '</div>';
     }
 }
+
 // ------------------------------------------------------------------------
+
 if (! function_exists('publication_pill')) {
     /**
      * Publication pill component
@@ -250,7 +257,9 @@ if (! function_exists('publication_pill')) {
             '</span>';
     }
 }
+
 // ------------------------------------------------------------------------
+
 if (! function_exists('publication_button')) {
     /**
      * Publication button component
@@ -508,27 +517,5 @@ if (! function_exists('relative_time')) {
         CODE_SAMPLE;
     }
 }
-// ------------------------------------------------------------------------
-if (! function_exists('xml_editor')) {
-    /**
-     * XML Editor field
-     *
-     * @param array<string, mixed> $customData
-     * @param array<string, mixed> $extra
-     */
-    function xml_editor(array $customData = [], string $value = '', array $extra = []): string
-    {
-        $defaultData = [
-            'slot' => 'textarea',
-            'rows' => 5,
-        ];
-        $data = array_merge($defaultData, $customData);
 
-        $textarea = form_textarea($data, $value, $extra);
-
-        return <<<CODE_SAMPLE
-            <xml-editor>{$textarea}</time-ago>
-        CODE_SAMPLE;
-    }
-}
 // ------------------------------------------------------------------------
diff --git a/app/Helpers/form_helper.php b/app/Helpers/form_helper.php
index 49eb58026921d8e88cfcd5be52e0fc4b8cc35520..3ff660bd2c91f2d1a2a1d3b210b6887fd8422363 100644
--- a/app/Helpers/form_helper.php
+++ b/app/Helpers/form_helper.php
@@ -141,38 +141,12 @@ if (! function_exists('form_label')) {
 
 //--------------------------------------------------------------------
 
-if (! function_exists('form_multiselect')) {
-    /**
-     * Multi-select menu
-     *
-     * @param array<string, string> $options
-     * @param string[] $selected
-     * @param array<string, string> $customExtra
-     */
-    function form_multiselect(
-        string $name = '',
-        array $options = [],
-        array $selected = [],
-        array $customExtra = []
-    ): string {
-        $defaultExtra = [
-            'data-class' => $customExtra['class'],
-            'multiple' => 'multiple',
-        ];
-        $extra = array_merge($defaultExtra, $customExtra);
-
-        return form_dropdown($name, $options, $selected, $extra);
-    }
-}
-
-//--------------------------------------------------------------------
-
 if (! function_exists('form_dropdown')) {
     /**
      * Drop-down Menu (based on html select tag)
      *
      * @param array<string, mixed> $options
-     * @param string[] $selected
+     * @param array<string|int> $selected
      * @param array<string, mixed> $customExtra
      */
     function form_dropdown(
@@ -236,81 +210,3 @@ if (! function_exists('form_dropdown')) {
         return $form . "</select>\n";
     }
 }
-
-//--------------------------------------------------------------------
-
-if (! function_exists('form_editor')) {
-    /**
-     * Markdown editor
-     *
-     * @param array<string, mixed> $data
-     * @param array<string, mixed>|string $extra
-     */
-    function form_markdown_editor(array $data = [], string $value = '', string | array $extra = ''): string
-    {
-        $editorClass = 'w-full flex flex-col bg-white border border-gray-500 focus-within:ring-1 focus-within:ring-blue-600';
-        if (array_key_exists('class', $data) && $data['class'] !== '') {
-            $editorClass .= ' ' . $data['class'];
-            unset($data['class']);
-        }
-
-        $data['class'] = 'border-none outline-none focus:border-none focus:outline-none w-full h-full';
-
-        return '<div class="' . $editorClass . '">' .
-            '<header class="sticky top-0 z-20 flex flex-wrap justify-between bg-white border-b border-gray-500">' .
-                '<markdown-write-preview for="' . $data['id'] . '" class="relative inline-flex h-8">' .
-                    '<button type="button" slot="write" class="px-2 font-semibold focus:outline-none focus:ring-inset focus:ring-2 focus:ring-pine-600">' . lang(
-                        'Common.forms.editor.write'
-                    ) . '</button>' .
-                    '<button type="button" slot="preview" class="px-2 focus:outline-none focus:ring-inset focus:ring-2 focus:ring-pine-600">' . lang(
-                        'Common.forms.editor.preview'
-                    ) . '</button>' .
-                '</markdown-write-preview>' .
-                '<markdown-toolbar for="' . $data['id'] . '" class="flex gap-4 px-2 py-1">' .
-                    '<div class="inline-flex text-2xl gap-x-1">' .
-                        '<md-header class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
-                            'heading'
-                        ) . '</md-header>' .
-                        '<md-bold class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
-                            'bold'
-                        ) . '</md-bold>' .
-                        '<md-italic class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
-                            'italic'
-                        ) . '</md-italic>' .
-                    '</div>' .
-                    '<div class="inline-flex text-2xl gap-x-1">' .
-                        '<md-unordered-list class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
-                            'list-unordered'
-                        ) . '</md-unordered-list>' .
-                        '<md-ordered-list class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
-                            'list-ordered'
-                        ) . '</md-ordered-list>' .
-                    '</div>' .
-                    '<div class="inline-flex text-2xl gap-x-1">' .
-                        '<md-quote class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
-                            'quote'
-                        ) . '</md-quote>' .
-                        '<md-link class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
-                            'link'
-                        ) . '</md-link>' .
-                        '<md-image class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
-                            'image-add'
-                        ) . '</md-image>' .
-                    '</div>' .
-                '</markdown-toolbar>' .
-            '</header>' .
-            '<div class="relative">' .
-                form_textarea($data, $value, $extra) .
-                '<markdown-preview for="' . $data['id'] . '" class="absolute top-0 left-0 hidden w-full h-full p-2 overflow-y-auto prose bg-gray-50" showClass="bg-white"></markdown-preview>' .
-            '</div>' .
-            '<footer class="flex px-2 py-1 bg-gray-100 border-t">' .
-                '<a href="https://commonmark.org/help/" class="inline-flex items-center text-xs font-semibold text-gray-500 hover:text-gray-700" target="_blank" rel="noopener noreferrer">' . icon(
-                    'markdown',
-                    'mr-1 text-lg text-gray-400'
-                ) . lang('Common.forms.editor.help') . '</a>' .
-            '</footer>' .
-        '</div>';
-    }
-}
-
-// ------------------------------------------------------------------------
diff --git a/app/Libraries/ViewComponents/Component.php b/app/Libraries/ViewComponents/Component.php
new file mode 100644
index 0000000000000000000000000000000000000000..57873df5c96734c7b329935136a047980abe049b
--- /dev/null
+++ b/app/Libraries/ViewComponents/Component.php
@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+namespace ViewComponents;
+
+class Component implements ComponentInterface
+{
+    /**
+     * @var array<string, string>
+     */
+    protected array $attributes = [
+        'class' => '',
+    ];
+
+    /**
+     * @param array<string, mixed> $properties
+     * @param array<string, string> $attributes
+     */
+    public function __construct(
+        protected array $properties,
+        array $attributes
+    ) {
+        // overwrite default properties if set
+        foreach ($properties as $key => $value) {
+            $this->{$key} = $value;
+        }
+
+        $this->attributes = array_merge($this->attributes, $attributes);
+    }
+
+    public function render(): string
+    {
+        return static::class . ': RENDER METHOD NOT IMPLEMENTED';
+    }
+}
diff --git a/app/Libraries/ViewComponents/ComponentInterface.php b/app/Libraries/ViewComponents/ComponentInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..397bee874eca19ff0c0361ea55fa9f827bc59cea
--- /dev/null
+++ b/app/Libraries/ViewComponents/ComponentInterface.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace ViewComponents;
+
+interface ComponentInterface
+{
+    public function render(): string;
+}
diff --git a/app/Libraries/ViewComponents/ComponentLoader.php b/app/Libraries/ViewComponents/ComponentLoader.php
new file mode 100644
index 0000000000000000000000000000000000000000..9ec75d7b875fdc62bbb44c8afb36ab1ff107a80b
--- /dev/null
+++ b/app/Libraries/ViewComponents/ComponentLoader.php
@@ -0,0 +1,86 @@
+<?php
+
+declare(strict_types=1);
+
+namespace ViewComponents;
+
+use ViewComponents\Config\ViewComponents;
+use ViewComponents\Exceptions\ComponentNotFoundException;
+
+class ComponentLoader
+{
+    protected ViewComponents $config;
+
+    protected string $name;
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected array $properties = [];
+
+    /**
+     * @var array<string, string>
+     */
+    protected array $attributes = [];
+
+    public function __construct()
+    {
+        $this->config = config('ViewComponents');
+    }
+
+    public function __get(string $property): mixed
+    {
+        if (property_exists($this, $property)) {
+            return $this->{$property};
+        }
+    }
+
+    // @phpstan-ignore-next-line
+    public function __set(string $property, mixed $value)
+    {
+        if (property_exists($this, $property)) {
+            $this->{$property} = $value;
+        }
+
+        return $this;
+    }
+
+    /**
+     * @throws ComponentNotFoundException
+     */
+    public function load(): string
+    {
+        // first, check if there exists a component class to load in class components path
+        if (file_exists("{$this->config->classComponentsPath}/{$this->name}.php")) {
+            return $this->loadComponentClass();
+        }
+
+        // check for the existence of a view file if no component class has been found
+        // component view files are camel case
+        $camelCaseName = strtolower(preg_replace('~(?<!^)(?<!\/)[A-Z]~', '_$0', $this->name) ?? '');
+
+        if (file_exists("{$this->config->componentsViewPath}/{$camelCaseName}.php")) {
+            return $this->loadComponentView($camelCaseName);
+        }
+
+        throw new ComponentNotFoundException("Could not find component \"{$this->name}\"");
+    }
+
+    private function loadComponentClass(): string
+    {
+        $classComponentsNamespace = $this->config->classComponentsNamespace;
+
+        $namespacedName = str_replace('/', '\\', $this->name);
+        $componentClassNamespace = "{$classComponentsNamespace}\\{$namespacedName}";
+
+        $component = new $componentClassNamespace($this->properties, $this->attributes);
+        return $component->render();
+    }
+
+    private function loadComponentView(string $name): string
+    {
+        $viewData = [...$this->properties, ...$this->attributes];
+
+        return view("components/{$name}", $viewData);
+    }
+}
diff --git a/app/Libraries/ViewComponents/Config/Services.php b/app/Libraries/ViewComponents/Config/Services.php
new file mode 100644
index 0000000000000000000000000000000000000000..1a503731080dac94b736df727d6c214875def1ac
--- /dev/null
+++ b/app/Libraries/ViewComponents/Config/Services.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace ViewComponents\Config;
+
+use CodeIgniter\Config\BaseService;
+use ViewComponents\ComponentLoader;
+
+/**
+ * Services Configuration file.
+ *
+ * Services are simply other classes/libraries that the system uses to do its job. This is used by CodeIgniter to allow
+ * the core of the framework to be swapped out easily without affecting the usage within the rest of your application.
+ *
+ * This file holds any application-specific services, or service overrides that you might need. An example has been
+ * included with the general method format you should use for your service methods. For more examples, see the core
+ * Services file at system/Config/Services.php.
+ */
+class Services extends BaseService
+{
+    public static function viewcomponents(bool $getShared = true): ComponentLoader
+    {
+        if ($getShared) {
+            return self::getSharedInstance('viewcomponents');
+        }
+
+        return new ComponentLoader();
+    }
+}
diff --git a/app/Libraries/ViewComponents/Config/ViewComponents.php b/app/Libraries/ViewComponents/Config/ViewComponents.php
new file mode 100644
index 0000000000000000000000000000000000000000..2131991288949d71a6d982774b1667a3a4d49a4b
--- /dev/null
+++ b/app/Libraries/ViewComponents/Config/ViewComponents.php
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+
+namespace ViewComponents\Config;
+
+use CodeIgniter\Config\BaseConfig;
+
+class ViewComponents extends BaseConfig
+{
+    public string $classComponentsNamespace = APP_NAMESPACE . '\View\Components';
+
+    public string $classComponentsPath = APPPATH . 'View/Components';
+
+    public string $componentsViewPath = APPPATH . 'Views/components';
+}
diff --git a/app/Libraries/ViewComponents/Exceptions/ComponentNotFoundException.php b/app/Libraries/ViewComponents/Exceptions/ComponentNotFoundException.php
new file mode 100644
index 0000000000000000000000000000000000000000..5b7aaa76a5785d96a192eaaa617cd4f0e4e3457a
--- /dev/null
+++ b/app/Libraries/ViewComponents/Exceptions/ComponentNotFoundException.php
@@ -0,0 +1,12 @@
+<?php
+
+declare(strict_types=1);
+
+namespace ViewComponents\Exceptions;
+
+use CodeIgniter\Exceptions\ExceptionInterface;
+use RuntimeException;
+
+class ComponentNotFoundException extends RuntimeException implements ExceptionInterface
+{
+}
diff --git a/app/Libraries/ViewComponents/Helpers/view_components_helper.php b/app/Libraries/ViewComponents/Helpers/view_components_helper.php
new file mode 100644
index 0000000000000000000000000000000000000000..d6a9625d891691c51ca5e7d295f3180e8121d243
--- /dev/null
+++ b/app/Libraries/ViewComponents/Helpers/view_components_helper.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+if (! function_exists('component')) {
+    /**
+     * Loads the specified class or view file component in the parameters
+     *
+     * @param array<string, array<string, mixed>> $properties
+     * @param array<string, array<string, mixed>> $attributes
+     */
+    function component(string $name, array $properties = [], array $attributes = []): string
+    {
+        $componentLoader = service('viewcomponents');
+
+        $componentLoader->name = $name;
+        $componentLoader->properties = $properties;
+        $componentLoader->attributes = $attributes;
+
+        return $componentLoader->load();
+    }
+}
diff --git a/app/Resources/js/modules/xml-editor.ts b/app/Resources/js/modules/xml-editor.ts
index 6f521d5b1acabd4a8de35043338510a61b7f9b2f..81a8b254e240ff5b261f36e5899d6ebea8f31679 100644
--- a/app/Resources/js/modules/xml-editor.ts
+++ b/app/Resources/js/modules/xml-editor.ts
@@ -60,6 +60,10 @@ export class XMLEditor extends LitElement {
       border: 1px solid #6b7280;
       background-color: #ffffff;
     }
+    .cm-editor.cm-focused {
+      outline: 2px solid transparent;
+      box-shadow: 0 0 0 1px #2563eb;
+    }
   `;
 
   render(): TemplateResult<1> {
diff --git a/app/View/Components/Button.php b/app/View/Components/Button.php
new file mode 100644
index 0000000000000000000000000000000000000000..ac4416b2377a7e0cc498ff973eff9276c73fef12
--- /dev/null
+++ b/app/View/Components/Button.php
@@ -0,0 +1,98 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\View\Components;
+
+use ViewComponents\Component;
+
+class Button extends Component
+{
+    protected string $label = '';
+
+    protected string $uri = '';
+
+    protected string $variant = 'default';
+
+    protected string $size = 'base';
+
+    protected string $iconLeft = '';
+
+    protected string $iconRight = '';
+
+    protected bool $isSquared = false;
+
+    public function render(): string
+    {
+        $baseClass =
+            'inline-flex items-center font-semibold shadow-xs rounded-full focus:outline-none focus:ring';
+
+        $variantClass = [
+            'default' => 'text-black bg-gray-300 hover:bg-gray-400',
+            'primary' => 'text-white bg-pine-700 hover:bg-pine-800',
+            'secondary' => 'text-white bg-gray-700 hover:bg-gray-800',
+            'accent' => 'text-white bg-rose-600 hover:bg-rose-800',
+            'success' => 'text-white bg-green-600 hover:bg-green-700',
+            'danger' => 'text-white bg-red-600 hover:bg-red-700',
+            'warning' => 'text-black bg-yellow-500 hover:bg-yellow-600',
+            'info' => 'text-white bg-blue-500 hover:bg-blue-600',
+        ];
+
+        $sizeClass = [
+            'small' => 'text-xs md:text-sm',
+            'base' => 'text-sm md:text-base',
+            'large' => 'text-lg md:text-xl',
+        ];
+
+        $basePaddings = [
+            'small' => 'px-2 md:px-3 md:py-1',
+            'base' => 'px-3 py-1 md:px-4 md:py-2',
+            'large' => 'px-3 py-2 md:px-5',
+        ];
+
+        $squaredPaddings = [
+            'small' => 'p-1',
+            'base' => 'p-2',
+            'large' => 'p-3',
+        ];
+
+        $buttonClass =
+            $baseClass .
+            ' ' .
+            ($this->isSquared
+                ? $squaredPaddings[$this->size]
+                : $basePaddings[$this->size]) .
+            ' ' .
+            $sizeClass[$this->size] .
+            ' ' .
+            $variantClass[$this->variant];
+
+        if (array_key_exists('class', $this->attributes)) {
+            $buttonClass .= ' ' . $this->attributes['class'];
+            unset($this->attributes['class']);
+        }
+
+        if ($this->iconLeft !== '') {
+            $this->label = icon($this->iconLeft, 'mr-2') . $this->label;
+        }
+
+        if ($this->iconRight !== '') {
+            $this->label .= icon($this->iconRight, 'ml-2');
+        }
+
+        if ($this->uri !== '') {
+            return anchor($this->uri, $this->label, array_merge([
+                'class' => $buttonClass,
+            ], $this->attributes));
+        }
+
+        $defaultButtonAttributes = [
+            'type' => 'button',
+        ];
+        $attributes = stringify_attributes(array_merge($defaultButtonAttributes, $this->attributes));
+
+        return <<<CODE_SAMPLE
+            <button class="{$buttonClass}" {$attributes}>{$this->label}</button>
+        CODE_SAMPLE;
+    }
+}
diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php
new file mode 100644
index 0000000000000000000000000000000000000000..3dda5d57bf9f5d2e24b49afba757c6f490425a3f
--- /dev/null
+++ b/app/View/Components/Forms/Input.php
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\View\Components\Forms;
+
+use ViewComponents\Component;
+
+class Input extends Component
+{
+    public function render(): string
+    {
+        return '';
+    }
+}
diff --git a/app/View/Components/Forms/Label.php b/app/View/Components/Forms/Label.php
new file mode 100644
index 0000000000000000000000000000000000000000..ea43a51303012eedd47398d97882818dfcaecd40
--- /dev/null
+++ b/app/View/Components/Forms/Label.php
@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\View\Components\Forms;
+
+use ViewComponents\Component;
+
+class Label extends Component
+{
+    /**
+     * @var array<string, string>
+     */
+    protected array $attributes = [
+        'for' => '',
+        'name' => '',
+        'value' => '',
+        'class' => '',
+    ];
+
+    protected string $text = '';
+
+    protected string $hint = '';
+
+    protected bool $isOptional = false;
+
+    public function render(): string
+    {
+        $labelClass = $this->attributes['class'];
+        unset($this->attributes['class']);
+
+        $attributes = stringify_attributes($this->attributes);
+        $optionalText = $this->isOptional ? '<small class="ml-1 lowercase">(' .
+        lang('Common.optional') .
+        ')</small>' : '';
+        $hint = $this->hint !== '' ? hint_tooltip($this->hint, 'ml-1') : '';
+
+        return <<<CODE_SAMPLE
+            <label class="{$labelClass}" {$attributes}>{$this->text}{$optionalText}{$hint}</label>
+        CODE_SAMPLE;
+    }
+}
diff --git a/app/View/Components/Forms/MarkdownEditor.php b/app/View/Components/Forms/MarkdownEditor.php
new file mode 100644
index 0000000000000000000000000000000000000000..48bb716540da431048a52e268ea33bbdce4f2047
--- /dev/null
+++ b/app/View/Components/Forms/MarkdownEditor.php
@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\View\Components\Forms;
+
+use ViewComponents\Component;
+
+class MarkdownEditor extends Component
+{
+    protected string $content = '';
+
+    public function render(): string
+    {
+        $editorClass = 'w-full flex flex-col bg-white border border-gray-500 focus-within:ring-1 focus-within:ring-blue-600';
+        if ($this->attributes['class'] !== '') {
+            $editorClass .= ' ' . $this->attributes['class'];
+            unset($this->attributes['class']);
+        }
+
+        $this->attributes['class'] = 'border-none outline-none focus:border-none focus:outline-none w-full h-full';
+
+        return '<div class="' . $editorClass . '">' .
+            '<header class="sticky top-0 z-20 flex flex-wrap justify-between bg-white border-b border-gray-500">' .
+                '<markdown-write-preview for="' . $this->attributes['id'] . '" class="relative inline-flex h-8">' .
+                    '<button type="button" slot="write" class="px-2 font-semibold focus:outline-none focus:ring-inset focus:ring-2 focus:ring-pine-600">' . lang(
+                        'Common.forms.editor.write'
+                    ) . '</button>' .
+                    '<button type="button" slot="preview" class="px-2 focus:outline-none focus:ring-inset focus:ring-2 focus:ring-pine-600">' . lang(
+                        'Common.forms.editor.preview'
+                    ) . '</button>' .
+                '</markdown-write-preview>' .
+                '<markdown-toolbar for="' . $this->attributes['id'] . '" class="flex gap-4 px-2 py-1">' .
+                    '<div class="inline-flex text-2xl gap-x-1">' .
+                        '<md-header class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
+                            'heading'
+                        ) . '</md-header>' .
+                        '<md-bold class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
+                            'bold'
+                        ) . '</md-bold>' .
+                        '<md-italic class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
+                            'italic'
+                        ) . '</md-italic>' .
+                    '</div>' .
+                    '<div class="inline-flex text-2xl gap-x-1">' .
+                        '<md-unordered-list class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
+                            'list-unordered'
+                        ) . '</md-unordered-list>' .
+                        '<md-ordered-list class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
+                            'list-ordered'
+                        ) . '</md-ordered-list>' .
+                    '</div>' .
+                    '<div class="inline-flex text-2xl gap-x-1">' .
+                        '<md-quote class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
+                            'quote'
+                        ) . '</md-quote>' .
+                        '<md-link class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
+                            'link'
+                        ) . '</md-link>' .
+                        '<md-image class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon(
+                            'image-add'
+                        ) . '</md-image>' .
+                    '</div>' .
+                '</markdown-toolbar>' .
+            '</header>' .
+            '<div class="relative">' .
+                form_textarea($this->attributes, $this->content) .
+                '<markdown-preview for="' . $this->attributes['id'] . '" class="absolute top-0 left-0 hidden w-full h-full p-2 overflow-y-auto prose bg-gray-50" showClass="bg-white"></markdown-preview>' .
+            '</div>' .
+            '<footer class="flex px-2 py-1 bg-gray-100 border-t">' .
+                '<a href="https://commonmark.org/help/" class="inline-flex items-center text-xs font-semibold text-gray-500 hover:text-gray-700" target="_blank" rel="noopener noreferrer">' . icon(
+                    'markdown',
+                    'mr-1 text-lg text-gray-400'
+                ) . lang('Common.forms.editor.help') . '</a>' .
+            '</footer>' .
+        '</div>';
+    }
+}
diff --git a/app/View/Components/Forms/MultiSelect.php b/app/View/Components/Forms/MultiSelect.php
new file mode 100644
index 0000000000000000000000000000000000000000..ea9374b63ac851135aed6f150b64a7b161f8dbf0
--- /dev/null
+++ b/app/View/Components/Forms/MultiSelect.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\View\Components\Forms;
+
+use ViewComponents\Component;
+
+class MultiSelect extends Component
+{
+    /**
+     * @var array<string, string>
+     */
+    protected array $options = [];
+
+    /**
+     * @var string[]
+     */
+    protected array $selected = [];
+
+    public function render(): string
+    {
+        $defaultAttributes = [
+            'data-class' => $this->attributes['class'],
+            'multiple' => 'multiple',
+        ];
+        $extra = array_merge($defaultAttributes, $this->attributes);
+
+        return form_dropdown($this->attributes['name'], $this->options, $this->selected, $extra);
+    }
+}
diff --git a/app/View/Components/Forms/Select.php b/app/View/Components/Forms/Select.php
new file mode 100644
index 0000000000000000000000000000000000000000..ea9374b63ac851135aed6f150b64a7b161f8dbf0
--- /dev/null
+++ b/app/View/Components/Forms/Select.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\View\Components\Forms;
+
+use ViewComponents\Component;
+
+class MultiSelect extends Component
+{
+    /**
+     * @var array<string, string>
+     */
+    protected array $options = [];
+
+    /**
+     * @var string[]
+     */
+    protected array $selected = [];
+
+    public function render(): string
+    {
+        $defaultAttributes = [
+            'data-class' => $this->attributes['class'],
+            'multiple' => 'multiple',
+        ];
+        $extra = array_merge($defaultAttributes, $this->attributes);
+
+        return form_dropdown($this->attributes['name'], $this->options, $this->selected, $extra);
+    }
+}
diff --git a/app/View/Components/Forms/Toggler.php b/app/View/Components/Forms/Toggler.php
new file mode 100644
index 0000000000000000000000000000000000000000..f3421bfc17bd77fbfe9e7ed8cafa4a5e4a2fb836
--- /dev/null
+++ b/app/View/Components/Forms/Toggler.php
@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\View\Components\Forms;
+
+use ViewComponents\Component;
+
+/**
+ * Form Checkbox Switch
+ *
+ * Abstracts form_label to stylize it as a switch toggle
+ */
+class Toggler extends Component
+{
+    /**
+     * @var array<string, string>
+     */
+    protected array $attributes = [
+        'id' => '',
+        'name' => '',
+        'value' => '',
+        'class' => '',
+    ];
+
+    protected string $label = '';
+
+    protected string $hint = '';
+
+    protected bool $checked = false;
+
+    public function render(): string
+    {
+        $wrapperClass = $this->attributes['class'];
+        unset($this->attributes['class']);
+
+        $this->attributes['class'] = 'form-switch';
+
+        $checkbox = form_checkbox($this->attributes, $this->attributes['value'], $this->checked);
+        $hint = $this->hint !== '' ? hint_tooltip(lang('Podcast.form.lock_hint'), 'ml-1') : '';
+        return <<<CODE_SAMPLE
+            <label class="relative inline-flex items-center {$wrapperClass}">
+                {$checkbox}
+                <span class="form-switch-slider"></span>
+                <span class="ml-2">{$this->label}{$hint}</span>
+            </label>
+        CODE_SAMPLE;
+    }
+}
diff --git a/app/View/Components/Forms/XMLEditor.php b/app/View/Components/Forms/XMLEditor.php
new file mode 100644
index 0000000000000000000000000000000000000000..ef3406e47e60431ce815ecefdea9dfcf83327541
--- /dev/null
+++ b/app/View/Components/Forms/XMLEditor.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\View\Components\Forms;
+
+use ViewComponents\Component;
+
+class XMLEditor extends Component
+{
+    protected string $content = '';
+
+    /**
+     * @var array<string, string>
+     */
+    protected array $attributes = [
+        'slot' => 'textarea',
+        'rows' => '5',
+        'class' => 'textarea',
+    ];
+
+    public function render(): string
+    {
+        $textarea = form_textarea($this->attributes, $this->content);
+
+        return <<<CODE_SAMPLE
+            <xml-editor>{$textarea}</time-ago>
+        CODE_SAMPLE;
+    }
+}
diff --git a/app/View/Components/Icon.php b/app/View/Components/Icon.php
new file mode 100644
index 0000000000000000000000000000000000000000..b60051af445b307527c1a2962354e6663317aeaf
--- /dev/null
+++ b/app/View/Components/Icon.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\View\Components;
+
+use ViewComponents\Component;
+
+class Icon extends Component
+{
+    public string $glyph = '';
+
+    public function render(): string
+    {
+        $svgContents = file_get_contents('assets/icons/' . $this->glyph . '.svg');
+
+        if ($svgContents) {
+            if ($this->attributes['class'] !== '') {
+                $svgContents = str_replace('<svg', '<svg class="' . $this->attributes['class'] . '"', $svgContents);
+            }
+
+            return $svgContents;
+        }
+
+        return 'â–¡';
+    }
+}
diff --git a/app/Views/admin/contributor/add.php b/app/Views/admin/contributor/add.php
index 5c982f2d821c9572b61349085cd90851b4fb2fc1..fbe53d9adf56957ab94c42eb7b6fb89ebef8b35c 100644
--- a/app/Views/admin/contributor/add.php
+++ b/app/Views/admin/contributor/add.php
@@ -16,7 +16,7 @@
 ]) ?>
 <?= csrf_field() ?>
     
-<?= form_label(lang('Contributor.form.user'), 'user') ?>
+<?= component('Forms/Label', ['text' => lang('Contributor.form.user')], ['for' => 'user']) ?>
 <?= form_dropdown('user', $userOptions, [old('user', '')], [
     'id' => 'user',
     'class' => 'form-select mb-4',
@@ -24,7 +24,7 @@
     'placeholder' => lang('Contributor.form.user_placeholder')
 ]) ?>
 
-<?= form_label(lang('Contributor.form.role'), 'role') ?>
+<?= component('Forms/Label', ['text' => lang('Contributor.form.role')], ['for' => 'role']) ?>
 <?= form_dropdown('role', $roleOptions, [old('role', '')], [
     'id' => 'role',
     'class' => 'form-select mb-4',
diff --git a/app/Views/admin/contributor/edit.php b/app/Views/admin/contributor/edit.php
index a0be336a9523c32d851b4622b2f7932e70e3b525..221e9fb6b36186b417fd0c10309b6a01d627ad51 100644
--- a/app/Views/admin/contributor/edit.php
+++ b/app/Views/admin/contributor/edit.php
@@ -16,7 +16,7 @@
 ]) ?>
 <?= csrf_field() ?>
 
-<?= form_label(lang('Contributor.form.role'), 'role') ?>
+<?= component('Forms/Label', ['text' => lang('Contributor.form.role')], ['for' => 'role']) ?>
 <?= form_dropdown('role', $roleOptions, [old('role', $contributorGroupId)], [
     'id' => 'role',
     'class' => 'form-select mb-4',
diff --git a/app/Views/admin/episode/create.php b/app/Views/admin/episode/create.php
index 738b62272b7d874280d2464512576b92de856837..c5b885f347191690041f59eaa71ef2fcc7932b85 100644
--- a/app/Views/admin/episode/create.php
+++ b/app/Views/admin/episode/create.php
@@ -28,11 +28,10 @@
     lang('Episode.form.info_section_subtitle'),
 ) ?>
 
-<?= form_label(
-    lang('Episode.form.audio_file'),
-    'audio_file',
-    [],
-    lang('Episode.form.audio_file_hint'),
+<?= component(
+    'Forms/Label',
+    ['text' => lang('Episode.form.audio_file'), 'hint' => lang('Episode.form.audio_file_hint')],
+    ['for' => 'audio_file'],
 ) ?>
 <?= form_input([
     'id' => 'audio_file',
@@ -43,12 +42,14 @@
     'accept' => '.mp3,.m4a',
 ]) ?>
 
-<?= form_label(
-    lang('Episode.form.image'),
-    'image',
-    [],
-    lang('Episode.form.image_hint'),
-    true,
+<?= component(
+    'Forms/Label',
+    [   
+        'text' => lang('Episode.form.image'),
+        'hint' => lang('Episode.form.image_hint'),
+        'isOptional' => true
+    ],
+    ['for' => 'image'],
 ) ?>
 <?= form_input([
     'id' => 'image',
@@ -61,11 +62,11 @@
     'Common.forms.image_size_hint',
 ) ?></small>
 
-<?= form_label(
-    lang('Episode.form.title'),
-    'title',
-    [],
-    lang('Episode.form.title_hint'),
+<?= component(
+    'Forms/Label',
+    ['text' => lang('Episode.form.title'),
+     'hint' => lang('Episode.form.title_hint')],
+    ['for' => 'title'],
 ) ?>
 <?= form_input([
     'id' => 'title',
@@ -76,10 +77,10 @@
     'data-slugify' => 'title',
 ]) ?>
 
-<?= form_label(
-    lang('Episode.form.permalink'),
-    'slug',
-    [],
+<?= component(
+    'Forms/Label',
+    ['text' => lang('Episode.form.permalink')],
+    ['for' => 'slug']
 ) ?>
 <permalink-edit class="inline-flex items-center mb-4 text-xs" edit-label="<?= lang('Common.edit') ?>" copy-label="<?= lang('Common.copy') ?>" copied-label="<?= lang('Common.copied') ?>">
     <span slot="domain"><?= base_url('/@'. $podcast->handle . '/episodes' ) . '/' ?></span>
@@ -96,7 +97,7 @@
 
 <div class="flex flex-col mb-4 gap-x-2 gap-y-4 md:flex-row">
     <div class="flex flex-col flex-1">
-        <?= form_label(lang('Episode.form.season_number'), 'season_number') ?>
+        <?= component('Forms/Label', ['text' => lang('Episode.form.season_number')], ['for' => 'season_number']) ?>
         <?= form_input([
             'id' => 'season_number',
             'name' => 'season_number',
@@ -106,7 +107,7 @@
         ]) ?>
     </div>
     <div class="flex flex-col flex-1">
-        <?= form_label(lang('Episode.form.episode_number'), 'episode_number') ?>
+        <?= component('Forms/Label', ['text' => lang('Episode.form.episode_number')], ['for' => 'episode_number']) ?>
         <?= form_input([
             'id' => 'episode_number',
             'name' => 'episode_number',
@@ -203,36 +204,45 @@
 ) ?>
 
 <div class="mb-4">
-    <?= form_label(lang('Episode.form.description'), 'description') ?>
-    <?= form_markdown_editor(
+    <?= component('Forms/Label', ['text' => lang('Episode.form.description')], ['for' => 'description']) ?>
+    <?= component(
+        'Forms/MarkdownEditor',
+        [
+            'content' => old('description', '', false),
+        ],
         [
             'id' => 'description',
             'name' => 'description',
             'required' => 'required',
         ],
-        old('description', '', false),
     ) ?>
 </div>
 
 <div class="mb-4">
-    <?= form_label(
-        lang('Episode.form.description_footer'),
-        'description_footer',
-        [],
-        lang('Episode.form.description_footer_hint'),
-        true
+    <?= component( 'Forms/Label',
+    [
+        'text' => lang('Episode.form.description_footer'),
+        'hint' =>  lang('Episode.form.description_footer_hint'),
+        'isOptional' => true
+    ],
+    [
+        'for' => 'description_footer'
+    ],
     ) ?>
-    <?= form_markdown_editor(
+    <?= component(
+        'Forms/MarkdownEditor',
+        [
+            'content' => old(
+                'description_footer',
+                $podcast->episode_description_footer_markdown ?? '',
+                false,
+            ),
+        ],
         [
             'id' => 'description_footer',
             'name' => 'description_footer',
             'rows' => 6
         ],
-        old(
-            'description_footer',
-            $podcast->episode_description_footer_markdown ?? '',
-            false,
-        ),
     ) ?>
 </div>
 
@@ -243,12 +253,13 @@
     lang('Episode.form.location_section_subtitle'),
 ) ?>
 
-<?= form_label(
-    lang('Episode.form.location_name'),
-    'location_name',
-    [],
-    lang('Episode.form.location_name_hint'),
-    true,
+<?= component( 'Forms/Label',
+    [
+        'text' => lang('Episode.form.location_name'),
+        'hint' => lang('Episode.form.location_name_hint'),
+        'isOptional' => true
+    ],
+    ['for' => 'location_name'],
 ) ?>
 <?= form_input([
     'id' => 'location_name',
@@ -290,12 +301,12 @@
 
         <div class="py-2 tab-panels">
             <section id="transcript-file-upload" class="flex items-center tab-panel">
-            <?= form_label(
-                lang('Episode.form.transcript_file'),
-                'transcript_file',
-                ['class' => 'sr-only'],
-                lang('Episode.form.transcript_file'),
-                true,
+            <?= component( 'Forms/Label',
+                [
+                    'text' => lang('Episode.form.transcript_file'),
+                    'isOptional' => true    
+                ],
+                ['for' => 'transcript_file', 'class' => 'sr-only'],
             ) ?>
             <?= form_input([
                 'id' => 'transcript_file',
@@ -306,12 +317,12 @@
             ]) ?>
             </section>
             <section id="transcript-file-remote-url" class="tab-panel">
-            <?= form_label(
-                lang('Episode.form.transcript_file_remote_url'),
-                'transcript_file_remote_url',
-                ['class' => 'sr-only'],
-                lang('Episode.form.transcript_file_remote_url'),
-                true,
+            <?= component( 'Forms/Label',
+                [
+                    'text' => lang('Episode.form.transcript_file_remote_url'),
+                    'isOptional' => true
+                ],
+                ['for' => 'transcript_file_remote_url', 'class' => 'sr-only'],
             ) ?>
             <?= form_input([
                 'id' => 'transcript_file_remote_url',
@@ -353,12 +364,12 @@
 
         <div class="py-2 tab-panels">
             <section id="chapters-file-upload" class="flex items-center tab-panel">
-            <?= form_label(
-                lang('Episode.form.chapters_file'),
-                'chapters_file',
-                ['class' => 'sr-only'],
-                lang('Episode.form.chapters_file'),
-                true,
+            <?= component( 'Forms/Label',
+                [
+                    'text' => lang('Episode.form.chapters_file'),
+                    'isOptional' => true
+                ],
+                ['for' => 'chapters_file', 'class' => 'sr-only'],
             ) ?>
             <?= form_input([
                 'id' => 'chapters_file',
@@ -369,12 +380,15 @@
             ]) ?>
             </section>
             <section id="chapters-file-remote-url" class="tab-panel">
-            <?= form_label(
-                lang('Episode.form.chapters_file_remote_url'),
-                'chapters_file_remote_url',
-                ['class' => 'sr-only'],
-                lang('Episode.form.chapters_file_remote_url'),
-                true,
+            <?= component( 'Forms/Label',
+                [
+                    'text' =>  lang('Episode.form.chapters_file_remote_url'),
+                    'isOptional' => true
+                ],
+                [
+                    'for' => 'chapters_file_remote_url',
+                    'class' => 'sr-only'
+                ],
             ) ?>
             <?= form_input([
                 'id' => 'chapters_file_remote_url',
@@ -395,27 +409,37 @@
     lang('Episode.form.advanced_section_title'),
     lang('Episode.form.advanced_section_subtitle'),
 ) ?>
-<?= form_label(
-    lang('Episode.form.custom_rss'),
-    'custom_rss',
-    [],
-    lang('Episode.form.custom_rss_hint'),
-    true,
+<?= component( 'Forms/Label',
+    [
+        'text' => lang('Episode.form.custom_rss'),
+    'hint' => lang('Episode.form.custom_rss_hint'),
+    'isOptional' => true
+    ],
+    ['for' => 'custom_rss']
+) ?>
+<?= component('Forms/XMLEditor',
+    [
+        'content' => old('custom_rss', '')
+    ],
+    [
+        'id' => 'custom_rss',
+        'name' => 'custom_rss',
+    ]
 ) ?>
-<?= xml_editor([
-    'id' => 'custom_rss',
-    'name' => 'custom_rss',
-    'class' => 'form-textarea',
-    'value' => old('custom_rss'),
-]) ?>
 <?= form_section_close() ?>
 
-<?= form_switch(
-    lang('Episode.form.block') .
-        hint_tooltip(lang('Episode.form.block_hint'), 'ml-1'),
-    ['id' => 'block', 'name' => 'block'],
-    'yes',
-    old('block', false),
+<?= component(
+    'Forms/Toggler',
+    [
+        'label' => lang('Episode.form.block'),
+        'hint' => lang('Episode.form.block_hint')
+    ],
+    [
+        'id' => 'block',
+        'name' => 'block',
+        'value' => 'yes',
+        'checked' => old('block', false),
+    ]
 ) ?>
 
 <?= button(
diff --git a/app/Views/admin/episode/edit.php b/app/Views/admin/episode/edit.php
index 1fd1e87d2cfc709ee97819f418dd39997095757a..863d8cfaaa28a267e23fc5b36a856f738076d1b7 100644
--- a/app/Views/admin/episode/edit.php
+++ b/app/Views/admin/episode/edit.php
@@ -18,8 +18,8 @@
 <?= csrf_field() ?>
 
 <div class="inline-flex w-full p-2 mb-4 text-sm font-semibold text-yellow-800 bg-red-100 border border-red-300 rounded" role="alert">
-  <?= icon('alert', 'mr-2 text-lg flex-shrink-0') .
-      lang('Episode.form.warning') ?>
+    <?= icon('alert', 'mr-2 text-lg flex-shrink-0') .
+        lang('Episode.form.warning') ?>
 </div>
 
 <?= form_section(
@@ -35,11 +35,11 @@
 />',
 ) ?>
 
-<?= form_label(
-    lang('Episode.form.audio_file'),
-    'audio_file',
-    [],
-    lang('Episode.form.audio_file_hint'),
+<?= component(
+    'Forms/Label',
+    ['text' =>
+    lang('Episode.form.audio_file'), 'hint' => lang('Episode.form.audio_file_hint'),],
+    ['for' => 'audio_file'],
 ) ?>
 <?= form_input([
     'id' => 'audio_file',
@@ -49,12 +49,12 @@
     'accept' => '.mp3,.m4a',
 ]) ?>
 
-<?= form_label(
-    lang('Episode.form.image'),
-    'image',
-    [],
-    lang('Episode.form.image_hint'),
-    true,
+<?= component(
+    'Forms/Label',
+    ['text' =>
+    lang('Episode.form.image'), 'hint' => lang('Episode.form.image_hint'), 'isOptional' =>     true,],
+    ['for' =>
+    'image',]
 ) ?>
 
 <?= form_input([
@@ -68,11 +68,12 @@
     'Common.forms.image_size_hint',
 ) ?></small>
 
-<?= form_label(
-    lang('Episode.form.title'),
-    'title',
-    [],
-    lang('Episode.form.title_hint'),
+<?= component(
+    'Forms/Label',
+    ['text' =>
+    lang('Episode.form.title'), 'hint' => lang('Episode.form.title_hint'),],
+    ['for' =>
+    'title',]
 ) ?>
 <?= form_input([
     'id' => 'title',
@@ -83,27 +84,29 @@
     'data-slugify' => 'title',
 ]) ?>
 
-<?= form_label(
-    lang('Episode.form.permalink'),
-    'slug',
-    [],
+<?= component(
+    'Forms/Label',
+    [
+        'text' => lang('Episode.form.permalink')
+    ],
+    ['for' => 'slug',]
 ) ?>
 <permalink-edit class="inline-flex items-center mb-4 text-xs" edit-label="<?= lang('Common.edit') ?>" copy-label="<?= lang('Common.copy') ?>" copied-label="<?= lang('Common.copied') ?>">
-    <span slot="domain"><?= base_url('/@'. $podcast->handle . '/episodes' ) . '/' ?></span>
+    <span slot="domain"><?= base_url('/@' . $podcast->handle . '/episodes') . '/' ?></span>
     <?= form_input([
-    'id' => 'slug',
-    'name' => 'slug',
-    'class' => 'form-input flex-1 w-0 text-xs',
-    'value' => old('slug', $episode->slug),
-    'required' => 'required',
-    'data-slugify' => 'slug',
-    'slot' => 'slug-input'
+        'id' => 'slug',
+        'name' => 'slug',
+        'class' => 'form-input flex-1 w-0 text-xs',
+        'value' => old('slug', $episode->slug),
+        'required' => 'required',
+        'data-slugify' => 'slug',
+        'slot' => 'slug-input'
     ]) ?>
 </permalink-edit>
 
 <div class="flex flex-col mb-4 gap-x-2 gap-y-4 md:flex-row">
     <div class="flex flex-col flex-1">
-        <?= form_label(lang('Episode.form.season_number'), 'season_number') ?>
+        <?= component('Forms/Label', ['text' => lang('Episode.form.season_number')], ['for' => 'season_number']) ?>
         <?= form_input([
             'id' => 'season_number',
             'name' => 'season_number',
@@ -113,7 +116,7 @@
         ]) ?>
     </div>
     <div class="flex flex-col flex-1">
-        <?= form_label(lang('Episode.form.episode_number'), 'episode_number') ?>
+        <?= component('Forms/Label', ['text' => lang('Episode.form.episode_number')], ['for' => 'episode_number']) ?>
         <?= form_input([
             'id' => 'episode_number',
             'name' => 'episode_number',
@@ -125,83 +128,83 @@
 </div>
 
 <?= form_fieldset('', ['class' => 'flex mb-4 gap-1']) ?>
-    <legend>
+<legend>
     <?= lang('Episode.form.type.label') .
         hint_tooltip(lang('Episode.form.type.hint'), 'ml-1') ?>
-    </legend>
-    <?= form_radio(
-        ['id' => 'full', 'name' => 'type', 'class' => 'form-radio-btn'],
-        'full',
-        old('type') ? old('type') === 'full' : $episode->type === 'full',
-    ) ?>
-    <label for="full" class="inline-flex items-center">
-        <?= lang('Episode.form.type.full') ?>
-    </label>
-    <?= form_radio(
-        ['id' => 'trailer', 'name' => 'type', 'class' => 'form-radio-btn'],
-        'trailer',
-        old('type') ? old('type') === 'trailer' : $episode->type === 'trailer',
-    ) ?>
-    <label for="trailer" class="inline-flex items-center">
-        <?= lang('Episode.form.type.trailer') ?>
-    </label>
-    <?= form_radio(
-        ['id' => 'bonus', 'name' => 'type', 'class' => 'form-radio-btn'],
-        'bonus',
-        old('type') ? old('type') === 'bonus' : $episode->type === 'bonus',
-    ) ?>
-    <label for="bonus" class="inline-flex items-center">
-        <?= lang('Episode.form.type.bonus') ?>
-    </label>
+</legend>
+<?= form_radio(
+    ['id' => 'full', 'name' => 'type', 'class' => 'form-radio-btn'],
+    'full',
+    old('type') ? old('type') === 'full' : $episode->type === 'full',
+) ?>
+<label for="full" class="inline-flex items-center">
+    <?= lang('Episode.form.type.full') ?>
+</label>
+<?= form_radio(
+    ['id' => 'trailer', 'name' => 'type', 'class' => 'form-radio-btn'],
+    'trailer',
+    old('type') ? old('type') === 'trailer' : $episode->type === 'trailer',
+) ?>
+<label for="trailer" class="inline-flex items-center">
+    <?= lang('Episode.form.type.trailer') ?>
+</label>
+<?= form_radio(
+    ['id' => 'bonus', 'name' => 'type', 'class' => 'form-radio-btn'],
+    'bonus',
+    old('type') ? old('type') === 'bonus' : $episode->type === 'bonus',
+) ?>
+<label for="bonus" class="inline-flex items-center">
+    <?= lang('Episode.form.type.bonus') ?>
+</label>
 <?= form_fieldset_close() ?>
 
 <?= form_fieldset('', ['class' => 'mb-6']) ?>
-    <legend>
+<legend>
     <?= lang('Episode.form.parental_advisory.label') .
         hint_tooltip(lang('Episode.form.parental_advisory.hint'), 'ml-1') ?>
-    </legend>
-    <?= form_radio(
-        [
-            'id' => 'undefined',
-            'name' => 'parental_advisory',
-            'class' => 'form-radio-btn',
-        ],
-        'undefined',
-        old('parental_advisory')
-            ? old('parental_advisory') === 'undefined'
-            : $episode->parental_advisory === null,
-    ) ?>
-    <label for="undefined"><?= lang(
-        'Episode.form.parental_advisory.undefined',
-    ) ?></label>
-    <?= form_radio(
-        [
-            'id' => 'clean',
-            'name' => 'parental_advisory',
-            'class' => 'form-radio-btn',
-        ],
-        'clean',
-        old('parental_advisory')
-            ? old('parental_advisory') === 'clean'
-            : $episode->parental_advisory === 'clean',
-    ) ?>
-    <label for="clean"><?= lang(
-        'Episode.form.parental_advisory.clean',
-    ) ?></label>
-    <?= form_radio(
-        [
-            'id' => 'explicit',
-            'name' => 'parental_advisory',
-            'class' => 'form-radio-btn',
-        ],
-        'explicit',
-        old('parental_advisory')
-            ? old('parental_advisory') === 'explicit'
-            : $episode->parental_advisory === 'explicit',
-    ) ?>
-    <label for="explicit"><?= lang(
-        'Episode.form.parental_advisory.explicit',
-    ) ?></label>
+</legend>
+<?= form_radio(
+    [
+        'id' => 'undefined',
+        'name' => 'parental_advisory',
+        'class' => 'form-radio-btn',
+    ],
+    'undefined',
+    old('parental_advisory')
+        ? old('parental_advisory') === 'undefined'
+        : $episode->parental_advisory === null,
+) ?>
+<label for="undefined"><?= lang(
+                            'Episode.form.parental_advisory.undefined',
+                        ) ?></label>
+<?= form_radio(
+    [
+        'id' => 'clean',
+        'name' => 'parental_advisory',
+        'class' => 'form-radio-btn',
+    ],
+    'clean',
+    old('parental_advisory')
+        ? old('parental_advisory') === 'clean'
+        : $episode->parental_advisory === 'clean',
+) ?>
+<label for="clean"><?= lang(
+                        'Episode.form.parental_advisory.clean',
+                    ) ?></label>
+<?= form_radio(
+    [
+        'id' => 'explicit',
+        'name' => 'parental_advisory',
+        'class' => 'form-radio-btn',
+    ],
+    'explicit',
+    old('parental_advisory')
+        ? old('parental_advisory') === 'explicit'
+        : $episode->parental_advisory === 'explicit',
+) ?>
+<label for="explicit"><?= lang(
+                            'Episode.form.parental_advisory.explicit',
+                        ) ?></label>
 <?= form_fieldset_close() ?>
 
 <?= form_section_close() ?>
@@ -213,36 +216,43 @@
 ) ?>
 
 <div class="mb-4">
-    <?= form_label(lang('Episode.form.description'), 'description') ?>
-    <?= form_markdown_editor(
+    <?= component('Forms/Label', ['text' => lang('Episode.form.description')], ['for' => 'description']) ?>
+    <?= component(
+        'Forms/MarkdownEditor',
+        [
+            'content' => old('description', $episode->description_markdown, false),
+        ],
         [
             'id' => 'description',
             'name' => 'description',
             'required' => 'required',
         ],
-        old('description', $episode->description_markdown, false),
     ) ?>
 </div>
 
 <div class="mb-4">
-    <?= form_label(
-        lang('Episode.form.description_footer'),
-        'description_footer',
-        [],
-        lang('Episode.form.description_footer_hint'),
-        true
+    <?= component('Forms/Label', 
+        [
+            'text' => lang('Episode.form.description_footer'),
+            'hint' => lang('Episode.form.description_footer_hint'),
+            'isOptional' => true
+        ],
+        ['for' => 'description_footer'],
     ) ?>
-    <?= form_markdown_editor(
+    <?= component(
+        'Forms/MarkdownEditor',
+        [
+            'content' => old(
+                'description_footer',
+                $podcast->episode_description_footer_markdown ?? '',
+                false,
+            ),
+        ],
         [
             'id' => 'description_footer',
             'name' => 'description_footer',
             'rows' => 6
         ],
-        old(
-            'description_footer',
-            $podcast->episode_description_footer_markdown ?? '',
-            false,
-        ),
     ) ?>
 </div>
 
@@ -253,12 +263,13 @@
     lang('Episode.form.location_section_subtitle'),
 ) ?>
 
-<?= form_label(
-    lang('Episode.form.location_name'),
-    'location_name',
-    [],
-    lang('Episode.form.location_name_hint'),
-    true,
+<?= component('Forms/Label',
+    [
+        'text' => lang('Episode.form.location_name'),
+        'hint' => lang('Episode.form.location_name_hint'),
+        'isOptional' => true
+    ],
+    ['for' => 'location_name']
 ) ?>
 <?= form_input([
     'id' => 'location_name',
@@ -273,34 +284,34 @@
     lang('Episode.form.additional_files_section_title'),
     lang('Episode.form.additional_files_section_subtitle', [
         'podcastNamespaceLink' =>
-            '“<a href="https://github.com/Podcastindex-org/podcast-namespace" target="_blank" rel="noreferrer noopener" style="text-decoration: underline;">podcast namespace</a>”',
+        '“<a href="https://github.com/Podcastindex-org/podcast-namespace" target="_blank" rel="noreferrer noopener" style="text-decoration: underline;">podcast namespace</a>”',
     ]),
 ) ?>
 
 <?= form_fieldset('', ['class' => 'flex flex-col mb-4']) ?>
-    <legend><?= lang('Episode.form.transcript') .
-        '<small class="ml-1 lowercase">(' .
-        lang('Common.optional') .
-        ')</small>' .
-        hint_tooltip(lang('Episode.form.transcript_hint'), 'ml-1') ?></legend>
-    <div class="mb-4 form-input-tabs">
-        <input type="radio" name="transcript-choice" id="transcript-file-upload-choice" aria-controls="transcript-file-upload-choice" value="upload-file" <?= $episode->transcript_file_remote_url
-            ? ''
-            : 'checked' ?> />
-        <label for="transcript-file-upload-choice"><?= lang(
-            'Common.forms.upload_file',
-        ) ?></label>
-
-        <input type="radio" name="transcript-choice" id="transcript-file-remote-url-choice" aria-controls="transcript-file-remote-url-choice" value="remote-url" <?= $episode->transcript_file_remote_url
-            ? 'checked'
-            : '' ?> />
-        <label for="transcript-file-remote-url-choice"><?= lang(
-            'Common.forms.remote_url',
-        ) ?></label>
-
-        <div class="py-2 tab-panels">
-            <section id="transcript-file-upload" class="flex items-center tab-panel">
-            <?php if ($episode->transcript_file): ?>
+<legend><?= lang('Episode.form.transcript') .
+            '<small class="ml-1 lowercase">(' .
+            lang('Common.optional') .
+            ')</small>' .
+            hint_tooltip(lang('Episode.form.transcript_hint'), 'ml-1') ?></legend>
+<div class="mb-4 form-input-tabs">
+    <input type="radio" name="transcript-choice" id="transcript-file-upload-choice" aria-controls="transcript-file-upload-choice" value="upload-file" <?= $episode->transcript_file_remote_url
+                                                                                                                                                            ? ''
+                                                                                                                                                            : 'checked' ?> />
+    <label for="transcript-file-upload-choice"><?= lang(
+                                                    'Common.forms.upload_file',
+                                                ) ?></label>
+
+    <input type="radio" name="transcript-choice" id="transcript-file-remote-url-choice" aria-controls="transcript-file-remote-url-choice" value="remote-url" <?= $episode->transcript_file_remote_url
+                                                                                                                                                                    ? 'checked'
+                                                                                                                                                                    : '' ?> />
+    <label for="transcript-file-remote-url-choice"><?= lang(
+                                                        'Common.forms.remote_url',
+                                                    ) ?></label>
+
+    <div class="py-2 tab-panels">
+        <section id="transcript-file-upload" class="flex items-center tab-panel">
+            <?php if ($episode->transcript_file) : ?>
                 <div class="flex justify-between">
                     <?= anchor(
                         $episode->transcript_file_url,
@@ -321,7 +332,7 @@
                             icon('delete-bin', 'mx-auto'),
                             [
                                 'class' =>
-                                    'p-1 bg-red-200 rounded-full text-red-700 hover:text-red-900',
+                                'p-1 bg-red-200 rounded-full text-red-700 hover:text-red-900',
                                 'data-toggle' => 'tooltip',
                                 'data-placement' => 'bottom',
                                 'title' => lang(
@@ -331,12 +342,12 @@
                         ) ?>
                 </div>
             <?php endif; ?>
-            <?= form_label(
-                lang('Episode.form.transcript_file'),
-                'transcript_file',
-                ['class' => 'sr-only'],
-                lang('Episode.form.transcript_file'),
-                true,
+            <?= component( 'Forms/Label',
+                [
+                    'text' => lang('Episode.form.transcript_file'),
+                    'isOptional' => true    
+                ],
+                ['for' => 'transcript_file', 'class' => 'sr-only'],
             ) ?>
             <?= form_input([
                 'id' => 'transcript_file',
@@ -345,14 +356,14 @@
                 'type' => 'file',
                 'accept' => '.txt,.html,.srt,.json',
             ]) ?>
-            </section>
-            <section id="transcript-file-remote-url" class="tab-panel">
-            <?= form_label(
-                lang('Episode.form.transcript_file_remote_url'),
-                'transcript_file_remote_url',
-                ['class' => 'sr-only'],
-                lang('Episode.form.transcript_file_remote_url'),
-                true,
+        </section>
+        <section id="transcript-file-remote-url" class="tab-panel">
+            <?= component( 'Forms/Label',
+                [
+                    'text' => lang('Episode.form.transcript_file_remote_url'),
+                    'isOptional' => true
+                ],
+                ['for' => 'transcript_file_remote_url', 'class' => 'sr-only'],
             ) ?>
             <?= form_input([
                 'id' => 'transcript_file_remote_url',
@@ -365,35 +376,35 @@
                     $episode->transcript_file_remote_url,
                 ),
             ]) ?>
-            </section>
-        </div>
+        </section>
     </div>
+</div>
 <?= form_fieldset_close() ?>
 
 <?= form_fieldset('', ['class' => 'flex flex-col mb-4']) ?>
-    <legend><?= lang('Episode.form.chapters') .
-        '<small class="ml-1 lowercase">(' .
-        lang('Common.optional') .
-        ')</small>' .
-        hint_tooltip(lang('Episode.form.chapters_hint'), 'ml-1') ?></legend>
-    <div class="mb-4 form-input-tabs">
-        <input type="radio" name="chapters-choice" id="chapters-file-upload-choice" aria-controls="chapters-file-upload-choice" value="upload-file" <?= $episode->chapters_file_remote_url
-            ? ''
-            : 'checked' ?> />
-        <label for="chapters-file-upload-choice"><?= lang(
-            'Common.forms.upload_file',
-        ) ?></label>
-
-        <input type="radio" name="chapters-choice" id="chapters-file-remote-url-choice" aria-controls="chapters-file-remote-url-choice" value="remote-url" <?= $episode->chapters_file_remote_url
-            ? 'checked'
-            : '' ?> />
-        <label for="chapters-file-remote-url-choice"><?= lang(
-            'Common.forms.remote_url',
-        ) ?></label>
-
-        <div class="py-2 tab-panels">
-            <section id="chapters-file-upload" class="flex items-center tab-panel">
-            <?php if ($episode->chapters_file): ?>
+<legend><?= lang('Episode.form.chapters') .
+            '<small class="ml-1 lowercase">(' .
+            lang('Common.optional') .
+            ')</small>' .
+            hint_tooltip(lang('Episode.form.chapters_hint'), 'ml-1') ?></legend>
+<div class="mb-4 form-input-tabs">
+    <input type="radio" name="chapters-choice" id="chapters-file-upload-choice" aria-controls="chapters-file-upload-choice" value="upload-file" <?= $episode->chapters_file_remote_url
+                                                                                                                                                    ? ''
+                                                                                                                                                    : 'checked' ?> />
+    <label for="chapters-file-upload-choice"><?= lang(
+                                                    'Common.forms.upload_file',
+                                                ) ?></label>
+
+    <input type="radio" name="chapters-choice" id="chapters-file-remote-url-choice" aria-controls="chapters-file-remote-url-choice" value="remote-url" <?= $episode->chapters_file_remote_url
+                                                                                                                                                            ? 'checked'
+                                                                                                                                                            : '' ?> />
+    <label for="chapters-file-remote-url-choice"><?= lang(
+                                                        'Common.forms.remote_url',
+                                                    ) ?></label>
+
+    <div class="py-2 tab-panels">
+        <section id="chapters-file-upload" class="flex items-center tab-panel">
+            <?php if ($episode->chapters_file) : ?>
                 <div class="flex justify-between">
                     <?= anchor(
                         $episode->chapters_file_url,
@@ -413,7 +424,7 @@
                             icon('delete-bin', 'mx-auto'),
                             [
                                 'class' =>
-                                    'p-1 bg-red-200 rounded-full text-red-700 hover:text-red-900',
+                                'p-1 bg-red-200 rounded-full text-red-700 hover:text-red-900',
                                 'data-toggle' => 'tooltip',
                                 'data-placement' => 'bottom',
                                 'title' => lang(
@@ -423,12 +434,12 @@
                         ) ?>
                 </div>
             <?php endif; ?>
-            <?= form_label(
-                lang('Episode.form.chapters_file'),
-                'chapters_file',
-                ['class' => 'sr-only'],
-                lang('Episode.form.chapters_file'),
-                true,
+            <?= component( 'Forms/Label',
+                [
+                    'text' => lang('Episode.form.chapters_file'),
+                    'isOptional' => true
+                ],
+                ['for' => 'chapters_file', 'class' => 'sr-only'],
             ) ?>
             <?= form_input([
                 'id' => 'chapters_file',
@@ -437,14 +448,17 @@
                 'type' => 'file',
                 'accept' => '.json',
             ]) ?>
-            </section>
-            <section id="chapters-file-remote-url" class="tab-panel">
-            <?= form_label(
-                lang('Episode.form.chapters_file_remote_url'),
-                'chapters_file_remote_url',
-                ['class' => 'sr-only'],
-                lang('Episode.form.chapters_file_remote_url'),
-                true,
+        </section>
+        <section id="chapters-file-remote-url" class="tab-panel">
+        <?= component( 'Forms/Label',
+                [
+                    'text' =>  lang('Episode.form.chapters_file_remote_url'),
+                    'isOptional' => true
+                ],
+                [
+                    'for' => 'chapters_file_remote_url',
+                    'class' => 'sr-only'
+                ],
             ) ?>
             <?= form_input([
                 'id' => 'chapters_file_remote_url',
@@ -457,9 +471,9 @@
                     $episode->chapters_file_remote_url,
                 ),
             ]) ?>
-            </section>
-        </div>
+        </section>
     </div>
+</div>
 <?= form_fieldset_close() ?>
 
 <?= form_section_close() ?>
@@ -468,27 +482,39 @@
     lang('Episode.form.advanced_section_title'),
     lang('Episode.form.advanced_section_subtitle'),
 ) ?>
-<?= form_label(
-    lang('Episode.form.custom_rss'),
-    'custom_rss',
-    [],
-    lang('Episode.form.custom_rss_hint'),
-    true,
+<?= component('Forms/Label', 
+    [
+        'text' => lang('Episode.form.custom_rss'),
+        'hint' => lang('Episode.form.custom_rss_hint'),
+        'isOptional' => true,
+    ],
+    [
+        'for' => 'custom_rss',
+    ]
+) ?>
+<?= component('Forms/XMLEditor',
+    [
+        'content' => old('custom_rss', $episode->custom_rss_string)
+    ],
+    [
+        'id' => 'custom_rss',
+        'name' => 'custom_rss',
+    ]
 ) ?>
-<?= xml_editor([
-    'id' => 'custom_rss',
-    'name' => 'custom_rss',
-    'class' => 'form-textarea',
-    'value' => old('custom_rss', $episode->custom_rss_string),
-]) ?>
 <?= form_section_close() ?>
 
-<?= form_switch(
-    lang('Episode.form.block') .
-        hint_tooltip(lang('Episode.form.block_hint'), 'ml-1'),
-    ['id' => 'block', 'name' => 'block'],
-    'yes',
-    old('block', $episode->is_blocked),
+<?= component(
+    'Forms/Toggler',
+    [
+        'label' => lang('Episode.form.block'),
+        'hint' => lang('Episode.form.block_hint')
+    ],
+    [
+        'id' => 'block',
+        'name' => 'block',
+        'value' => 'yes',
+        'checked' => old('block', $episode->is_blocked),
+    ]
 ) ?>
 
 <?= button(
diff --git a/app/Views/admin/episode/embeddable_player.php b/app/Views/admin/episode/embeddable_player.php
index b15060b77c09f7652308abeaa4c58d4563610afe..4cadb490c6220314a781ab3f818369c0f84cc744 100644
--- a/app/Views/admin/episode/embeddable_player.php
+++ b/app/Views/admin/episode/embeddable_player.php
@@ -10,7 +10,7 @@
 
 <?= $this->section('content') ?>
 
-<?= form_label(lang('Episode.embeddable_player.label'), 'label') ?>
+<p><?= lang('Episode.embeddable_player.label') ?></p>
 
 <div class="flex w-full mt-6 mb-6">
     <?php foreach ($themes as $themeKey => $theme): ?>
diff --git a/app/Views/admin/episode/persons.php b/app/Views/admin/episode/persons.php
index 293129f52476e9bd7ddce9950a429e0d219f1f9c..d2e1dc910f46b7dc29fcbdbf95af889eb5f4acd4 100644
--- a/app/Views/admin/episode/persons.php
+++ b/app/Views/admin/episode/persons.php
@@ -60,8 +60,8 @@
                     ($person->information_url === null
                         ? ''
                         : "<a href=\"{$person->information_url}\" target=\"_blank\" rel=\"noreferrer noopener\" class=\"text-sm text-blue-800 hover:underline\">" .
-                            $person->information_url .
-                            '</a>') .
+                        $person->information_url .
+                        '</a>') .
                     '</div></div>';
             },
         ],
@@ -95,40 +95,36 @@
     lang('Person.episode_form.add_section_subtitle'),
 ) ?>
 
-<?= form_label(
-    lang('Person.episode_form.persons'),
-    'persons',
-    [],
-    lang('Person.episode_form.persons_hint'),
+<?= component(
+    'Forms/Label',
+    ['text' => lang('Person.episode_form.persons'), 'hint' =>     lang('Person.episode_form.persons_hint')],
+    ['for' => 'persons'],
 ) ?>
-<?= form_multiselect('persons[]', $personOptions, old('persons', []), [
+<?= component('Forms/MultiSelect', ['options' => $personOptions, 'selected' => old('persons', [])], [
     'id' => 'persons',
-    'class' => 'form-select mb-4',
+    'name' => 'persons[]',
+    'class' => 'mb-4',
     'required' => 'required',
 ]) ?>
 
-<?= form_label(
-    lang('Person.episode_form.roles'),
-    'roles',
-    [],
-    lang('Person.episode_form.roles_hint'),
-    true,
+<?= component(
+    'Forms/Label',
+    ['text' => lang('Person.episode_form.roles'), 'hint' =>     lang('Person.episode_form.roles_hint'), 'isOptional' => true],
+    ['for' => 'roles'],
 ) ?>
-<?= form_multiselect(
-    'roles[]',
-    $taxonomyOptions,
-    old('roles', []),
-    ['id' => 'roles', 'class' => 'form-select mb-4'],
-) ?>
-        
-    
+<?= component('Forms/MultiSelect', ['options' => $taxonomyOptions, 'selected' => old('roles', [])], [
+    'id' => 'roles',
+    'name' => 'roles[]',
+    'class' => 'mb-4',
+]) ?>
+
 <?= form_section_close() ?>
 <?= button(
     lang('Person.episode_form.submit_add'),
     '',
     ['variant' => 'primary'],
     ['type' => 'submit', 'class' => 'self-end'],
-) ?> 
+) ?>
 <?= form_close() ?>
 
 <?= $this->endSection() ?>
diff --git a/app/Views/admin/episode/publish_edit.php b/app/Views/admin/episode/publish_edit.php
index b83914a027714227bf140781488ef4723dd0b87d..8e18531bfc33600d0283d5b326a90144edfe2122 100644
--- a/app/Views/admin/episode/publish_edit.php
+++ b/app/Views/admin/episode/publish_edit.php
@@ -8,7 +8,6 @@
 <?= lang('Episode.publish_edit') ?>
 <?= $this->endSection() ?>
 
-
 <?= $this->section('content') ?>
 
 <?= anchor(
@@ -33,14 +32,11 @@
 <small class="max-w-md mb-2 text-gray-600"><?= lang('Episode.publish_form.post_hint') ?></small>
 <div class="mb-8 overflow-hidden bg-white shadow-md rounded-xl">
     <div class="flex px-4 py-3">
-        <img src="<?= $podcast->actor->avatar_image_url ?>" alt="<?= $podcast->actor
-                                                                        ->display_name ?>" class="w-12 h-12 mr-4 rounded-full" />
+        <img src="<?= $podcast->actor->avatar_image_url ?>" alt="<?= $podcast->actor->display_name ?>" class="w-12 h-12 mr-4 rounded-full" />
         <div class="flex flex-col min-w-0">
             <p class="flex items-baseline min-w-0">
-                <span class="mr-2 font-semibold truncate"><?= $podcast->actor
-                                                                ->display_name ?></span>
-                <span class="text-sm text-gray-500 truncate">@<?= $podcast
-                                                                    ->actor->username ?></span>
+                <span class="mr-2 font-semibold truncate"><?= $podcast->actor->display_name ?></span>
+                <span class="text-sm text-gray-500 truncate">@<?= $podcast->actor->username ?></span>
             </p>
             <?= relative_time($post->published_at, 'text-xs text-gray-500') ?>
         </div>
@@ -85,17 +81,17 @@
     </div>
     <footer class="flex justify-around px-6 py-3">
         <span class="inline-flex items-center"><?= icon(
-                                                    'chat',
-                                                    'text-xl mr-1 text-gray-400',
-                                                ) . '0' ?></span>
+            'chat',
+            'text-xl mr-1 text-gray-400',
+        ) . '0' ?></span>
         <span class="inline-flex items-center"><?= icon(
-                                                    'repeat',
-                                                    'text-xl mr-1 text-gray-400',
-                                                ) . '0' ?></span>
+            'repeat',
+            'text-xl mr-1 text-gray-400',
+        ) . '0' ?></span>
         <span class="inline-flex items-center"><?= icon(
-                                                    'heart',
-                                                    'text-xl mr-1 text-gray-400',
-                                                ) . '0' ?></span>
+            'heart',
+            'text-xl mr-1 text-gray-400',
+        ) . '0' ?></span>
     </footer>
 </div>
 
@@ -151,10 +147,10 @@
                 'data-input' => '',
             ]) ?>
             <button class="p-3 border border-l-0 border-gray-500 bg-pine-100 focus:outline-none rounded-r-md hover:bg-pine-200 focus:ring" type="button" aria-label="<?= lang(
-                                                                                                                                                                            'Episode.publish_form.scheduled_publication_date_clear',
-                                                                                                                                                                        ) ?>" title="<?= lang(
-                                                                                                                                                                                            'Episode.publish_form.scheduled_publication_date_clear',
-                                                                                                                                                                                        ) ?>" data-clear=""><?= icon('close') ?></button>
+                'Episode.publish_form.scheduled_publication_date_clear',
+            ) ?>" title="<?= lang(
+                'Episode.publish_form.scheduled_publication_date_clear',
+            ) ?>" data-clear=""><?= icon('close') ?></button>
         </div>
     </div>
 </div>
diff --git a/app/Views/admin/page/create.php b/app/Views/admin/page/create.php
index 087b08056960310711a1cade47fc773bb2d300f4..711f7fdf9ad04fe03586e9c99176ec8bfd0f4ccb 100644
--- a/app/Views/admin/page/create.php
+++ b/app/Views/admin/page/create.php
@@ -46,14 +46,17 @@
 
 <div class="mb-4">
     <?= form_label(lang('Page.form.content'), 'content') ?>
-    <?= form_markdown_editor(
+    <?= component(
+        'Forms/MarkdownEditor',
+        [
+            'content' => old('content', '', false)
+        ],
         [
             'id' => 'content',
             'name' => 'content',
             'required' => 'required',
+            'rows' => 20
         ],
-        old('content', '', false),
-        ['rows' => '20']
     ) ?>
 </div>
 
diff --git a/app/Views/admin/page/edit.php b/app/Views/admin/page/edit.php
index 4f74d2c3e9f944625cb2b3ddb94ec56df3cb3f9f..0b2902d8d8a52a29087bf4d373491c287eb740c8 100644
--- a/app/Views/admin/page/edit.php
+++ b/app/Views/admin/page/edit.php
@@ -46,13 +46,16 @@
 
 <div class="mb-4">
     <?= form_label(lang('Page.form.content'), 'content') ?>
-    <?= form_markdown_editor(
+    <?= component(
+        'Forms/MarkdownEditor',
+        [
+            'content' => old('content', $page->content_markdown, false),
+        ],
         [
             'id' => 'content',
             'name' => 'content',
             'required' => 'required',
         ],
-        old('content', $page->content_markdown, false),
     ) ?>
 </div>
 
diff --git a/app/Views/admin/podcast/create.php b/app/Views/admin/podcast/create.php
index 3a5a9b94f85150e7a606ea44cf4dcc83ddaedaeb..5bdbbf8ad9693c1135020e5f8822a1084ee9582d 100644
--- a/app/Views/admin/podcast/create.php
+++ b/app/Views/admin/podcast/create.php
@@ -82,13 +82,16 @@
 
 <div class="mb-4">
     <?= form_label(lang('Podcast.form.description'), 'description') ?>
-    <?= form_markdown_editor(
+    <?= component(
+        'Forms/MarkdownEditor',
+        [
+            'content' => old('description', '', false),
+        ],
         [
             'id' => 'description',
             'name' => 'description',
             'required' => 'required',
         ],
-        old('description', '', false),
     ) ?>
 </div>
 
@@ -122,16 +125,12 @@
     '',
     true,
 ) ?>
-<?= form_multiselect(
-    'other_categories[]',
-    $categoryOptions,
-    [old('other_categories', '')],
-    [
-        'id' => 'other_categories',
-        'class' => 'mb-4',
-        'data-max-item-count' => '2',
-    ],
-) ?>
+<?= component('Forms/MultiSelect', ['options' => $categoryOptions, 'selected' => old('other_categories', [])], [
+    'id' => 'other_categories',
+    'name' => 'other_categories[]',
+    'class' => 'mb-4',
+    'data-max-item-count' => '2',
+]) ?>
 
 <?= form_fieldset('', ['class' => 'mb-4']) ?>
     <legend>
@@ -340,12 +339,15 @@
     lang('Podcast.form.custom_rss_hint'),
     true,
 ) ?>
-<?= xml_editor([
-    'id' => 'custom_rss',
-    'name' => 'custom_rss',
-    'class' => 'form-textarea',
-    'value' => old('custom_rss'),
-]) ?>
+<?= component('Forms/XMLEditor', 
+    [
+        'content' => old('custom_rss', '')
+    ],
+    [
+        'id' => 'custom_rss',
+        'name' => 'custom_rss',
+    ]
+) ?>
 <?= form_section_close() ?>
 
 <?= form_section(
@@ -353,28 +355,46 @@
     lang('Podcast.form.status_section_subtitle'),
 ) ?>
 
-<?= form_switch(
-    lang('Podcast.form.block'),
-    ['id' => 'block', 'name' => 'block'],
-    'yes',
-    old('block', false),
-    'mb-2',
+<?= component(
+    'Forms/Toggler',
+    [
+        'label' => lang('Podcast.form.lock'),
+        'hint' => lang('Podcast.form.lock_hint'),
+    ],
+    [
+        'id' => 'lock',
+        'name' => 'lock',
+        'value' => 'yes',
+        'checked' => old('complete', true),
+        'class' => 'mb-2'
+    ]
 ) ?>
 
-<?= form_switch(
-    lang('Podcast.form.complete'),
-    ['id' => 'complete', 'name' => 'complete'],
-    'yes',
-    old('complete', false),
-    'mb-2',
+<?= component(
+    'Forms/Toggler',
+    [
+        'label' => lang('Podcast.form.block'),
+    ],
+    [
+        'id' => 'block',
+        'name' => 'block',
+        'value' => 'yes',
+        'checked' => old('block', false),
+        'class' => 'mb-2'
+    ]
 ) ?>
 
-<?= form_switch(
-    lang('Podcast.form.lock') .
-        hint_tooltip(lang('Podcast.form.lock_hint'), 'ml-1'),
-    ['id' => 'lock', 'name' => 'lock'],
-    'yes',
-    old('lock', true),
+<?= component(
+    'Forms/Toggler',
+    [
+        'label' => lang('Podcast.form.complete'),
+    ],
+    [
+        'id' => 'complete',
+        'name' => 'complete',
+        'value' => 'yes',
+        'checked' => old('complete', false),
+    ]
 ) ?>
 
 <?= form_section_close() ?>
diff --git a/app/Views/admin/podcast/edit.php b/app/Views/admin/podcast/edit.php
index a24413ae3596fb7fbf4a615390367122eca1babb..8523209c955c020c10525808fbe311072b0971e4 100644
--- a/app/Views/admin/podcast/edit.php
+++ b/app/Views/admin/podcast/edit.php
@@ -8,12 +8,14 @@
 <?= lang('Podcast.edit') ?>
 <?= $this->endSection() ?>
 
+
 <?= $this->section('content') ?>
 
 <?= form_open_multipart(route_to('podcast-edit', $podcast->id), [
     'method' => 'post',
     'class' => 'flex flex-col',
 ]) ?>
+
 <?= csrf_field() ?>
 
 <?= form_section(
@@ -22,11 +24,8 @@
 ) ?>
 
 <?= form_label(lang('Podcast.form.image'), 'image') ?>
-<img
-    src="<?= $podcast->image->thumbnail_url ?>"
-    alt="<?= $podcast->title ?>"
-    class="object-cover w-32 h-32"
-/>
+
+<img src="<?= $podcast->image->thumbnail_url ?>" alt="<?= $podcast->title ?>" class="object-cover w-32 h-32" />
 <?= form_input([
     'id' => 'image',
     'name' => 'image',
@@ -34,11 +33,14 @@
     'type' => 'file',
     'accept' => '.jpg,.jpeg,.png',
 ]) ?>
+
+
 <small class="mb-4 text-gray-600"><?= lang(
-    'Common.forms.image_size_hint',
-) ?></small>
+                                        'Common.forms.image_size_hint',
+                                    ) ?></small>
 
 <?= form_label(lang('Podcast.form.title'), 'title') ?>
+
 <?= form_input([
     'id' => 'title',
     'name' => 'title',
@@ -46,46 +48,52 @@
     'value' => old('title', $podcast->title),
     'required' => 'required',
 ]) ?>
+
 <span class="mb-4 text-sm"><?= $podcast->link ?></span>
 
 <?= form_fieldset('', ['class' => 'mb-4']) ?>
-    <legend><?= lang('Podcast.form.type.label') .
-        hint_tooltip(lang('Podcast.form.type.hint'), 'ml-1') ?>
-    </legend>
-    <?= form_radio(
-        ['id' => 'episodic', 'name' => 'type', 'class' => 'form-radio-btn'],
-        'episodic',
-        old('type') ? old('type') == 'episodic' : $podcast->type == 'episodic',
-    ) ?>
-    <label for="episodic"><?= lang('Podcast.form.type.episodic') ?></label>
-    <?= form_radio(
-        ['id' => 'serial', 'name' => 'type', 'class' => 'form-radio-btn'],
-        'serial',
-        old('type') ? old('type') == 'serial' : $podcast->type == 'serial',
-    ) ?>
-    <label for="serial"><?= lang('Podcast.form.type.serial') ?></label>
+
+<legend><?= lang('Podcast.form.type.label') .
+            hint_tooltip(lang('Podcast.form.type.hint'), 'ml-1') ?>
+</legend>
+<?= form_radio(
+    ['id' => 'episodic', 'name' => 'type', 'class' => 'form-radio-btn'],
+    'episodic',
+    old('type') ? old('type') == 'episodic' : $podcast->type == 'episodic',
+) ?>
+<label for="episodic"><?= lang('Podcast.form.type.episodic') ?></label>
+<?= form_radio(
+    ['id' => 'serial', 'name' => 'type', 'class' => 'form-radio-btn'],
+    'serial',
+    old('type') ? old('type') == 'serial' : $podcast->type == 'serial',
+) ?>
+<label for="serial"><?= lang('Podcast.form.type.serial') ?></label>
 <?= form_fieldset_close() ?>
 
 <div class="mb-4">
     <?= form_label(lang('Podcast.form.description'), 'description') ?>
-    <?= form_markdown_editor([
+    <?= component(
+        'Forms/MarkdownEditor',
+        [
+            'content' => old('description', $podcast->description_markdown, false)
+        ],
+        [
             'id' => 'description',
             'name' => 'description',
             'required' => 'required',
         ],
-        old('description', $podcast->description_markdown, false)
     ) ?>
 </div>
 
 <?= form_section_close() ?>
 
-
 <?= form_section(
-    lang('Podcast.form.classification_section_title'),
-    lang('Podcast.form.classification_section_subtitle'),
+lang('Podcast.form.classification_section_title'),
+lang('Podcast.form.classification_section_subtitle'),
 ) ?>
 
 <?= form_label(lang('Podcast.form.language'), 'language') ?>
+
 <?= form_dropdown(
     'language',
     $languageOptions,
@@ -101,7 +109,7 @@
 <?= form_dropdown(
     'category',
     $categoryOptions,
-    [old('category', (string) $podcast->category_id)],
+    [old('category', $podcast->category_id)],
     [
         'id' => 'category',
         'class' => 'form-select mb-4',
@@ -116,65 +124,63 @@
     '',
     true,
 ) ?>
-<?= form_multiselect(
-    'other_categories[]',
-    $categoryOptions,
-    old('other_categories', $podcast->other_categories_ids),
+
+<?= component('Forms/MultiSelect', ['options' => $categoryOptions, 'selected' => old('other_categories', $podcast->other_categories_ids)], [
+    'id' => 'other_categories',
+    'name' => 'other_categories[]',
+    'class' => 'mb-4',
+    'data-max-item-count' => '2',
+]) ?>
+
+<?= form_fieldset('', ['class' => 'mb-4']) ?>
+<legend><?= lang('Podcast.form.parental_advisory.label') .
+            hint_tooltip(lang('Podcast.form.parental_advisory.hint'), 'ml-1') ?></legend>
+<?= form_radio(
     [
-        'id' => 'other_categories',
-        'class' => 'mb-4',
-        'data-max-item-count' => '2',
-    ],
+    'id' => 'undefined',
+    'name' => 'parental_advisory',
+    'class' => 'form-radio-btn',
+],
+    'undefined',
+    old('parental_advisory')
+    ? old('parental_advisory') === 'undefined'
+    : $podcast->parental_advisory === null,
 ) ?>
 
-<?= form_fieldset('', ['class' => 'mb-4']) ?>
-    <legend><?= lang('Podcast.form.parental_advisory.label') .
-        hint_tooltip(lang('Podcast.form.parental_advisory.hint'), 'ml-1') ?>
-    </legend>
-    <?= form_radio(
-        [
-            'id' => 'undefined',
-            'name' => 'parental_advisory',
-            'class' => 'form-radio-btn',
-        ],
-        'undefined',
-        old('parental_advisory')
-            ? old('parental_advisory') === 'undefined'
-            : $podcast->parental_advisory === null,
-    ) ?>
-    <label for="undefined"><?= lang(
-        'Podcast.form.parental_advisory.undefined',
-    ) ?></label>
-    <?= form_radio(
-        [
-            'id' => 'clean',
-            'name' => 'parental_advisory',
-            'class' => 'form-radio-btn',
-        ],
-        'clean',
-        old('parental_advisory')
-            ? old('parental_advisory') === 'clean'
-            : $podcast->parental_advisory === 'clean',
-    ) ?>
-    <label for="clean"><?= lang(
-        'Podcast.form.parental_advisory.clean',
-    ) ?></label>
-    <?= form_radio(
-        [
-            'id' => 'explicit',
-            'name' => 'parental_advisory',
-            'class' => 'form-radio-btn',
-        ],
-        'explicit',
-        old('parental_advisory')
-            ? old('parental_advisory') === 'explicit'
-            : $podcast->parental_advisory === 'explicit',
-    ) ?>
-    <label for="explicit"><?= lang(
-        'Podcast.form.parental_advisory.explicit',
-    ) ?></label>
-<?= form_fieldset_close() ?>
+<label for="undefined"><?= lang(
+                            'Podcast.form.parental_advisory.undefined',
+                        ) ?></label>
+<?= form_radio(
+    [
+    'id' => 'clean',
+    'name' => 'parental_advisory',
+    'class' => 'form-radio-btn',
+],
+    'clean',
+    old('parental_advisory')
+    ? old('parental_advisory') === 'clean'
+    : $podcast->parental_advisory === 'clean',
+) ?>
+
+<label for="clean"><?= lang(
+                        'Podcast.form.parental_advisory.clean',
+                    ) ?></label>
+<?= form_radio(
+    [
+    'id' => 'explicit',
+    'name' => 'parental_advisory',
+    'class' => 'form-radio-btn',
+],
+    'explicit',
+    old('parental_advisory')
+    ? old('parental_advisory') === 'explicit'
+    : $podcast->parental_advisory === 'explicit',
+) ?>
 
+<label for="explicit"><?= lang(
+                            'Podcast.form.parental_advisory.explicit',
+                        ) ?></label>
+<?= form_fieldset_close() ?>
 <?= form_section_close() ?>
 
 <?= form_section(
@@ -188,6 +194,7 @@
     [],
     lang('Podcast.form.owner_name_hint'),
 ) ?>
+
 <?= form_input([
     'id' => 'owner_name',
     'name' => 'owner_name',
@@ -202,6 +209,7 @@
     [],
     lang('Podcast.form.owner_email_hint'),
 ) ?>
+
 <?= form_input([
     'id' => 'owner_email',
     'name' => 'owner_email',
@@ -218,6 +226,7 @@
     lang('Podcast.form.publisher_hint'),
     true,
 ) ?>
+
 <?= form_input([
     'id' => 'publisher',
     'name' => 'publisher',
@@ -226,6 +235,7 @@
 ]) ?>
 
 <?= form_label(lang('Podcast.form.copyright'), 'copyright', [], '', true) ?>
+
 <?= form_input([
     'id' => 'copyright',
     'name' => 'copyright',
@@ -247,12 +257,14 @@
     lang('Podcast.form.location_name_hint'),
     true,
 ) ?>
+
 <?= form_input([
     'id' => 'location_name',
     'name' => 'location_name',
     'class' => 'form-input mb-4',
     'value' => old('location_name', $podcast->location_name),
 ]) ?>
+
 <?= form_section_close() ?>
 
 <?= form_section(
@@ -267,6 +279,7 @@
     lang('Podcast.form.payment_pointer_hint'),
     true,
 ) ?>
+
 <?= form_input([
     'id' => 'payment_pointer',
     'name' => 'payment_pointer',
@@ -285,41 +298,41 @@
             true,
         ) ?>
         <?= form_input([
-            'id' => 'partner_id',
-            'name' => 'partner_id',
-            'class' => 'form-input w-full',
-            'value' => old('partner_id', $podcast->partner_id),
-        ]) ?>
+    'id' => 'partner_id',
+    'name' => 'partner_id',
+    'class' => 'form-input w-full',
+    'value' => old('partner_id', $podcast->partner_id),
+]) ?>
     </div>
     <div class="flex flex-col flex-1">
         <?= form_label(
-            lang('Podcast.form.partner_link_url'),
-            'partner_link_url',
-            [],
-            lang('Podcast.form.partner_link_url_hint'),
-            true,
-        ) ?>
+    lang('Podcast.form.partner_link_url'),
+    'partner_link_url',
+    [],
+    lang('Podcast.form.partner_link_url_hint'),
+    true,
+) ?>
         <?= form_input([
-            'id' => 'partner_link_url',
-            'name' => 'partner_link_url',
-            'class' => 'form-input w-full',
-            'value' => old('partner_link_url', $podcast->partner_link_url),
-        ]) ?>
+    'id' => 'partner_link_url',
+    'name' => 'partner_link_url',
+    'class' => 'form-input w-full',
+    'value' => old('partner_link_url', $podcast->partner_link_url),
+]) ?>
     </div>
     <div class="flex flex-col flex-1">
         <?= form_label(
-            lang('Podcast.form.partner_image_url'),
-            'partner_image_url',
-            [],
-            lang('Podcast.form.partner_image_url_hint'),
-            true,
-        ) ?>
-        <?= form_input([
-            'id' => 'partner_image_url',
-            'name' => 'partner_image_url',
-            'class' => 'form-input w-full',
-            'value' => old('partner_image_url', $podcast->partner_image_url),
-        ]) ?>
+    lang('Podcast.form.partner_image_url'),
+    'partner_image_url',
+    [],
+    lang('Podcast.form.partner_image_url_hint'),
+    true,
+) ?>
+    <?= form_input([
+    'id' => 'partner_image_url',
+    'name' => 'partner_image_url',
+    'class' => 'form-input w-full',
+    'value' => old('partner_image_url', $podcast->partner_image_url),
+]) ?>
     </div>
 </div>
 <?= form_section_close() ?>
@@ -328,6 +341,7 @@
     lang('Podcast.form.advanced_section_title'),
     lang('Podcast.form.advanced_section_subtitle'),
 ) ?>
+
 <?= form_label(
     lang('Podcast.form.custom_rss'),
     'custom_rss',
@@ -335,12 +349,17 @@
     lang('Podcast.form.custom_rss_hint'),
     true,
 ) ?>
-<?= xml_editor([
-    'id' => 'custom_rss',
-    'name' => 'custom_rss',
-    'class' => 'form-textarea',
-    'value' => old('custom_rss', $podcast->custom_rss_string),
-]) ?>
+
+<?= component('Forms/XMLEditor', 
+    [
+        'content' => old('custom_rss', $podcast->custom_rss_string)
+    ],
+    [
+        'id' => 'custom_rss',
+        'name' => 'custom_rss',
+    ]
+) ?>
+
 <?= form_section_close() ?>
 
 <?= form_section(
@@ -348,40 +367,62 @@
     lang('Podcast.form.status_section_subtitle'),
 ) ?>
 
-<?= form_switch(
-    lang('Podcast.form.block'),
-    ['id' => 'block', 'name' => 'block'],
-    'yes',
-    old('block', $podcast->is_blocked),
-    'mb-2',
+<?= component(
+    'Forms/Toggler',
+    [
+        'label' => lang('Podcast.form.lock'),
+        'hint' => lang('Podcast.form.lock_hint'),
+    ],
+    [
+        'id' => 'lock',
+        'name' => 'lock',
+        'value' => 'yes',
+        'checked' => old('complete', $podcast->is_locked),
+        'class' => 'mb-2'
+    ]
 ) ?>
 
-<?= form_switch(
-    lang('Podcast.form.complete'),
-    ['id' => 'complete', 'name' => 'complete'],
-    'yes',
-    old('complete', $podcast->is_completed),
-    'mb-2',
+<?= component(
+    'Forms/Toggler',
+    [
+        'label' => lang('Podcast.form.block'),
+    ],
+    [
+        'id' => 'block',
+        'name' => 'block',
+        'value' => 'yes',
+        'checked' => old('block', $podcast->is_blocked),
+        'class' => 'mb-2'
+    ]
 ) ?>
 
-<?= form_switch(
-    lang('Podcast.form.lock') .
-        hint_tooltip(lang('Podcast.form.lock_hint'), 'ml-1'),
-    ['id' => 'lock', 'name' => 'lock'],
-    'yes',
-    old('lock', $podcast->is_locked),
+<?= component(
+    'Forms/Toggler',
+    [
+        'label' => lang('Podcast.form.complete'),
+    ],
+    [
+        'id' => 'complete',
+        'name' => 'complete',
+        'value' => 'yes',
+        'checked' => old('complete', $podcast->is_completed),
+    ]
 ) ?>
 
 <?= form_section_close() ?>
 
-<?= button(
-    lang('Podcast.form.submit_edit'),
-    '',
-    ['variant' => 'primary'],
-    ['type' => 'submit', 'class' => 'self-end'],
+<?= component(
+    'Button',
+    [
+        'label' => lang('Podcast.form.submit_edit'),
+        'variant' => 'primary',
+    ],
+    [
+        'type' => 'submit',
+        'class' => 'self-end'
+    ]
 ) ?>
 
 <?= form_close() ?>
 
-
 <?= $this->endSection() ?>
diff --git a/app/Views/admin/podcast/persons.php b/app/Views/admin/podcast/persons.php
index 506cedfe41f5250f8ad1d36aa3c957399e0ef927..f9360ec0f64b5b9fc0b1caffd29d2e2f571e8759 100644
--- a/app/Views/admin/podcast/persons.php
+++ b/app/Views/admin/podcast/persons.php
@@ -99,10 +99,12 @@
     [],
     lang('Person.podcast_form.persons_hint'),
 ) ?>
-<?= form_multiselect('persons[]', $personOptions, old('persons', []), [
+<?= component('Forms/MultiSelect', ['options' => $personOptions, 'selected' => old('persons', [])], [
     'id' => 'persons',
-    'class' => 'form-select mb-4',
+    'name' => 'persons[]',
+    'class' => 'mb-4',
     'required' => 'required',
+    'data-max-item-count' => '2',
 ]) ?>
 
 <?= form_label(
@@ -112,9 +114,10 @@
     lang('Person.podcast_form.roles_hint'),
     true,
 ) ?>
-<?= form_multiselect('roles[]', $taxonomyOptions, old('roles', []), [
+<?= component('Forms/MultiSelect', ['options' => $taxonomyOptions, 'selected' => old('roles', [])], [
     'id' => 'roles',
-    'class' => 'form-select mb-4',
+    'name' => 'roles[]',
+    'class' => 'mb-4',
 ]) ?>
 
 <?= form_section_close() ?>
diff --git a/app/Views/admin/podcast/platforms.php b/app/Views/admin/podcast/platforms.php
index 6cd06182d0830f8d1dde91881c81f858e26a1384..77b46d34933f86a5a6f0b7d6c13db9ff0e47ab54 100644
--- a/app/Views/admin/podcast/platforms.php
+++ b/app/Views/admin/podcast/platforms.php
@@ -103,34 +103,39 @@
             'type' => 'text',
             'placeholder' => lang("Platforms.description.{$platform->type}"),
         ]) ?>
-        <?= form_switch(
-            lang('Platforms.visible'),
+        <?= component(
+            'Forms/Toggler',
+            [
+                'label' => lang('Platforms.visible'),
+            ],
             [
                 'id' => $platform->slug . '_visible',
                 'name' => 'platforms[' . $platform->slug . '][visible]',
-            ],
-            'yes',
-            old(
-                $platform->slug . '_visible',
-                $platform->is_visible ? $platform->is_visible : false,
-            ),
-            'text-sm mb-1',
+                'value' => 'yes',
+                'checked' => old(
+                    $platform->slug . '_visible',
+                    $platform->is_visible ? $platform->is_visible : false,
+                ),
+                'class' => 'text-sm mb-1'
+            ]
         ) ?>
-        <?= form_switch(
-            lang('Platforms.on_embeddable_player'),
+        <?= component(
+            'Forms/Toggler',
             [
-                'id' => $platform->slug . '_on_embeddable_player',
-                'name' =>
-                    'platforms[' . $platform->slug . '][on_embeddable_player]',
+                'label' => lang('Platforms.on_embeddable_player'),
             ],
-            'yes',
-            old(
-                $platform->slug . '_on_embeddable_player',
-                $platform->is_on_embeddable_player
-                    ? $platform->is_on_embeddable_player
-                    : false,
-            ),
-            'text-sm',
+            [
+                'id' => $platform->slug . '_on_embeddable_player',
+                'name' => 'platforms[' . $platform->slug . '][on_embeddable_player]',
+                'value' => 'yes',
+                'checked' => old(
+                    $platform->slug . '_on_embeddable_player',
+                    $platform->is_on_embeddable_player
+                        ? $platform->is_on_embeddable_player
+                        : false,
+                ),
+                'class' => 'text-sm'
+            ]
         ) ?>
     </div>
 </div>
diff --git a/app/Views/admin/user/edit.php b/app/Views/admin/user/edit.php
index 4fac32d99a5ae2f4054d7c2129f30e69923f2fc9..d57f80842b26a06f485969d5eae82efa20aa8937 100644
--- a/app/Views/admin/user/edit.php
+++ b/app/Views/admin/user/edit.php
@@ -17,8 +17,9 @@
 <?= csrf_field() ?>
 
 <?= form_label(lang('User.form.roles'), 'roles') ?>
-<?= form_multiselect('roles[]', $roleOptions, $user->roles, [
+<?= component('Forms/MultiSelect', ['options' => $roleOptions, 'selected' => $user->roles], [
     'id' => 'roles',
+    'name' => 'roles[]',
     'class' => 'mb-4',
 ]) ?>
 
diff --git a/app/Views/components/.gitkeep b/app/Views/components/.gitkeep
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/app/Views/podcast/_partials/comment_with_replies_authenticated.php b/app/Views/podcast/_partials/comment_with_replies_authenticated.php
index 567137b395fa1360171861a672803bede206db95..52c0b4b3d0c4b05c1d1a744b6ba26ed0cb89514a 100644
--- a/app/Views/podcast/_partials/comment_with_replies_authenticated.php
+++ b/app/Views/podcast/_partials/comment_with_replies_authenticated.php
@@ -1,4 +1,4 @@
-<?= $this->include('podcast/_partials/comment_card_authenticated') ?>
+<?=  $this->include('podcast/_partials/comment_card_authenticated') ?>
 <div class="-mt-2 overflow-hidden border-b border-l border-r post-replies rounded-b-xl">
 <?= form_open(
     route_to('comment-attempt-reply', $podcast->id, $episode->id, $comment->id),
diff --git a/app/Views/podcast/activity.php b/app/Views/podcast/activity.php
index 11959f914c656def98bed2baacdbba1353d06584..a6d91f94dcd8eb3c18dba99752b73912d76137fc 100644
--- a/app/Views/podcast/activity.php
+++ b/app/Views/podcast/activity.php
@@ -38,7 +38,6 @@
     'Podcast.episodes',
 ) ?></a>
 </nav>
-
 <section class="max-w-2xl px-6 py-8 mx-auto space-y-8">
 <?php foreach ($posts as $post): ?>
     <?php if ($post->reblog_of_id !== null): ?>
diff --git a/ecs.php b/ecs.php
index 8a67127a37fcc233dd7dd7515f7c91e34a43ef39..1ef585e7341ba94a6a04bae1e29823783cdf178e 100644
--- a/ecs.php
+++ b/ecs.php
@@ -1,6 +1,7 @@
 <?php
 
 use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
+use Symplify\CodingStandard\Fixer\Naming\StandardizeHereNowDocKeywordFixer;
 use Symplify\EasyCodingStandard\ValueObject\Option;
 use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
 
@@ -20,6 +21,10 @@ return static function (ContainerConfigurator $containerConfigurator): void {
 
         // skip specific generated files
         __DIR__ . '/app/Language/*/PersonsTaxonomy.php',
+
+        StandardizeHereNowDocKeywordFixer::class => [
+            __DIR__ . '/app/Views/Components',
+        ]
     ]);
 
     $containerConfigurator->import(SetList::PSR_12);
diff --git a/phpstan.neon b/phpstan.neon
index 36d4a64106d2c442a5eeda6a913567b1707140aa..ac3d788d6c1e01d5ba3babfe8c1d772b8287fa3a 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -35,3 +35,4 @@ parameters:
                 - app/Helpers
                 - app/Libraries/ActivityPub/Helpers
                 - app/Libraries/Analytics/Helpers
+                - app/Libraries/ViewComponents/Helpers
diff --git a/rector.php b/rector.php
index e3ba7ecf788ef14c9c93210e86e1ed2d7f177549..f3057c667927285936e47bc45a3ec15fc9e2ef21 100644
--- a/rector.php
+++ b/rector.php
@@ -65,7 +65,7 @@ return static function (ContainerConfigurator $containerConfigurator): void {
         ],
     ]);
 
-    // Path to phpstan with extensions, that PHPSTan in Rector uses to determine types
+    // Path to phpstan with extensions, that PHPStan in Rector uses to determine types
     $parameters->set(
         Option::PHPSTAN_FOR_RECTOR_PATH,
         __DIR__ . '/phpstan.neon',
diff --git a/tailwind.config.js b/tailwind.config.js
index 36fd2211a020f3aa3baf1cb909563f21d9b50e51..4b4b984edd939611f4e8d17b7c654d9a988bb19c 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -4,6 +4,7 @@ module.exports = {
   mode: "jit",
   purge: [
     "./app/Views/**/*.php",
+    "./app/View/Components/**/*.php",
     "./app/Helpers/*.php",
     "./app/Resources/**/*.ts",
   ],