diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json
index 02fba8d..601ff58 100644
--- a/assets/translations/en-US.json
+++ b/assets/translations/en-US.json
@@ -139,6 +139,7 @@
   "fieldPostTitle": "Title",
   "fieldPostDescription": "Description",
   "fieldPostTags": "Tags",
+  "fieldPostCategories": "Categories",
   "fieldPostAlias": "Alias",
   "fieldPostAliasHint": "Optional, used to represent the post in URL, should follow URL-Safe.",
   "postPublish": "Publish",
diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json
index f1de935..0ad7b16 100644
--- a/assets/translations/zh-CN.json
+++ b/assets/translations/zh-CN.json
@@ -123,6 +123,7 @@
   "fieldPostTitle": "标题",
   "fieldPostDescription": "描述",
   "fieldPostTags": "标签",
+  "fieldPostCategories": "分类",
   "fieldPostAlias": "别名",
   "fieldPostAliasHint": "可选项,用于在 URL 中表示该帖子,应遵循 URL-Safe 的原则。",
   "postPublish": "发布",
diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json
index fcd16a8..b7edfa4 100644
--- a/assets/translations/zh-HK.json
+++ b/assets/translations/zh-HK.json
@@ -123,6 +123,7 @@
   "fieldPostTitle": "標題",
   "fieldPostDescription": "描述",
   "fieldPostTags": "標籤",
+  "fieldPostCategories": "分類",
   "fieldPostAlias": "別名",
   "fieldPostAliasHint": "可選項,用於在 URL 中表示該帖子,應遵循 URL-Safe 的原則。",
   "postPublish": "發佈",
diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json
index 7760052..b1c0b7a 100644
--- a/assets/translations/zh-TW.json
+++ b/assets/translations/zh-TW.json
@@ -123,6 +123,7 @@
   "fieldPostTitle": "標題",
   "fieldPostDescription": "描述",
   "fieldPostTags": "標籤",
+  "fieldPostCategories": "分類",
   "fieldPostAlias": "別名",
   "fieldPostAliasHint": "可選項,用於在 URL 中表示該帖子,應遵循 URL-Safe 的原則。",
   "postPublish": "釋出",
