diff --git a/packages/client/src/components/global/page-header.vue b/packages/client/src/components/global/page-header.vue
index a080c39dde..5395a87961 100644
--- a/packages/client/src/components/global/page-header.vue
+++ b/packages/client/src/components/global/page-header.vue
@@ -34,7 +34,7 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, onMounted, onUnmounted, ref, inject, watch } from 'vue';
+import { computed, onMounted, onUnmounted, ref, inject, watch, shallowReactive, nextTick, reactive } from 'vue';
 import tinycolor from 'tinycolor2';
 import { popupMenu } from '@/os';
 import { scrollToTop } from '@/scripts/scroll';
@@ -137,16 +137,18 @@ onMounted(() => {
 	calcBg();
 	globalEvents.on('themeChanged', calcBg);
 
-	watch(() => props.tab, () => {
-		const tabEl = tabRefs[props.tab];
-		if (tabEl && tabHighlightEl) {
-			// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
-			// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
-			const parentRect = tabEl.parentElement.getBoundingClientRect();
-			const rect = tabEl.getBoundingClientRect();
-			tabHighlightEl.style.width = rect.width + 'px';
-			tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px';
-		}
+	watch(() => [props.tab, props.tabs], () => {
+		nextTick(() => {
+			const tabEl = tabRefs[props.tab];
+			if (tabEl && tabHighlightEl) {
+				// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
+				// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
+				const parentRect = tabEl.parentElement.getBoundingClientRect();
+				const rect = tabEl.getBoundingClientRect();
+				tabHighlightEl.style.width = rect.width + 'px';
+				tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px';
+			}
+		});
 	}, {
 		immediate: true,
 	});
diff --git a/packages/client/src/pages/admin/_header_.vue b/packages/client/src/pages/admin/_header_.vue
index 1883b4abee..1c3cdcb51f 100644
--- a/packages/client/src/pages/admin/_header_.vue
+++ b/packages/client/src/pages/admin/_header_.vue
@@ -28,7 +28,7 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, onMounted, onUnmounted, ref, inject, watch } from 'vue';
+import { computed, onMounted, onUnmounted, ref, inject, watch, nextTick } from 'vue';
 import tinycolor from 'tinycolor2';
 import { popupMenu } from '@/os';
 import { url } from '@/config';
@@ -126,16 +126,18 @@ onMounted(() => {
 	calcBg();
 	globalEvents.on('themeChanged', calcBg);
 
-	watch(() => props.tab, () => {
-		const tabEl = tabRefs[props.tab];
-		if (tabEl && tabHighlightEl) {
-			// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
-			// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
-			const parentRect = tabEl.parentElement.getBoundingClientRect();
-			const rect = tabEl.getBoundingClientRect();
-			tabHighlightEl.style.width = rect.width + 'px';
-			tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px';
-		}
+	watch(() => [props.tab, props.tabs], () => {
+		nextTick(() => {
+			const tabEl = tabRefs[props.tab];
+			if (tabEl && tabHighlightEl) {
+				// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
+				// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
+				const parentRect = tabEl.parentElement.getBoundingClientRect();
+				const rect = tabEl.getBoundingClientRect();
+				tabHighlightEl.style.width = rect.width + 'px';
+				tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px';
+			}
+		});
 	}, {
 		immediate: true,
 	});
diff --git a/packages/client/src/pages/user/followers.vue b/packages/client/src/pages/user/followers.vue
new file mode 100644
index 0000000000..3feec15e4a
--- /dev/null
+++ b/packages/client/src/pages/user/followers.vue
@@ -0,0 +1,62 @@
+<template>
+<MkStickyContainer>
+	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
+	<MkSpacer :content-max="1000">
+		<transition name="fade" mode="out-in">
+			<div v-if="user">
+				<XFollowList :user="user" type="following"/>
+			</div>
+			<MkError v-else-if="error" @retry="fetch()"/>
+			<MkLoading v-else/>
+		</transition>
+	</MkSpacer>
+</MkStickyContainer>
+</template>
+
+<script lang="ts" setup>
+import { defineAsyncComponent, computed, inject, onMounted, onUnmounted, watch } from 'vue';
+import * as Acct from 'misskey-js/built/acct';
+import * as misskey from 'misskey-js';
+import XFollowList from './follow-list.vue';
+import * as os from '@/os';
+import { definePageMetadata } from '@/scripts/page-metadata';
+import { i18n } from '@/i18n';
+
+const props = withDefaults(defineProps<{
+	acct: string;
+}>(), {
+});
+
+let user = $ref<null | misskey.entities.UserDetailed>(null);
+let error = $ref(null);
+
+function fetchUser(): void {
+	if (props.acct == null) return;
+	user = null;
+	os.api('users/show', Acct.parse(props.acct)).then(u => {
+		user = u;
+	}).catch(err => {
+		error = err;
+	});
+}
+
+watch(() => props.acct, fetchUser, {
+	immediate: true,
+});
+
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata(computed(() => user ? {
+	icon: 'fas fa-user',
+	title: user.name ? `${user.name} (@${user.username})` : `@${user.username}`,
+	subtitle: i18n.ts.followers,
+	userName: user,
+	avatar: user,
+	bg: 'var(--bg)',
+} : null));
+</script>
+
+<style lang="scss" scoped>
+</style>
diff --git a/packages/client/src/pages/user/following.vue b/packages/client/src/pages/user/following.vue
new file mode 100644
index 0000000000..0c6bb1c9f4
--- /dev/null
+++ b/packages/client/src/pages/user/following.vue
@@ -0,0 +1,62 @@
+<template>
+<MkStickyContainer>
+	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
+	<MkSpacer :content-max="1000">
+		<transition name="fade" mode="out-in">
+			<div v-if="user">
+				<XFollowList :user="user" type="following"/>
+			</div>
+			<MkError v-else-if="error" @retry="fetch()"/>
+			<MkLoading v-else/>
+		</transition>
+	</MkSpacer>
+</MkStickyContainer>
+</template>
+
+<script lang="ts" setup>
+import { defineAsyncComponent, computed, inject, onMounted, onUnmounted, watch } from 'vue';
+import * as Acct from 'misskey-js/built/acct';
+import * as misskey from 'misskey-js';
+import XFollowList from './follow-list.vue';
+import * as os from '@/os';
+import { definePageMetadata } from '@/scripts/page-metadata';
+import { i18n } from '@/i18n';
+
+const props = withDefaults(defineProps<{
+	acct: string;
+}>(), {
+});
+
+let user = $ref<null | misskey.entities.UserDetailed>(null);
+let error = $ref(null);
+
+function fetchUser(): void {
+	if (props.acct == null) return;
+	user = null;
+	os.api('users/show', Acct.parse(props.acct)).then(u => {
+		user = u;
+	}).catch(err => {
+		error = err;
+	});
+}
+
+watch(() => props.acct, fetchUser, {
+	immediate: true,
+});
+
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata(computed(() => user ? {
+	icon: 'fas fa-user',
+	title: user.name ? `${user.name} (@${user.username})` : `@${user.username}`,
+	subtitle: i18n.ts.following,
+	userName: user,
+	avatar: user,
+	bg: 'var(--bg)',
+} : null));
+</script>
+
+<style lang="scss" scoped>
+</style>
diff --git a/packages/client/src/pages/user/home.vue b/packages/client/src/pages/user/home.vue
new file mode 100644
index 0000000000..f7c25f077c
--- /dev/null
+++ b/packages/client/src/pages/user/home.vue
@@ -0,0 +1,478 @@
+<template>
+<MkSpacer :content-max="narrow ? 800 : 1100">
+	<div ref="rootEl" v-size="{ max: [500] }" class="ftskorzw" :class="{ wide: !narrow }">
+		<div class="main">
+			<!-- TODO -->
+			<!-- <div class="punished" v-if="user.isSuspended"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSuspended }}</div> -->
+			<!-- <div class="punished" v-if="user.isSilenced"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSilenced }}</div> -->
+
+			<div class="profile">
+				<MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/>
+
+				<div :key="user.id" class="_block main">
+					<div class="banner-container" :style="style">
+						<div ref="bannerEl" class="banner" :style="style"></div>
+						<div class="fade"></div>
+						<div class="title">
+							<MkUserName class="name" :user="user" :nowrap="true"/>
+							<div class="bottom">
+								<span class="username"><MkAcct :user="user" :detail="true"/></span>
+								<span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span>
+								<span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span>
+								<span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span>
+								<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span>
+							</div>
+						</div>
+						<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ $ts.followsYou }}</span>
+						<div v-if="$i" class="actions">
+							<button class="menu _button" @click="menu"><i class="fas fa-ellipsis-h"></i></button>
+							<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
+						</div>
+					</div>
+					<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
+					<div class="title">
+						<MkUserName :user="user" :nowrap="false" class="name"/>
+						<div class="bottom">
+							<span class="username"><MkAcct :user="user" :detail="true"/></span>
+							<span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span>
+							<span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span>
+							<span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span>
+							<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span>
+						</div>
+					</div>
+					<div class="description">
+						<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/>
+						<p v-else class="empty">{{ $ts.noAccountDescription }}</p>
+					</div>
+					<div class="fields system">
+						<dl v-if="user.location" class="field">
+							<dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt>
+							<dd class="value">{{ user.location }}</dd>
+						</dl>
+						<dl v-if="user.birthday" class="field">
+							<dt class="name"><i class="fas fa-birthday-cake fa-fw"></i> {{ $ts.birthday }}</dt>
+							<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
+						</dl>
+						<dl class="field">
+							<dt class="name"><i class="fas fa-calendar-alt fa-fw"></i> {{ $ts.registeredDate }}</dt>
+							<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd>
+						</dl>
+					</div>
+					<div v-if="user.fields.length > 0" class="fields">
+						<dl v-for="(field, i) in user.fields" :key="i" class="field">
+							<dt class="name">
+								<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
+							</dt>
+							<dd class="value">
+								<Mfm :text="field.value" :author="user" :i="$i" :custom-emojis="user.emojis" :colored="false"/>
+							</dd>
+						</dl>
+					</div>
+					<div class="status">
+						<MkA v-click-anime :to="userPage(user)" :class="{ active: page === 'index' }">
+							<b>{{ number(user.notesCount) }}</b>
+							<span>{{ $ts.notes }}</span>
+						</MkA>
+						<MkA v-click-anime :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
+							<b>{{ number(user.followingCount) }}</b>
+							<span>{{ $ts.following }}</span>
+						</MkA>
+						<MkA v-click-anime :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
+							<b>{{ number(user.followersCount) }}</b>
+							<span>{{ $ts.followers }}</span>
+						</MkA>
+					</div>
+				</div>
+			</div>
+
+			<div class="contents">
+				<div v-if="user.pinnedNotes.length > 0" class="_gap">
+					<XNote v-for="note in user.pinnedNotes" :key="note.id" class="note _block" :note="note" :pinned="true"/>
+				</div>
+				<MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo>
+				<template v-if="narrow">
+					<XPhotos :key="user.id" :user="user"/>
+					<XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/>
+				</template>
+			</div>
+			<div>
+				<XUserTimeline :user="user"/>
+			</div>
+		</div>
+		<div v-if="!narrow" class="sub">
+			<XPhotos :key="user.id" :user="user"/>
+			<XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/>
+		</div>
+	</div>
+</MkSpacer>
+</template>
+
+<script lang="ts" setup>
+import { defineAsyncComponent, computed, inject, onMounted, onUnmounted, watch } from 'vue';
+import calcAge from 's-age';
+import * as misskey from 'misskey-js';
+import XUserTimeline from './index.timeline.vue';
+import XNote from '@/components/note.vue';
+import MkFollowButton from '@/components/follow-button.vue';
+import MkContainer from '@/components/ui/container.vue';
+import MkFolder from '@/components/ui/folder.vue';
+import MkRemoteCaution from '@/components/remote-caution.vue';
+import MkTab from '@/components/tab.vue';
+import MkInfo from '@/components/ui/info.vue';
+import { getScrollPosition } from '@/scripts/scroll';
+import { getUserMenu } from '@/scripts/get-user-menu';
+import number from '@/filters/number';
+import { userPage, acct as getAcct } from '@/filters/user';
+import * as os from '@/os';
+import { useRouter } from '@/router';
+import { i18n } from '@/i18n';
+import { $i } from '@/account';
+
+const XPhotos = defineAsyncComponent(() => import('./index.photos.vue'));
+const XActivity = defineAsyncComponent(() => import('./index.activity.vue'));
+
+const props = withDefaults(defineProps<{
+	user: misskey.entities.UserDetailed;
+}>(), {
+});
+
+const router = useRouter();
+
+let parallaxAnimationId = $ref<null | number>(null);
+let narrow = $ref<null | boolean>(null);
+let rootEl = $ref<null | HTMLElement>(null);
+let bannerEl = $ref<null | HTMLElement>(null);
+
+const style = $computed(() => {
+	if (props.user.bannerUrl == null) return {};
+	return {
+		backgroundImage: `url(${ props.user.bannerUrl })`,
+	};
+});
+
+const age = $computed(() => {
+	return calcAge(props.user.birthday);
+});
+
+function menu(ev) {
+	os.popupMenu(getUserMenu(props.user), ev.currentTarget ?? ev.target);
+}
+
+function parallaxLoop() {
+	parallaxAnimationId = window.requestAnimationFrame(parallaxLoop);
+	parallax();
+}
+
+function parallax() {
+	const banner = bannerEl as any;
+	if (banner == null) return;
+
+	const top = getScrollPosition(rootEl);
+
+	if (top < 0) return;
+
+	const z = 1.75; // 奥行き(小さいほど奥)
+	const pos = -(top / z);
+	banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
+}
+
+onMounted(() => {
+	window.requestAnimationFrame(parallaxLoop);
+	narrow = rootEl!.clientWidth < 1000;
+});
+
+onUnmounted(() => {
+	if (parallaxAnimationId) {
+		window.cancelAnimationFrame(parallaxAnimationId);
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ftskorzw {
+
+	> .main {
+
+		> .punished {
+			font-size: 0.8em;
+			padding: 16px;
+		}
+
+		> .profile {
+
+			> .main {
+				position: relative;
+				overflow: hidden;
+
+				> .banner-container {
+					position: relative;
+					height: 250px;
+					overflow: hidden;
+					background-size: cover;
+					background-position: center;
+
+					> .banner {
+						height: 100%;
+						background-color: #4c5e6d;
+						background-size: cover;
+						background-position: center;
+						box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
+						will-change: background-position;
+					}
+
+					> .fade {
+						position: absolute;
+						bottom: 0;
+						left: 0;
+						width: 100%;
+						height: 78px;
+						background: linear-gradient(transparent, rgba(#000, 0.7));
+					}
+
+					> .followed {
+						position: absolute;
+						top: 12px;
+						left: 12px;
+						padding: 4px 8px;
+						color: #fff;
+						background: rgba(0, 0, 0, 0.7);
+						font-size: 0.7em;
+						border-radius: 6px;
+					}
+
+					> .actions {
+						position: absolute;
+						top: 12px;
+						right: 12px;
+						-webkit-backdrop-filter: var(--blur, blur(8px));
+						backdrop-filter: var(--blur, blur(8px));
+						background: rgba(0, 0, 0, 0.2);
+						padding: 8px;
+						border-radius: 24px;
+
+						> .menu {
+							vertical-align: bottom;
+							height: 31px;
+							width: 31px;
+							color: #fff;
+							text-shadow: 0 0 8px #000;
+							font-size: 16px;
+						}
+
+						> .koudoku {
+							margin-left: 4px;
+							vertical-align: bottom;
+						}
+					}
+
+					> .title {
+						position: absolute;
+						bottom: 0;
+						left: 0;
+						width: 100%;
+						padding: 0 0 8px 154px;
+						box-sizing: border-box;
+						color: #fff;
+
+						> .name {
+							display: block;
+							margin: 0;
+							line-height: 32px;
+							font-weight: bold;
+							font-size: 1.8em;
+							text-shadow: 0 0 8px #000;
+						}
+
+						> .bottom {
+							> * {
+								display: inline-block;
+								margin-right: 16px;
+								line-height: 20px;
+								opacity: 0.8;
+
+								&.username {
+									font-weight: bold;
+								}
+							}
+						}
+					}
+				}
+
+				> .title {
+					display: none;
+					text-align: center;
+					padding: 50px 8px 16px 8px;
+					font-weight: bold;
+					border-bottom: solid 0.5px var(--divider);
+
+					> .bottom {
+						> * {
+							display: inline-block;
+							margin-right: 8px;
+							opacity: 0.8;
+						}
+					}
+				}
+
+				> .avatar {
+					display: block;
+					position: absolute;
+					top: 170px;
+					left: 16px;
+					z-index: 2;
+					width: 120px;
+					height: 120px;
+					box-shadow: 1px 1px 3px rgba(#000, 0.2);
+				}
+
+				> .description {
+					padding: 24px 24px 24px 154px;
+					font-size: 0.95em;
+
+					> .empty {
+						margin: 0;
+						opacity: 0.5;
+					}
+				}
+
+				> .fields {
+					padding: 24px;
+					font-size: 0.9em;
+					border-top: solid 0.5px var(--divider);
+
+					> .field {
+						display: flex;
+						padding: 0;
+						margin: 0;
+						align-items: center;
+
+						&:not(:last-child) {
+							margin-bottom: 8px;
+						}
+
+						> .name {
+							width: 30%;
+							overflow: hidden;
+							white-space: nowrap;
+							text-overflow: ellipsis;
+							font-weight: bold;
+							text-align: center;
+						}
+
+						> .value {
+							width: 70%;
+							overflow: hidden;
+							white-space: nowrap;
+							text-overflow: ellipsis;
+							margin: 0;
+						}
+					}
+
+					&.system > .field > .name {
+					}
+				}
+
+				> .status {
+					display: flex;
+					padding: 24px;
+					border-top: solid 0.5px var(--divider);
+
+					> a {
+						flex: 1;
+						text-align: center;
+
+						&.active {
+							color: var(--accent);
+						}
+
+						&:hover {
+							text-decoration: none;
+						}
+
+						> b {
+							display: block;
+							line-height: 16px;
+						}
+
+						> span {
+							font-size: 70%;
+						}
+					}
+				}
+			}
+		}
+
+		> .contents {
+			> .content {
+				margin-bottom: var(--margin);
+			}
+		}
+	}
+
+	&.max-width_500px {
+		> .main {
+			> .profile > .main {
+				> .banner-container {
+					height: 140px;
+
+					> .fade {
+						display: none;
+					}
+
+					> .title {
+						display: none;
+					}
+				}
+
+				> .title {
+					display: block;
+				}
+
+				> .avatar {
+					top: 90px;
+					left: 0;
+					right: 0;
+					width: 92px;
+					height: 92px;
+					margin: auto;
+				}
+
+				> .description {
+					padding: 16px;
+					text-align: center;
+				}
+
+				> .fields {
+					padding: 16px;
+				}
+
+				> .status {
+					padding: 16px;
+				}
+			}
+
+			> .contents {
+				> .nav {
+					font-size: 80%;
+				}
+			}
+		}
+	}
+
+	&.wide {
+		display: flex;
+		width: 100%;
+
+		> .main {
+			width: 100%;
+			min-width: 0;
+		}
+
+		> .sub {
+			max-width: 350px;
+			min-width: 350px;
+			margin-left: var(--margin);
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/user/index.vue b/packages/client/src/pages/user/index.vue
index 7b2e2cde1a..bd1bb11a5b 100644
--- a/packages/client/src/pages/user/index.vue
+++ b/packages/client/src/pages/user/index.vue
@@ -1,124 +1,15 @@
 <template>
 <MkStickyContainer>
-	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
-	<div ref="rootEl">
+	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
+	<div>
 		<transition name="fade" mode="out-in">
-			<MkSpacer v-if="user" :content-max="narrow ? 800 : 1100">
-				<div v-size="{ max: [500] }" class="ftskorzw" :class="{ wide: !narrow }">
-					<div class="main">
-						<!-- TODO -->
-						<!-- <div class="punished" v-if="user.isSuspended"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSuspended }}</div> -->
-						<!-- <div class="punished" v-if="user.isSilenced"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSilenced }}</div> -->
-
-						<div class="profile">
-							<MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/>
-
-							<div :key="user.id" class="_block main">
-								<div class="banner-container" :style="style">
-									<div ref="bannerEl" class="banner" :style="style"></div>
-									<div class="fade"></div>
-									<div class="title">
-										<MkUserName class="name" :user="user" :nowrap="true"/>
-										<div class="bottom">
-											<span class="username"><MkAcct :user="user" :detail="true"/></span>
-											<span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span>
-											<span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span>
-											<span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span>
-											<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span>
-										</div>
-									</div>
-									<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ $ts.followsYou }}</span>
-									<div v-if="$i" class="actions">
-										<button class="menu _button" @click="menu"><i class="fas fa-ellipsis-h"></i></button>
-										<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
-									</div>
-								</div>
-								<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
-								<div class="title">
-									<MkUserName :user="user" :nowrap="false" class="name"/>
-									<div class="bottom">
-										<span class="username"><MkAcct :user="user" :detail="true"/></span>
-										<span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span>
-										<span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span>
-										<span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span>
-										<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span>
-									</div>
-								</div>
-								<div class="description">
-									<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/>
-									<p v-else class="empty">{{ $ts.noAccountDescription }}</p>
-								</div>
-								<div class="fields system">
-									<dl v-if="user.location" class="field">
-										<dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt>
-										<dd class="value">{{ user.location }}</dd>
-									</dl>
-									<dl v-if="user.birthday" class="field">
-										<dt class="name"><i class="fas fa-birthday-cake fa-fw"></i> {{ $ts.birthday }}</dt>
-										<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
-									</dl>
-									<dl class="field">
-										<dt class="name"><i class="fas fa-calendar-alt fa-fw"></i> {{ $ts.registeredDate }}</dt>
-										<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd>
-									</dl>
-								</div>
-								<div v-if="user.fields.length > 0" class="fields">
-									<dl v-for="(field, i) in user.fields" :key="i" class="field">
-										<dt class="name">
-											<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
-										</dt>
-										<dd class="value">
-											<Mfm :text="field.value" :author="user" :i="$i" :custom-emojis="user.emojis" :colored="false"/>
-										</dd>
-									</dl>
-								</div>
-								<div class="status">
-									<MkA v-click-anime :to="userPage(user)" :class="{ active: page === 'index' }">
-										<b>{{ number(user.notesCount) }}</b>
-										<span>{{ $ts.notes }}</span>
-									</MkA>
-									<MkA v-click-anime :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
-										<b>{{ number(user.followingCount) }}</b>
-										<span>{{ $ts.following }}</span>
-									</MkA>
-									<MkA v-click-anime :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
-										<b>{{ number(user.followersCount) }}</b>
-										<span>{{ $ts.followers }}</span>
-									</MkA>
-								</div>
-							</div>
-						</div>
-
-						<div class="contents">
-							<template v-if="page === 'index'">
-								<div>
-									<div v-if="user.pinnedNotes.length > 0" class="_gap">
-										<XNote v-for="note in user.pinnedNotes" :key="note.id" class="note _block" :note="note" :pinned="true"/>
-									</div>
-									<MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo>
-									<template v-if="narrow">
-										<XPhotos :key="user.id" :user="user"/>
-										<XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/>
-									</template>
-								</div>
-								<div>
-									<XUserTimeline :user="user"/>
-								</div>
-							</template>
-							<XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_content _gap"/>
-							<XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_content _gap"/>
-							<XReactions v-else-if="page === 'reactions'" :user="user" class="_gap"/>
-							<XClips v-else-if="page === 'clips'" :user="user" class="_gap"/>
-							<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/>
-							<XGallery v-else-if="page === 'gallery'" :user="user" class="_gap"/>
-						</div>
-					</div>
-					<div v-if="!narrow" class="sub">
-						<XPhotos :key="user.id" :user="user"/>
-						<XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/>
-					</div>
-				</div>
-			</MkSpacer>
+			<div v-if="user">
+				<XHome v-if="tab === 'home'" :user="user"/>
+				<XReactions v-else-if="tab === 'reactions'" :user="user"/>
+				<XClips v-else-if="tab === 'clips'" :user="user"/>
+				<XPages v-else-if="tab === 'pages'" :user="user"/>
+				<XGallery v-else-if="tab === 'gallery'" :user="user"/>
+			</div>
 			<MkError v-else-if="error" @retry="fetch()"/>
 			<MkLoading v-else/>
 		</transition>
@@ -131,14 +22,6 @@ import { defineAsyncComponent, computed, inject, onMounted, onUnmounted, watch }
 import calcAge from 's-age';
 import * as Acct from 'misskey-js/built/acct';
 import * as misskey from 'misskey-js';
-import XUserTimeline from './index.timeline.vue';
-import XNote from '@/components/note.vue';
-import MkFollowButton from '@/components/follow-button.vue';
-import MkContainer from '@/components/ui/container.vue';
-import MkFolder from '@/components/ui/folder.vue';
-import MkRemoteCaution from '@/components/remote-caution.vue';
-import MkTab from '@/components/tab.vue';
-import MkInfo from '@/components/ui/info.vue';
 import { getScrollPosition } from '@/scripts/scroll';
 import { getUserMenu } from '@/scripts/get-user-menu';
 import number from '@/filters/number';
@@ -149,41 +32,24 @@ import { definePageMetadata } from '@/scripts/page-metadata';
 import { i18n } from '@/i18n';
 import { $i } from '@/account';
 
-const XFollowList = defineAsyncComponent(() => import('./follow-list.vue'));
+const XHome = defineAsyncComponent(() => import('./home.vue'));
 const XReactions = defineAsyncComponent(() => import('./reactions.vue'));
 const XClips = defineAsyncComponent(() => import('./clips.vue'));
 const XPages = defineAsyncComponent(() => import('./pages.vue'));
 const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
-const XPhotos = defineAsyncComponent(() => import('./index.photos.vue'));
-const XActivity = defineAsyncComponent(() => import('./index.activity.vue'));
 
 const props = withDefaults(defineProps<{
 	acct: string;
 	page?: string;
 }>(), {
-	page: 'index',
+	page: 'home',
 });
 
 const router = useRouter();
 
+let tab = $ref(props.page);
 let user = $ref<null | misskey.entities.UserDetailed>(null);
 let error = $ref(null);
-let parallaxAnimationId = $ref<null | number>(null);
-let narrow = $ref<null | boolean>(null);
-let rootEl = $ref<null | HTMLElement>(null);
-let bannerEl = $ref<null | HTMLElement>(null);
-
-const style = $computed(() => {
-	if (user?.bannerUrl == null) return {};
-	return {
-		backgroundImage: `url(${ user.bannerUrl })`,
-	};
-});
-
-const age = $computed(() => {
-	if (user == null) return null;
-	return calcAge(user.birthday);
-});
 
 function fetchUser(): void {
 	if (props.acct == null) return;
@@ -203,62 +69,28 @@ function menu(ev) {
 	os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target);
 }
 
-function parallaxLoop() {
-	parallaxAnimationId = window.requestAnimationFrame(parallaxLoop);
-	parallax();
-}
-
-function parallax() {
-	const banner = bannerEl as any;
-	if (banner == null) return;
-
-	const top = getScrollPosition(rootEl);
-
-	if (top < 0) return;
-
-	const z = 1.75; // 奥行き(小さいほど奥)
-	const pos = -(top / z);
-	banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
-}
-
-onMounted(() => {
-	window.requestAnimationFrame(parallaxLoop);
-	narrow = rootEl!.clientWidth < 1000;
-});
-
-onUnmounted(() => {
-	if (parallaxAnimationId) {
-		window.cancelAnimationFrame(parallaxAnimationId);
-	}
-});
-
 const headerActions = $computed(() => []);
 
 const headerTabs = $computed(() => user ? [{
-	active: props.page === 'index',
+	key: 'home',
 	title: i18n.ts.overview,
 	icon: 'fas fa-home',
-	onClick: () => { router.push('/@' + getAcct(user)); },
 }, ...($i && ($i.id === user.id)) || user.publicReactions ? [{
-	active: props.page === 'reactions',
+	key: 'reactions',
 	title: i18n.ts.reaction,
 	icon: 'fas fa-laugh',
-	onClick: () => { router.push('/@' + getAcct(user) + '/reactions'); },
 }] : [], {
-	active: props.page === 'clips',
+	key: 'clips',
 	title: i18n.ts.clips,
 	icon: 'fas fa-paperclip',
-	onClick: () => { router.push('/@' + getAcct(user) + '/clips'); },
 }, {
-	active: props.page === 'pages',
+	key: 'pages',
 	title: i18n.ts.pages,
 	icon: 'fas fa-file-alt',
-	onClick: () => { router.push('/@' + getAcct(user) + '/pages'); },
 }, {
-	active: props.page === 'gallery',
+	key: 'gallery',
 	title: i18n.ts.gallery,
 	icon: 'fas fa-icons',
-	onClick: () => { router.push('/@' + getAcct(user) + '/gallery'); },
 }] : null);
 
 definePageMetadata(computed(() => user ? {
@@ -284,291 +116,4 @@ definePageMetadata(computed(() => user ? {
 .fade-leave-to {
 	opacity: 0;
 }
-
-.ftskorzw {
-
-	> .main {
-
-		> .punished {
-			font-size: 0.8em;
-			padding: 16px;
-		}
-
-		> .profile {
-
-			> .main {
-				position: relative;
-				overflow: hidden;
-
-				> .banner-container {
-					position: relative;
-					height: 250px;
-					overflow: hidden;
-					background-size: cover;
-					background-position: center;
-
-					> .banner {
-						height: 100%;
-						background-color: #4c5e6d;
-						background-size: cover;
-						background-position: center;
-						box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
-						will-change: background-position;
-					}
-
-					> .fade {
-						position: absolute;
-						bottom: 0;
-						left: 0;
-						width: 100%;
-						height: 78px;
-						background: linear-gradient(transparent, rgba(#000, 0.7));
-					}
-
-					> .followed {
-						position: absolute;
-						top: 12px;
-						left: 12px;
-						padding: 4px 8px;
-						color: #fff;
-						background: rgba(0, 0, 0, 0.7);
-						font-size: 0.7em;
-						border-radius: 6px;
-					}
-
-					> .actions {
-						position: absolute;
-						top: 12px;
-						right: 12px;
-						-webkit-backdrop-filter: var(--blur, blur(8px));
-						backdrop-filter: var(--blur, blur(8px));
-						background: rgba(0, 0, 0, 0.2);
-						padding: 8px;
-						border-radius: 24px;
-
-						> .menu {
-							vertical-align: bottom;
-							height: 31px;
-							width: 31px;
-							color: #fff;
-							text-shadow: 0 0 8px #000;
-							font-size: 16px;
-						}
-
-						> .koudoku {
-							margin-left: 4px;
-							vertical-align: bottom;
-						}
-					}
-
-					> .title {
-						position: absolute;
-						bottom: 0;
-						left: 0;
-						width: 100%;
-						padding: 0 0 8px 154px;
-						box-sizing: border-box;
-						color: #fff;
-
-						> .name {
-							display: block;
-							margin: 0;
-							line-height: 32px;
-							font-weight: bold;
-							font-size: 1.8em;
-							text-shadow: 0 0 8px #000;
-						}
-
-						> .bottom {
-							> * {
-								display: inline-block;
-								margin-right: 16px;
-								line-height: 20px;
-								opacity: 0.8;
-
-								&.username {
-									font-weight: bold;
-								}
-							}
-						}
-					}
-				}
-
-				> .title {
-					display: none;
-					text-align: center;
-					padding: 50px 8px 16px 8px;
-					font-weight: bold;
-					border-bottom: solid 0.5px var(--divider);
-
-					> .bottom {
-						> * {
-							display: inline-block;
-							margin-right: 8px;
-							opacity: 0.8;
-						}
-					}
-				}
-
-				> .avatar {
-					display: block;
-					position: absolute;
-					top: 170px;
-					left: 16px;
-					z-index: 2;
-					width: 120px;
-					height: 120px;
-					box-shadow: 1px 1px 3px rgba(#000, 0.2);
-				}
-
-				> .description {
-					padding: 24px 24px 24px 154px;
-					font-size: 0.95em;
-
-					> .empty {
-						margin: 0;
-						opacity: 0.5;
-					}
-				}
-
-				> .fields {
-					padding: 24px;
-					font-size: 0.9em;
-					border-top: solid 0.5px var(--divider);
-
-					> .field {
-						display: flex;
-						padding: 0;
-						margin: 0;
-						align-items: center;
-
-						&:not(:last-child) {
-							margin-bottom: 8px;
-						}
-
-						> .name {
-							width: 30%;
-							overflow: hidden;
-							white-space: nowrap;
-							text-overflow: ellipsis;
-							font-weight: bold;
-							text-align: center;
-						}
-
-						> .value {
-							width: 70%;
-							overflow: hidden;
-							white-space: nowrap;
-							text-overflow: ellipsis;
-							margin: 0;
-						}
-					}
-
-					&.system > .field > .name {
-					}
-				}
-
-				> .status {
-					display: flex;
-					padding: 24px;
-					border-top: solid 0.5px var(--divider);
-
-					> a {
-						flex: 1;
-						text-align: center;
-
-						&.active {
-							color: var(--accent);
-						}
-
-						&:hover {
-							text-decoration: none;
-						}
-
-						> b {
-							display: block;
-							line-height: 16px;
-						}
-
-						> span {
-							font-size: 70%;
-						}
-					}
-				}
-			}
-		}
-
-		> .contents {
-			> .content {
-				margin-bottom: var(--margin);
-			}
-		}
-	}
-
-	&.max-width_500px {
-		> .main {
-			> .profile > .main {
-				> .banner-container {
-					height: 140px;
-
-					> .fade {
-						display: none;
-					}
-
-					> .title {
-						display: none;
-					}
-				}
-
-				> .title {
-					display: block;
-				}
-
-				> .avatar {
-					top: 90px;
-					left: 0;
-					right: 0;
-					width: 92px;
-					height: 92px;
-					margin: auto;
-				}
-
-				> .description {
-					padding: 16px;
-					text-align: center;
-				}
-
-				> .fields {
-					padding: 16px;
-				}
-
-				> .status {
-					padding: 16px;
-				}
-			}
-
-			> .contents {
-				> .nav {
-					font-size: 80%;
-				}
-			}
-		}
-	}
-
-	&.wide {
-		display: flex;
-		width: 100%;
-
-		> .main {
-			width: 100%;
-			min-width: 0;
-		}
-
-		> .sub {
-			max-width: 350px;
-			min-width: 350px;
-			margin-left: var(--margin);
-		}
-	}
-}
 </style>
diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts
index 1197976d5e..828708309c 100644
--- a/packages/client/src/router.ts
+++ b/packages/client/src/router.ts
@@ -12,15 +12,21 @@ const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
 });
 
 export const routes = [{
-	name: 'user',
-	path: '/@:acct/:page?',
-	component: page(() => import('./pages/user/index.vue')),
-}, {
 	path: '/@:initUser/pages/:initPageName/view-source',
 	component: page(() => import('./pages/page-editor/page-editor.vue')),
 }, {
 	path: '/@:username/pages/:pageName',
 	component: page(() => import('./pages/page.vue')),
+}, {
+	path: '/@:acct/following',
+	component: page(() => import('./pages/user/following.vue')),
+}, {
+	path: '/@:acct/followers',
+	component: page(() => import('./pages/user/followers.vue')),
+}, {
+	name: 'user',
+	path: '/@:acct/:page?',
+	component: page(() => import('./pages/user/index.vue')),
 }, {
 	name: 'note',
 	path: '/notes/:noteId',