diff --git a/frontend/src/lib/components/IconButton.svelte b/frontend/src/lib/components/IconButton.svelte
new file mode 100644
index 0000000..2c1d72e
--- /dev/null
+++ b/frontend/src/lib/components/IconButton.svelte
@@ -0,0 +1,16 @@
+<script lang="ts">
+  import { Button, Icon, Tooltip } from "sveltestrap";
+
+  export let icon: string;
+  export let color: "primary" | "secondary" | "success" | "danger";
+  export let tooltip: string;
+  export let active: boolean = false;
+  export let click: (e: MouseEvent) => void;
+
+  let button: HTMLElement;
+</script>
+
+<Tooltip target={button} placement="top">{tooltip}</Tooltip>
+<Button {color} {active} on:click={click} bind:inner={button}>
+  <Icon name={icon} />
+</Button>
diff --git a/frontend/src/routes/edit/profile/+page.svelte b/frontend/src/routes/edit/profile/+page.svelte
index eb88ab7..28f85f6 100644
--- a/frontend/src/routes/edit/profile/+page.svelte
+++ b/frontend/src/routes/edit/profile/+page.svelte
@@ -14,6 +14,7 @@
   import { Alert, Button, FormGroup, Icon, Input } from "sveltestrap";
   import { encode } from "base64-arraybuffer";
   import { apiFetchClient } from "$lib/api/fetch";
+  import IconButton from "$lib/components/IconButton.svelte";
 
   const MAX_AVATAR_BYTES = 1_000_000;
 
@@ -25,6 +26,7 @@
 
   let bio: string = $userStore?.bio || "";
   let display_name: string = $userStore?.display_name || "";
+  let links: string[] = $userStore ? window.structuredClone($userStore.links) : [];
   let names: FieldEntry[] = $userStore ? window.structuredClone($userStore.names) : [];
   let pronouns: Pronoun[] = $userStore ? window.structuredClone($userStore.pronouns) : [];
   let fields: Field[] = $userStore ? window.structuredClone($userStore.fields) : [];
@@ -32,10 +34,15 @@
   let avatar: string | null;
   let avatar_files: FileList | null;
 
+  let newName = "";
+  let newPronouns = "";
+  let newPronounsDisplay = "";
+  let newLink = "";
+
   let modified = false;
 
   $: redirectIfNoAuth($userStore);