diff --git a/lib/controllers/post_write_controller.dart b/lib/controllers/post_write_controller.dart
index aae9232..9c4913e 100644
--- a/lib/controllers/post_write_controller.dart
+++ b/lib/controllers/post_write_controller.dart
@@ -178,6 +178,7 @@ class PostWriteController extends ChangeNotifier {
   List<int> visibleUsers = List.empty();
   List<int> invisibleUsers = List.empty();
   List<String> tags = List.empty();
+  List<String> categories = List.empty();
   PostWriteMedia? thumbnail;
   List<PostWriteMedia> attachments = List.empty(growable: true);
   DateTime? publishedAt, publishedUntil;
@@ -207,6 +208,7 @@ class PostWriteController extends ChangeNotifier {
         invisibleUsers = List.from(post.invisibleUsersList ?? []);
         visibility = post.visibility;
         tags = List.from(post.tags.map((ele) => ele.alias));
+        categories = List.from(post.categories.map((ele) => ele.alias));
         attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
 
         if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
@@ -345,6 +347,7 @@ class PostWriteController extends ChangeNotifier {
           if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.rid,
           'attachments': attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
           'tags': tags.map((ele) => {'alias': ele}).toList(),
+          'categories': categories.map((ele) => {'alias': ele}).toList(),
           'visibility': visibility,
           'visible_users_list': visibleUsers,
           'invisible_users_list': invisibleUsers,
@@ -431,6 +434,11 @@ class PostWriteController extends ChangeNotifier {
     notifyListeners();
   }
 
+  void setCategories(List<String> value) {
+    categories = value;
+    notifyListeners();
+  }
+
   void setVisibility(int value) {
     visibility = value;
     notifyListeners();
@@ -467,6 +475,9 @@ class PostWriteController extends ChangeNotifier {
     titleController.clear();
     descriptionController.clear();
     contentController.clear();
+    aliasController.clear();
+    tags.clear();
+    categories.clear();
     attachments.clear();
     editingPost = null;
     replyingPost = null;
@@ -480,6 +491,7 @@ class PostWriteController extends ChangeNotifier {
     contentController.dispose();
     titleController.dispose();
     descriptionController.dispose();
+    aliasController.dispose();
     super.dispose();
   }
 }
diff --git a/lib/providers/userinfo.dart b/lib/providers/userinfo.dart
index 03a4345..105d418 100644
--- a/lib/providers/userinfo.dart
+++ b/lib/providers/userinfo.dart
@@ -1,7 +1,6 @@
 import 'dart:developer';
 
 import 'package:flutter/material.dart';
-import 'package:path/path.dart';
 import 'package:provider/provider.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 import 'package:surface/providers/config.dart';
diff --git a/lib/widgets/post/post_meta_editor.dart b/lib/widgets/post/post_meta_editor.dart
index 1bbd710..85aea48 100644
--- a/lib/widgets/post/post_meta_editor.dart
+++ b/lib/widgets/post/post_meta_editor.dart
@@ -83,167 +83,178 @@ class PostMetaEditor extends StatelessWidget {
     return ListenableBuilder(
       listenable: controller,
       builder: (context, _) {
-        return Column(
-          children: [
-            TextField(
-              controller: controller.titleController,
-              decoration: InputDecoration(
-                labelText: 'fieldPostTitle'.tr(),
-                border: UnderlineInputBorder(),
-              ),
-              onTapOutside: (_) =>
-                  FocusManager.instance.primaryFocus?.unfocus(),
-            ).padding(horizontal: 24),
-            if (controller.mode == 'articles') const Gap(4),
-            if (controller.mode == 'articles')
+        return SingleChildScrollView(
+          padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8),
+          child: Column(
+            children: [
               TextField(
-                controller: controller.descriptionController,
-                maxLines: null,
+                controller: controller.titleController,
                 decoration: InputDecoration(
-                  labelText: 'fieldPostDescription'.tr(),
+                  labelText: 'fieldPostTitle'.tr(),
                   border: UnderlineInputBorder(),
                 ),
                 onTapOutside: (_) =>
                     FocusManager.instance.primaryFocus?.unfocus(),
               ).padding(horizontal: 24),
-            const Gap(4),
-            PostTagsField(
-              initialTags: controller.tags,
-              labelText: 'fieldPostTags'.tr(),
-              onUpdate: (value) {
-                controller.setTags(value);
-              },
-            ).padding(horizontal: 24),
-            const Gap(4),
-            TextField(
-              controller: controller.aliasController,
-              decoration: InputDecoration(
-                labelText: 'fieldPostAlias'.tr(),
-                helperText: 'fieldPostAliasHint'.tr(),
-                helperMaxLines: 2,
-                border: UnderlineInputBorder(),
-              ),
-              onTapOutside: (_) =>
-                  FocusManager.instance.primaryFocus?.unfocus(),
-            ).padding(horizontal: 24),
-            const Gap(12),
-            ListTile(
-              contentPadding: const EdgeInsets.symmetric(horizontal: 24),
-              leading: const Icon(Symbols.visibility),
-              title: Text('postVisibility').tr(),
-              subtitle: Text('postVisibilityDescription').tr(),
-              trailing: SizedBox(
-                width: 180,
-                child: DropdownButtonHideUnderline(
-                  child: DropdownButton2<int>(
-                    isExpanded: true,
-                    items: kPostVisibilityLevel.entries
-                        .map(
-                          (entry) => DropdownMenuItem<int>(
-                            value: entry.key,
-                            child: Text(
-                              entry.value,
-                              style: const TextStyle(fontSize: 14),
-                            ).tr(),
-                          ),
-                        )
-                        .toList(),
-                    value: controller.visibility,
-                    onChanged: (int? value) {
-                      if (value != null) {
-                        controller.setVisibility(value);
-                      }
-                    },
-                    buttonStyleData: const ButtonStyleData(
-                      height: 40,
-                      padding: EdgeInsets.symmetric(
-                        horizontal: 4,
-                        vertical: 8,
+              if (controller.mode == 'articles') const Gap(4),
+              if (controller.mode == 'articles')
+                TextField(
+                  controller: controller.descriptionController,
+                  maxLines: null,
+                  decoration: InputDecoration(
+                    labelText: 'fieldPostDescription'.tr(),
+                    border: UnderlineInputBorder(),
+                  ),
+                  onTapOutside: (_) =>
+                      FocusManager.instance.primaryFocus?.unfocus(),
+                ).padding(horizontal: 24),
+              const Gap(4),
+              PostTagsField(
+                initialTags: controller.tags,
+                labelText: 'fieldPostTags'.tr(),
+                onUpdate: (value) {
+                  controller.setTags(value);
+                },
+              ).padding(horizontal: 24),
+              const Gap(4),
+              PostCategoriesField(
+                initialCategories: controller.categories,
+                labelText: 'fieldPostCategories'.tr(),
+                onUpdate: (value) {
+                  controller.setCategories(value);
+                },
+              ).padding(horizontal: 24),
+              const Gap(4),
+              TextField(
+                controller: controller.aliasController,
+                decoration: InputDecoration(
+                  labelText: 'fieldPostAlias'.tr(),
+                  helperText: 'fieldPostAliasHint'.tr(),
+                  helperMaxLines: 2,
+                  border: UnderlineInputBorder(),
+                ),
+                onTapOutside: (_) =>
+                    FocusManager.instance.primaryFocus?.unfocus(),
+              ).padding(horizontal: 24),
+              const Gap(12),
+              ListTile(
+                contentPadding: const EdgeInsets.symmetric(horizontal: 24),
+                leading: const Icon(Symbols.visibility),
+                title: Text('postVisibility').tr(),
+                subtitle: Text('postVisibilityDescription').tr(),
+                trailing: SizedBox(
+                  width: 180,
+                  child: DropdownButtonHideUnderline(
+                    child: DropdownButton2<int>(
+                      isExpanded: true,
+                      items: kPostVisibilityLevel.entries
+                          .map(
+                            (entry) => DropdownMenuItem<int>(
+                              value: entry.key,
+                              child: Text(
+                                entry.value,
+                                style: const TextStyle(fontSize: 14),
+                              ).tr(),
+                            ),
+                          )
+                          .toList(),
+                      value: controller.visibility,
+                      onChanged: (int? value) {
+                        if (value != null) {
+                          controller.setVisibility(value);
+                        }
+                      },
+                      buttonStyleData: const ButtonStyleData(
+                        height: 40,
+                        padding: EdgeInsets.symmetric(
+                          horizontal: 4,
+                          vertical: 8,
+                        ),
                       ),
+                      menuItemStyleData: const MenuItemStyleData(height: 40),
                     ),
-                    menuItemStyleData: const MenuItemStyleData(height: 40),
                   ),
                 ),
               ),
-            ),
-            if (controller.visibility == 2)
+              if (controller.visibility == 2)
+                ListTile(
+                  contentPadding: const EdgeInsets.symmetric(horizontal: 24),
+                  leading: Icon(Symbols.person),
+                  trailing: Icon(Symbols.chevron_right),
+                  title: Text('postVisibleUsers').tr(),
+                  subtitle: Text('postSelectedUsers')
+                      .plural(controller.visibleUsers.length),
+                  onTap: () {
+                    _selectVisibleUser(context);
+                  },
+                ),
+              if (controller.visibility == 3)
+                ListTile(
+                  contentPadding: const EdgeInsets.symmetric(horizontal: 24),
+                  leading: Icon(Symbols.person),
+                  trailing: Icon(Symbols.chevron_right),
+                  title: Text('postInvisibleUsers').tr(),
+                  subtitle: Text('postSelectedUsers')
+                      .plural(controller.invisibleUsers.length),
+                  onTap: () {
+                    _selectInvisibleUser(context);
+                  },
+                ),
               ListTile(
-                contentPadding: const EdgeInsets.symmetric(horizontal: 24),
-                leading: Icon(Symbols.person),
-                trailing: Icon(Symbols.chevron_right),
-                title: Text('postVisibleUsers').tr(),
-                subtitle: Text('postSelectedUsers')
-                    .plural(controller.visibleUsers.length),
+                leading: const Icon(Symbols.event_available),
+                title: Text('postPublishedAt').tr(),
+                subtitle: Text(
+                  controller.publishedAt != null
+                      ? dateFormatter.format(controller.publishedAt!)
+                      : 'unset'.tr(),
+                ),
+                trailing: controller.publishedAt != null
+                    ? IconButton(
+                        icon: const Icon(Symbols.cancel),
+                        onPressed: () {
+                          controller.setPublishedAt(null);
+                        },
+                      )
+                    : null,
+                contentPadding: const EdgeInsets.only(left: 24, right: 18),
                 onTap: () {
-                  _selectVisibleUser(context);
+                  _selectDate(
+                    context,
+                    initialDateTime: controller.publishedAt,
+                  ).then((value) {
+                    controller.setPublishedAt(value);
+                  });
                 },
               ),
-            if (controller.visibility == 3)
               ListTile(
-                contentPadding: const EdgeInsets.symmetric(horizontal: 24),
-                leading: Icon(Symbols.person),
-                trailing: Icon(Symbols.chevron_right),
-                title: Text('postInvisibleUsers').tr(),
-                subtitle: Text('postSelectedUsers')
-                    .plural(controller.invisibleUsers.length),
+                leading: const Icon(Symbols.event_busy),
+                title: Text('postPublishedUntil').tr(),
+                subtitle: Text(
+                  controller.publishedUntil != null
+                      ? dateFormatter.format(controller.publishedUntil!)
+                      : 'unset'.tr(),
+                ),
+                trailing: controller.publishedUntil != null
+                    ? IconButton(
+                        icon: const Icon(Symbols.cancel),
+                        onPressed: () {
+                          controller.setPublishedUntil(null);
+                        },
+                      )
+                    : null,
+                contentPadding: const EdgeInsets.only(left: 24, right: 18),
                 onTap: () {
-                  _selectInvisibleUser(context);
+                  _selectDate(
+                    context,
+                    initialDateTime: controller.publishedUntil,
+                  ).then((value) {
+                    controller.setPublishedUntil(value);
+                  });
                 },
               ),
-            ListTile(
-              leading: const Icon(Symbols.event_available),
-              title: Text('postPublishedAt').tr(),
-              subtitle: Text(
-                controller.publishedAt != null
-                    ? dateFormatter.format(controller.publishedAt!)
-                    : 'unset'.tr(),
-              ),
-              trailing: controller.publishedAt != null
-                  ? IconButton(
-                      icon: const Icon(Symbols.cancel),
-                      onPressed: () {
-                        controller.setPublishedAt(null);
-                      },
-                    )
-                  : null,
-              contentPadding: const EdgeInsets.only(left: 24, right: 18),
-              onTap: () {
-                _selectDate(
-                  context,
-                  initialDateTime: controller.publishedAt,
-                ).then((value) {
-                  controller.setPublishedAt(value);
-                });
-              },
-            ),
-            ListTile(
-              leading: const Icon(Symbols.event_busy),
-              title: Text('postPublishedUntil').tr(),
-              subtitle: Text(
-                controller.publishedUntil != null
-                    ? dateFormatter.format(controller.publishedUntil!)
-                    : 'unset'.tr(),
-              ),
-              trailing: controller.publishedUntil != null
-                  ? IconButton(
-                      icon: const Icon(Symbols.cancel),
-                      onPressed: () {
-                        controller.setPublishedUntil(null);
-                      },
-                    )
-                  : null,
-              contentPadding: const EdgeInsets.only(left: 24, right: 18),
-              onTap: () {
-                _selectDate(
-                  context,
-                  initialDateTime: controller.publishedUntil,
-                ).then((value) {
-                  controller.setPublishedUntil(value);
-                });
-              },
-            ),
-          ],
-        ).padding(vertical: 8);
+            ],
+          ).padding(vertical: 8),
+        );
       },
     );
   }
diff --git a/lib/widgets/post/post_tags_field.dart b/lib/widgets/post/post_tags_field.dart
index a81d1bd..7b6c167 100644
--- a/lib/widgets/post/post_tags_field.dart
+++ b/lib/widgets/post/post_tags_field.dart
@@ -21,9 +21,9 @@ class PostTagsField extends StatefulWidget {
   State<PostTagsField> createState() => _PostTagsFieldState();
 }
 
-class _PostTagsFieldState extends State<PostTagsField> {
-  static const List<String> kTagsDividers = [' ', ','];
+const List<String> kTagsDividers = [' ', ','];
 
+class _PostTagsFieldState extends State<PostTagsField> {
   late final _Debounceable<List<String>?, String> _debouncedSearch;
 
   final List<String> _currentTags = List.empty(growable: true);
@@ -155,6 +155,154 @@ class _PostTagsFieldState extends State<PostTagsField> {
   }
 }
 
+class PostCategoriesField extends StatefulWidget {
+  final List<String>? initialCategories;
+  final String labelText;
+  final Function(List<String>) onUpdate;
+
+  const PostCategoriesField({
+    super.key,
+    this.initialCategories,
+    required this.labelText,
+    required this.onUpdate,
+  });
+
+  @override
+  State<PostCategoriesField> createState() => _PostCategoriesFieldState();
+}
+
+class _PostCategoriesFieldState extends State<PostCategoriesField> {
+  late final _Debounceable<List<String>?, String> _debouncedSearch;
+
+  final List<String> _currentCategories = List.empty(growable: true);
+
+  String? _currentSearchProbe;
+  List<String> _lastAutocompleteResult = List.empty();
+  TextEditingController? _textEditingController;
+
+  Future<List<String>?> _searchCategories(String probe) async {
+    _currentSearchProbe = probe;
+
+    final sn = context.read<SnNetworkProvider>();
+    final resp = await sn.client.get(
+      '/cgi/co/categories?take=10&probe=$_currentSearchProbe',
+    );
+
+    if (_currentSearchProbe != probe) {
+      return null;
+    }
+    _currentSearchProbe = null;
+
+    return resp.data.map((x) => x['alias']).toList().cast<String>();
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    _debouncedSearch = _debounce<List<String>?, String>(_searchCategories);
+    if (widget.initialCategories != null) {
+      _currentCategories.addAll(widget.initialCategories!);
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Autocomplete<String>(
+      optionsBuilder: (TextEditingValue textEditingValue) async {
+        final result = await _debouncedSearch(textEditingValue.text);
+        if (result == null) {
+          return _lastAutocompleteResult;
+        }
+        _lastAutocompleteResult = result;
+        return result;
+      },
+      onSelected: (String value) {
+        if (value.isEmpty) return;
+        if (!_currentCategories.contains(value)) {
+          setState(() => _currentCategories.add(value));
+        }
+        _textEditingController?.clear();
+        widget.onUpdate(_currentCategories);
+      },
+      fieldViewBuilder: (context, controller, focusNode, onSubmitted) {
+        _textEditingController = controller;
+        return TextField(
+          controller: controller,
+          focusNode: focusNode,
+          decoration: InputDecoration(
+            label: Text(widget.labelText),
+            border: const UnderlineInputBorder(),
+            prefixIconConstraints: BoxConstraints(
+              maxWidth: MediaQuery.of(context).size.width * 0.75,
+            ),
+            prefixIcon: _currentCategories.isNotEmpty
+                ? SingleChildScrollView(
+              scrollDirection: Axis.horizontal,
+              child: Row(
+                children: _currentCategories.map((String category) {
+                  return Container(
+                    decoration: BoxDecoration(
+                      borderRadius: const BorderRadius.all(
+                        Radius.circular(20.0),
+                      ),
+                      color: Theme.of(context).colorScheme.primary,
+                    ),
+                    margin: const EdgeInsets.only(right: 8),
+                    padding: const EdgeInsets.symmetric(
+                        horizontal: 10.0, vertical: 4.0),
+                    child: Row(
+                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                      children: [
+                        InkWell(
+                          child: Text(
+                            '#$category',
+                            style: const TextStyle(color: Colors.white),
+                          ),
+                        ),
+                        const Gap(4),
+                        InkWell(
+                          child: const Icon(
+                            Icons.cancel,
+                            size: 14.0,
+                            color: Color.fromARGB(255, 233, 233, 233),
+                          ),
+                          onTap: () {
+                            setState(() => _currentCategories.remove(category));
+                            widget.onUpdate(_currentCategories);
+                          },
+                        )
+                      ],
+                    ),
+                  );
+                }).toList(),
+              ),
+            )
+                : null,
+          ),
+          onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
+          onChanged: (value) {
+            for (final divider in kTagsDividers) {
+              if (value.endsWith(divider)) {
+                final tagValue = value.substring(0, value.length - 1);
+                if (tagValue.isEmpty) return;
+                if (!_currentCategories.contains(tagValue)) {
+                  setState(() => _currentCategories.add(tagValue));
+                }
+                controller.clear();
+                widget.onUpdate(_currentCategories);
+                break;
+              }
+            }
+          },
+          onSubmitted: (_) {
+            onSubmitted();
+          },
+        );
+      },
+    );
+  }
+}
+
 typedef _Debounceable<S, T> = Future<S?> Function(T parameter);
 
 _Debounceable<S, T> _debounce<S, T>(_Debounceable<S?, T> function) {