diff --git a/.gitignore b/.gitignore
index 6ae0ae19..c3a8bd70 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,5 +16,6 @@ ui/src/translations
 
 # ide config
 .idea/
+.vscode/
 
 target
diff --git a/server/src/api/user.rs b/server/src/api/user.rs
index c2b6955b..f9a92cd3 100644
--- a/server/src/api/user.rs
+++ b/server/src/api/user.rs
@@ -97,6 +97,7 @@ pub struct SaveUserSettings {
   lang: String,
   avatar: Option<String>,
   email: Option<String>,
+  bio: Option<String>,
   matrix_user_id: Option<String>,
   new_password: Option<String>,
   new_password_verify: Option<String>,
@@ -557,6 +558,17 @@ impl Perform for Oper<SaveUserSettings> {
       None => read_user.email,
     };
 
+    let bio = match &data.bio {
+      Some(bio) => {
+        if bio.chars().count() <= 300 {
+          Some(bio.to_owned())
+        } else {
+          return Err(APIError::err("bio_length_overflow").into());
+        }
+      }
+      None => read_user.bio,
+    };
+
     let avatar = match &data.avatar {
       Some(avatar) => Some(avatar.to_owned()),
       None => read_user.avatar,
@@ -613,7 +625,7 @@ impl Perform for Oper<SaveUserSettings> {
       show_avatars: data.show_avatars,
       send_notifications_to_email: data.send_notifications_to_email,
       actor_id: read_user.actor_id,
-      bio: read_user.bio,
+      bio,
       local: read_user.local,
       private_key: read_user.private_key,
       public_key: read_user.public_key,
diff --git a/ui/src/components/markdown-textarea.tsx b/ui/src/components/markdown-textarea.tsx
index 237ef9ff..002d7c86 100644
--- a/ui/src/components/markdown-textarea.tsx
+++ b/ui/src/components/markdown-textarea.tsx
@@ -21,6 +21,7 @@ interface MarkdownTextAreaProps {
   replyType?: boolean;
   focus?: boolean;
   disabled?: boolean;
+  maxLength?: number;
   onSubmit?(msg: { val: string; formId: string }): any;
   onContentChange?(val: string): any;
   onReplyCancel?(): any;
@@ -121,7 +122,7 @@ export class MarkdownTextArea extends Component<
               required
               disabled={this.props.disabled}
               rows={2}
-              maxLength={10000}
+              maxLength={this.props.maxLength || 10000}
             />
             {this.state.previewMode && (
               <div
diff --git a/ui/src/components/user.tsx b/ui/src/components/user.tsx
index d0dcdf55..9bda8274 100644
--- a/ui/src/components/user.tsx
+++ b/ui/src/components/user.tsx
@@ -31,6 +31,7 @@ import {
   toast,
   setupTippy,
   getLanguage,
+  mdToHtml,
 } from '../utils';
 import { UserListing } from './user-listing';
 import { SortSelect } from './sort-select';
@@ -39,6 +40,7 @@ import { MomentTime } from './moment-time';
 import { i18n } from '../i18next';
 import moment from 'moment';
 import { UserDetails } from './user-details';
+import { MarkdownTextArea } from './markdown-textarea';
 
 interface UserState {
   user: UserView;
@@ -109,6 +111,7 @@ export class User extends Component<any, UserState> {
       show_avatars: null,
       send_notifications_to_email: null,
       auth: null,
+      bio: null,
     },
     userSettingsLoading: null,
     deleteAccountLoading: null,
@@ -149,7 +152,13 @@ export class User extends Component<any, UserState> {
     this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
       this
     );
+    this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
+      this
+    );
     this.handlePageChange = this.handlePageChange.bind(this);
+    this.handleUserSettingsBioChange = this.handleUserSettingsBioChange.bind(
+      this
+    );
 
     this.state.user_id = Number(this.props.match.params.id) || null;
     this.state.username = this.props.match.params.username;
@@ -375,6 +384,12 @@ export class User extends Component<any, UserState> {
                 )}
               </ul>
             </h5>
+            <div className="d-flex align-items-center mb-2">
+              <div
+                className="md-div"
+                dangerouslySetInnerHTML={mdToHtml(user.bio)}
+              />
+            </div>
             <div className="d-flex align-items-center mb-2">
               <svg class="icon">
                 <use xlinkHref="#icon-cake"></use>
@@ -570,6 +585,18 @@ export class User extends Component<any, UserState> {
                   />
                 </div>
               </div>
+              <div class="form-group row">
+                <label class="col-lg-3 col-form-label" htmlFor="user-bio">
+                  {i18n.t('bio')}
+                </label>
+                <div class="col-lg-9">
+                  <MarkdownTextArea
+                    initialContent={this.state.userSettingsForm.bio}
+                    onContentChange={this.handleUserSettingsBioChange}
+                    maxLength={300}
+                  />
+                </div>
+              </div>
               <div class="form-group row">
                 <label class="col-lg-5 col-form-label">
                   <a
@@ -900,6 +927,11 @@ export class User extends Component<any, UserState> {
     i.setState(i.state);
   }
 
+  handleUserSettingsBioChange(val: string) {
+    this.state.userSettingsForm.bio = val;
+    this.setState(this.state);
+  }
+
   handleUserSettingsMatrixUserIdChange(i: User, event: any) {
     i.state.userSettingsForm.matrix_user_id = event.target.value;
     if (
@@ -1057,6 +1089,7 @@ export class User extends Component<any, UserState> {
           this.state.userSettingsForm.lang = UserService.Instance.user.lang;
           this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
           this.state.userSettingsForm.email = this.state.user.email;
+          this.state.userSettingsForm.bio = this.state.user.bio;
           this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
           this.state.userSettingsForm.show_avatars =
             UserService.Instance.user.show_avatars;
@@ -1068,9 +1101,10 @@ export class User extends Component<any, UserState> {
     } else if (res.op == UserOperation.SaveUserSettings) {
       const data = res.data as LoginResponse;
       UserService.Instance.login(data);
-      this.setState({
-        userSettingsLoading: false,
-      });
+      this.state.user.bio = this.state.userSettingsForm.bio;
+      this.state.userSettingsLoading = false;
+      this.setState(this.state);
+
       window.scrollTo(0, 0);
     } else if (res.op == UserOperation.DeleteAccount) {
       this.setState({
diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts
index 87aa400a..9c0e7624 100644
--- a/ui/src/interfaces.ts
+++ b/ui/src/interfaces.ts
@@ -597,6 +597,7 @@ export interface UserSettingsForm {
   lang: string;
   avatar?: string;
   email?: string;
+  bio?: string;
   matrix_user_id?: string;
   new_password?: string;
   new_password_verify?: string;
diff --git a/ui/translations/en.json b/ui/translations/en.json
index 4ee0907a..7304d4a7 100644
--- a/ui/translations/en.json
+++ b/ui/translations/en.json
@@ -228,6 +228,7 @@
     "landing_0":
       "Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>. <14></14> <15>Thank you to our contributors: </15> dessalines, Nutomic, asonix, zacanger, and iav.",
     "not_logged_in": "Not logged in.",
+    "bio_length_overflow": "User bio cannot exceed 300 characters!",
     "logged_in": "Logged in.",
     "must_login": "You must <1>log in or register</1> to comment.",
     "site_saved": "Site Saved.",
@@ -284,5 +285,6 @@
     "cake_day_info": "It's {{ creator_name }}'s cake day today!",
     "invalid_post_title": "Invalid post title",
     "invalid_url": "Invalid URL.",
-    "play_captcha_audio": "Play Captcha Audio"
+    "play_captcha_audio": "Play Captcha Audio",
+    "bio": "Bio"
 }