-  $: modified = isModified(bio, display_name, names, pronouns, fields);
+  $: modified = isModified(bio, display_name, links, names, pronouns, fields, avatar);
   $: getAvatar(avatar_files).then((b64) => (avatar = b64));
 
   const redirectIfNoAuth = (user: MeUser | null) => {
@@ -47,14 +54,17 @@
   const isModified = (
     bio: string,
     display_name: string,
+    links: string[],
     names: FieldEntry[],
     pronouns: Pronoun[],
     fields: Field[],
+    avatar: string | null,
   ) => {
     if (!$userStore) return false;
 
     if (bio !== $userStore.bio) return true;
     if (display_name !== $userStore.display_name) return true;
+    if (!linksEqual(links, $userStore.links)) return true;
     if (!fieldsEqual(fields, $userStore.fields)) return true;
     if (!namesEqual(names, $userStore.names)) return true;
     if (!pronounsEqual(pronouns, $userStore.pronouns)) return true;
@@ -92,6 +102,11 @@
     return true;
   };
 
+  const linksEqual = (arr1: string[], arr2: string[]) => {
+    if (arr1.length !== arr2.length) return false;
+    return arr1.every((_, i) => arr1[i] === arr2[i]);
+  };
+
   const getAvatar = async (list: FileList | null) => {
     if (!list || list.length === 0) return null;
     if (list[0].size > MAX_AVATAR_BYTES) return null;
@@ -140,12 +155,53 @@
     pronouns[newIndex] = temp;
   };
 
+  const addName = () => {
+    names = [...names, { value: newName, status: WordStatus.Okay }];
+    newName = "";
+  };
+
+  const addPronouns = () => {
+    pronouns = [
+      ...pronouns,
+      { pronouns: newPronouns, display_text: newPronounsDisplay || null, status: WordStatus.Okay },
+    ];
+    newPronouns = "";
+    newPronounsDisplay = "";
+  };
+
+  const addLink = () => {
+    links = [...links, newLink];
+    newLink = "";
+  };
+
+  const removeName = (index: number) => {
+    if (names.length === 1) names = [];
+    else if (index === 0) names = names.slice(1);
+    else if (index === names.length - 1) names = names.slice(0, names.length - 1);
+    else names = [...names.slice(0, index - 1), ...names.slice(0, index + 1)];
+  };
+
+  const removePronoun = (index: number) => {
+    if (pronouns.length === 1) pronouns = [];
+    else if (index === 0) pronouns = pronouns.slice(1);
+    else if (index === pronouns.length - 1) pronouns = pronouns.slice(0, pronouns.length - 1);
+    else pronouns = [...pronouns.slice(0, index - 1), ...pronouns.slice(0, index + 1)];
+  };
+
+  const removeLink = (index: number) => {
+    if (links.length === 1) links = [];
+    else if (index === 0) links = links.slice(1);
+    else if (index === links.length - 1) links = links.slice(0, links.length - 1);
+    else links = [...links.slice(0, index - 1), ...links.slice(0, index + 1)];
+  };
+
   const updateUser = async () => {
     try {
       const resp = await apiFetchClient<MeUser>("/users/@me", "PATCH", {
         display_name,
         avatar,
         bio,
+        links,
         names,
         pronouns,
         fields,
@@ -227,46 +283,71 @@
               <Icon name="chevron-down" />
             </Button>
             <input type="text" class="form-control" bind:value={names[index].value} />
-            <Button
+            <IconButton
               color="secondary"
-              on:click={() => (names[index].status = WordStatus.Favourite)}
+              icon="heart-fill"
+              tooltip="Favourite"
+              click={() => (names[index].status = WordStatus.Favourite)}
               active={names[index].status === WordStatus.Favourite}
-            >
-              <Icon name="heart-fill" />
-            </Button>
-            <Button
+            />
+            <IconButton
               color="secondary"
-              on:click={() => (names[index].status = WordStatus.Okay)}
+              icon="hand-thumbs-up"
+              tooltip="Okay"
+              click={() => (names[index].status = WordStatus.Okay)}
               active={names[index].status === WordStatus.Okay}
-            >
-              <Icon name="hand-thumbs-up" />
-            </Button>
-            <Button
+            />
+            <IconButton
               color="secondary"
-              on:click={() => (names[index].status = WordStatus.Jokingly)}
+              icon="emoji-laughing"
+              tooltip="Jokingly"
+              click={() => (names[index].status = WordStatus.Jokingly)}
               active={names[index].status === WordStatus.Jokingly}
-            >
-              <Icon name="emoji-laughing" />
-            </Button>
-            <Button
+            />
+            <IconButton
               color="secondary"
-              on:click={() => (names[index].status = WordStatus.FriendsOnly)}
+              icon="people"
+              tooltip="Friends only"
+              click={() => (names[index].status = WordStatus.FriendsOnly)}
               active={names[index].status === WordStatus.FriendsOnly}
-            >
-              <Icon name="people" />
-            </Button>
-            <Button
+            />
+            <IconButton
               color="secondary"
-              on:click={() => (names[index].status = WordStatus.Avoid)}
+              icon="hand-thumbs-down"
+              tooltip="Avoid"
+              click={() => (names[index].status = WordStatus.Avoid)}
               active={names[index].status === WordStatus.Avoid}
-            >
-              <Icon name="hand-thumbs-down" />
-            </Button>
-            <Button color="danger">
-              <Icon name="trash3" />
-            </Button>
+            />
+            <IconButton
+              color="danger"
+              icon="trash3"
+              tooltip="Remove name"
+              click={() => removeName(index)}
+            />
           </div>
         {/each}
+        <div class="input-group m-1">
+          <input type="text" class="form-control" bind:value={newName} />
+          <IconButton color="success" icon="plus" tooltip="Add name" click={() => addName()} />
+        </div>
+      </div>
+      <div class="col-md">
+        <h4>Links</h4>
+        {#each links as _, index}
+          <div class="input-group m-1">
+            <input type="text" class="form-control" bind:value={links[index]} />
+            <IconButton
+              color="danger"
+              icon="trash3"
+              tooltip="Remove link"
+              click={() => removeLink(index)}
+            />
+          </div>
+        {/each}
+        <div class="input-group m-1">
+          <input type="text" class="form-control" bind:value={newLink} />
+          <IconButton color="success" icon="plus" tooltip="Add link" click={() => addLink()} />
+        </div>
       </div>
     </div>
     <div class="row m-1">
@@ -282,46 +363,59 @@
             </Button>
             <input type="text" class="form-control" bind:value={pronouns[index].pronouns} />
             <input type="text" class="form-control" bind:value={pronouns[index].display_text} />
-            <Button
+            <IconButton
               color="secondary"
-              on:click={() => (pronouns[index].status = WordStatus.Favourite)}
+              icon="heart-fill"
+              tooltip="Favourite"
+              click={() => (pronouns[index].status = WordStatus.Favourite)}
               active={pronouns[index].status === WordStatus.Favourite}
-            >
-              <Icon name="heart-fill" />
-            </Button>
-            <Button
+            />
+            <IconButton
               color="secondary"
-              on:click={() => (pronouns[index].status = WordStatus.Okay)}
+              icon="hand-thumbs-up"
+              tooltip="Okay"
+              click={() => (pronouns[index].status = WordStatus.Okay)}
               active={pronouns[index].status === WordStatus.Okay}
-            >
-              <Icon name="hand-thumbs-up" />
-            </Button>
-            <Button
+            />
+            <IconButton
               color="secondary"
-              on:click={() => (pronouns[index].status = WordStatus.Jokingly)}
+              icon="emoji-laughing"
+              tooltip="Jokingly"
+              click={() => (pronouns[index].status = WordStatus.Jokingly)}
               active={pronouns[index].status === WordStatus.Jokingly}
-            >
-              <Icon name="emoji-laughing" />
-            </Button>
-            <Button
+            />
+            <IconButton
               color="secondary"
-              on:click={() => (pronouns[index].status = WordStatus.FriendsOnly)}
+              icon="people"
+              tooltip="Friends only"
+              click={() => (pronouns[index].status = WordStatus.FriendsOnly)}
               active={pronouns[index].status === WordStatus.FriendsOnly}
-            >
-              <Icon name="people" />
-            </Button>
-            <Button
+            />
+            <IconButton
               color="secondary"
-              on:click={() => (pronouns[index].status = WordStatus.Avoid)}
+              icon="hand-thumbs-down"
+              tooltip="Avoid"
+              click={() => (pronouns[index].status = WordStatus.Avoid)}
               active={pronouns[index].status === WordStatus.Avoid}
-            >
-              <Icon name="hand-thumbs-down" />
-            </Button>
-            <Button color="danger">
-              <Icon name="trash3" />
-            </Button>
+            />
+            <IconButton
+              color="danger"
+              icon="trash3"
+              tooltip="Remove pronouns"
+              click={() => removePronoun(index)}
+            />
           </div>
         {/each}
+        <div class="input-group m-1">
+          <input type="text" class="form-control" bind:value={newPronouns} />
+          <input type="text" class="form-control" bind:value={newPronounsDisplay} />
+          <IconButton
+            color="success"
+            icon="plus"
+            tooltip="Add pronouns"
+            click={() => addPronouns()}
+          />
+        </div>
       </div>
     </div>
   </div>