diff --git a/packages/client/src/components/launch-pad.vue b/packages/client/src/components/launch-pad.vue
index 4693df2916..7891f61bf1 100644
--- a/packages/client/src/components/launch-pad.vue
+++ b/packages/client/src/components/launch-pad.vue
@@ -15,20 +15,6 @@
 				</MkA>
 			</template>
 		</div>
-		<div class="sub">
-			<button v-click-anime class="_button" @click="help">
-				<i class="fas fa-question-circle icon"></i>
-				<div class="text">{{ $ts.help }}</div>
-			</button>
-			<MkA v-click-anime to="/about" @click.passive="close()">
-				<i class="fas fa-info-circle icon"></i>
-				<div class="text">{{ $ts.instanceInfo }}</div>
-			</MkA>
-			<MkA v-click-anime to="/about-misskey" @click.passive="close()">
-				<img src="/static-assets/favicon.png" class="icon"/>
-				<div class="text">{{ $ts.aboutMisskey }}</div>
-			</MkA>
-		</div>
 	</div>
 </MkModal>
 </template>
@@ -74,28 +60,6 @@ const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k =>
 function close() {
 	modal.close();
 }
-
-function help(ev: MouseEvent) {
-	os.popupMenu([{
-		type: 'link',
-		to: '/mfm-cheat-sheet',
-		text: i18n.ts._mfm.cheatSheet,
-		icon: 'fas fa-code',
-	}, {
-		type: 'link',
-		to: '/scratchpad',
-		text: i18n.ts.scratchpad,
-		icon: 'fas fa-terminal',
-	}, null, {
-		text: i18n.ts.document,
-		icon: 'fas fa-question-circle',
-		action: () => {
-			window.open('https://misskey-hub.net/help.html', '_blank');
-		},
-	}], ev.currentTarget ?? ev.target);
-
-	close();
-}
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/ui/child-menu.vue b/packages/client/src/components/ui/child-menu.vue
new file mode 100644
index 0000000000..a0c26b50cd
--- /dev/null
+++ b/packages/client/src/components/ui/child-menu.vue
@@ -0,0 +1,63 @@
+<template>
+<div ref="el" class="sfhdhdhr">
+	<MkMenu ref="menu" :items="items" :align="align" :width="width" :as-drawer="false" @close="onChildClosed"/>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { on } from 'events';
+import { nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue';
+import MkMenu from './menu.vue';
+import { MenuItem } from '@/types/menu';
+import * as os from '@/os';
+
+const props = defineProps<{
+	items: MenuItem[];
+	targetElement: HTMLElement;
+	width?: number;
+	viaKeyboard?: boolean;
+}>();
+
+const emit = defineEmits<{
+	(ev: 'closed'): void;
+	(ev: 'actioned'): void;
+}>();
+
+const el = ref<HTMLElement>();
+const align = 'left';
+
+function setPosition() {
+	const rect = props.targetElement.getBoundingClientRect();
+	const left = rect.left + props.targetElement.offsetWidth;
+	const top = rect.top - 8;
+	el.value.style.left = left + 'px';
+	el.value.style.top = top + 'px';
+}
+
+function onChildClosed(actioned?: boolean) {
+	if (actioned) {
+		emit('actioned');
+	} else {
+		emit('closed');
+	}
+}
+
+onMounted(() => {
+	setPosition();
+	nextTick(() => {
+		setPosition();
+	});
+});
+
+defineExpose({
+	checkHit: (ev: MouseEvent) => {
+		return (ev.target === el.value || el.value.contains(ev.target));
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.sfhdhdhr {
+	position: fixed;
+}
+</style>
diff --git a/packages/client/src/components/ui/menu.vue b/packages/client/src/components/ui/menu.vue
index 6ad63c2ad7..26283ffe55 100644
--- a/packages/client/src/components/ui/menu.vue
+++ b/packages/client/src/components/ui/menu.vue
@@ -1,55 +1,67 @@
 <template>
-<div
-	ref="itemsEl" v-hotkey="keymap"
-	class="rrevdjwt"
-	:class="{ center: align === 'center', asDrawer }"
-	:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
-	@contextmenu.self="e => e.preventDefault()"
->
-	<template v-for="(item, i) in items2">
-		<div v-if="item === null" class="divider"></div>
-		<span v-else-if="item.type === 'label'" class="label item">
-			<span>{{ item.text }}</span>
+<div>
+	<div
+		ref="itemsEl" v-hotkey="keymap"
+		class="rrevdjwt _popup _shadow"
+		:class="{ center: align === 'center', asDrawer }"
+		:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
+		@contextmenu.self="e => e.preventDefault()"
+	>
+		<template v-for="(item, i) in items2">
+			<div v-if="item === null" class="divider"></div>
+			<span v-else-if="item.type === 'label'" class="label item">
+				<span>{{ item.text }}</span>
+			</span>
+			<span v-else-if="item.type === 'pending'" :tabindex="i" class="pending item">
+				<span><MkEllipsis/></span>
+			</span>
+			<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="i" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+				<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
+				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
+				<span>{{ item.text }}</span>
+				<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
+			</MkA>
+			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+				<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
+				<span>{{ item.text }}</span>
+				<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
+			</a>
+			<button v-else-if="item.type === 'user'" :tabindex="i" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+				<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
+				<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
+			</button>
+			<span v-else-if="item.type === 'switch'" :tabindex="i" class="item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+				<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
+			</span>
+			<button v-else-if="item.type === 'parent'" :tabindex="i" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)">
+				<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
+				<span>{{ item.text }}</span>
+				<span class="caret"><i class="fas fa-caret-right fa-fw"></i></span>
+			</button>
+			<button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+				<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
+				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
+				<span>{{ item.text }}</span>
+				<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
+			</button>
+		</template>
+		<span v-if="items2.length === 0" class="none item">
+			<span>{{ $ts.none }}</span>
 		</span>
-		<span v-else-if="item.type === 'pending'" :tabindex="i" class="pending item">
-			<span><MkEllipsis/></span>
-		</span>
-		<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="i" class="_button item" @click.passive="close()">
-			<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
-			<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
-			<span>{{ item.text }}</span>
-			<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
-		</MkA>
-		<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button item" @click="close()">
-			<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
-			<span>{{ item.text }}</span>
-			<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
-		</a>
-		<button v-else-if="item.type === 'user'" :tabindex="i" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)">
-			<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
-			<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
-		</button>
-		<span v-else-if="item.type === 'switch'" :tabindex="i" class="item">
-			<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
-		</span>
-		<button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)">
-			<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
-			<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
-			<span>{{ item.text }}</span>
-			<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
-		</button>
-	</template>
-	<span v-if="items2.length === 0" class="none item">
-		<span>{{ $ts.none }}</span>
-	</span>
+	</div>
+	<div v-if="childMenu" class="child">
+		<XChild ref="child" :items="childMenu" :target-element="childTarget" showing @actioned="childActioned"/>
+	</div>
 </div>
 </template>
 
 <script lang="ts" setup>
-import { nextTick, onMounted, watch } from 'vue';
+import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
 import { focusPrev, focusNext } from '@/scripts/focus';
 import FormSwitch from '@/components/form/switch.vue';
 import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
+import * as os from '@/os';
+const XChild = defineAsyncComponent(() => import('./child-menu.vue'));
 
 const props = defineProps<{
 	items: MenuItem[];
@@ -61,19 +73,23 @@ const props = defineProps<{
 }>();
 
 const emit = defineEmits<{
-	(ev: 'close'): void;
+	(ev: 'close', actioned?: boolean): void;
 }>();
 
 let itemsEl = $ref<HTMLDivElement>();
 
 let items2: InnerMenuItem[] = $ref([]);
 
+let child = $ref<InstanceType<typeof XChild>>();
+
 let keymap = $computed(() => ({
 	'up|k|shift+tab': focusUp,
 	'down|j|tab': focusDown,
 	'esc': close,
 }));
 
+let childShowingItem = $ref<MenuItem | null>();
+
 watch(() => props.items, () => {
 	const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined);
 
@@ -93,21 +109,53 @@ watch(() => props.items, () => {
 	immediate: true,
 });
 
-onMounted(() => {
-	if (props.viaKeyboard) {
-		nextTick(() => {
-			focusNext(itemsEl.children[0], true, false);
-		});
+let childMenu = $ref<MenuItem[] | null>();
+let childTarget = $ref<HTMLElement | null>();
+
+function closeChild() {
+	childMenu = null;
+	childShowingItem = null;
+}
+
+function childActioned() {
+	closeChild();
+	close(true);
+}
+
+function onGlobalMousedown(event: MouseEvent) {
+	if (childTarget && (event.target === childTarget || childTarget.contains(event.target))) return;
+	if (child && child.checkHit(event)) return;
+	closeChild();
+}
+
+let childCloseTimer: null | number = null;
+function onItemMouseEnter(item) {
+	childCloseTimer = window.setTimeout(() => {
+		closeChild();
+	}, 300);
+}
+function onItemMouseLeave(item) {
+	if (childCloseTimer) window.clearTimeout(childCloseTimer);
+}
+
+async function showChildren(item: MenuItem, ev: MouseEvent) {
+	if (props.asDrawer) {
+		os.popupMenu(item.children, ev.currentTarget ?? ev.target);
+		close();
+	} else {
+		childTarget = ev.currentTarget ?? ev.target;
+		childMenu = item.children;
+		childShowingItem = item;
 	}
-});
+}
 
 function clicked(fn: MenuAction, ev: MouseEvent) {
 	fn(ev);
-	close();
+	close(true);
 }
 
-function close() {
-	emit('close');
+function close(actioned = false) {
+	emit('close', actioned);
 }
 
 function focusUp() {
@@ -117,6 +165,20 @@ function focusUp() {
 function focusDown() {
 	focusNext(document.activeElement);
 }
+
+onMounted(() => {
+	if (props.viaKeyboard) {
+		nextTick(() => {
+			focusNext(itemsEl.children[0], true, false);
+		});
+	}
+
+	document.addEventListener('mousedown', onGlobalMousedown, { passive: true });
+});
+
+onBeforeUnmount(() => {
+	document.removeEventListener('mousedown', onGlobalMousedown);
+});
 </script>
 
 <style lang="scss" scoped>
@@ -225,6 +287,25 @@ function focusDown() {
 			opacity: 0.7;
 		}
 
+		&.parent {
+			display: flex;
+			align-items: center;
+			cursor: default;
+
+			> .caret {
+				margin-left: auto;
+			}
+
+			&.childShowing {
+				color: var(--accent);
+				text-decoration: none;
+
+				&:before {
+					background: var(--accentedBg);
+				}
+			}
+		}
+
 		> i {
 			margin-right: 5px;
 			width: 20px;
diff --git a/packages/client/src/components/ui/popup-menu.vue b/packages/client/src/components/ui/popup-menu.vue
index 2bc7030d77..c29aff45e7 100644
--- a/packages/client/src/components/ui/popup-menu.vue
+++ b/packages/client/src/components/ui/popup-menu.vue
@@ -1,6 +1,6 @@
 <template>
 <MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')">
-	<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq _popup _shadow" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/>
+	<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/>
 </MkModal>
 </template>
 
diff --git a/packages/client/src/components/ui/tooltip.vue b/packages/client/src/components/ui/tooltip.vue
index f81bf2fc5b..4c6258d245 100644
--- a/packages/client/src/components/ui/tooltip.vue
+++ b/packages/client/src/components/ui/tooltip.vue
@@ -12,6 +12,7 @@
 <script lang="ts" setup>
 import { nextTick, onMounted, onUnmounted, ref } from 'vue';
 import * as os from '@/os';
+import { calcPopupPosition } from '@/scripts/popup-position';
 
 const props = withDefaults(defineProps<{
 	showing: boolean;
@@ -36,151 +37,20 @@ const emit = defineEmits<{
 const el = ref<HTMLElement>();
 const zIndex = os.claimZIndex('high');
 
-const setPosition = () => {
-	if (el.value == null) return;
+function setPosition() {
+	const data = calcPopupPosition(el.value, {
+		anchorElement: props.targetElement,
+		direction: props.direction,
+		align: 'center',
+		innerMargin: props.innerMargin,
+		x: props.x,
+		y: props.y,
+	});
 
-	const contentWidth = el.value.offsetWidth;
-	const contentHeight = el.value.offsetHeight;
-
-	let rect: DOMRect;
-
-	if (props.targetElement) {
-		rect = props.targetElement.getBoundingClientRect();
-	}
-
-	const calcPosWhenTop = () => {
-		let left: number;
-		let top: number;
-
-		if (props.targetElement) {
-			left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2);
-			top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin;
-		} else {
-			left = props.x;
-			top = (props.y - contentHeight) - props.innerMargin;
-		}
-
-		left -= (el.value.offsetWidth / 2);
-
-		if (left + contentWidth - window.pageXOffset > window.innerWidth) {
-			left = window.innerWidth - contentWidth + window.pageXOffset - 1;
-		}
-
-		return [left, top];
-	};
-
-	const calcPosWhenBottom = () => {
-		let left: number;
-		let top: number;
-
-		if (props.targetElement) {
-			left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2);
-			top = (rect.top + window.pageYOffset + props.targetElement.offsetHeight) + props.innerMargin;
-		} else {
-			left = props.x;
-			top = (props.y) + props.innerMargin;
-		}
-
-		left -= (el.value.offsetWidth / 2);
-
-		if (left + contentWidth - window.pageXOffset > window.innerWidth) {
-			left = window.innerWidth - contentWidth + window.pageXOffset - 1;
-		}
-
-		return [left, top];
-	};
-
-	const calcPosWhenLeft = () => {
-		let left: number;
-		let top: number;
-
-		if (props.targetElement) {
-			left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin;
-			top = rect.top + window.pageYOffset + (props.targetElement.offsetHeight / 2);
-		} else {
-			left = (props.x - contentWidth) - props.innerMargin;
-			top = props.y;
-		}
-
-		top -= (el.value.offsetHeight / 2);
-
-		if (top + contentHeight - window.pageYOffset > window.innerHeight) {
-			top = window.innerHeight - contentHeight + window.pageYOffset - 1;
-		}
-
-		return [left, top];
-	};
-
-	const calcPosWhenRight = () => {
-		let left: number;
-		let top: number;
-
-		if (props.targetElement) {
-			left = (rect.left + props.targetElement.offsetWidth + window.pageXOffset) + props.innerMargin;
-			top = rect.top + window.pageYOffset + (props.targetElement.offsetHeight / 2);
-		} else {
-			left = props.x + props.innerMargin;
-			top = props.y;
-		}
-
-		top -= (el.value.offsetHeight / 2);
-
-		if (top + contentHeight - window.pageYOffset > window.innerHeight) {
-			top = window.innerHeight - contentHeight + window.pageYOffset - 1;
-		}
-
-		return [left, top];
-	};
-
-	const calc = (): {
-		left: number;
-		top: number;
-		transformOrigin: string;
-	} => {
-		switch (props.direction) {
-			case 'top': {
-				const [left, top] = calcPosWhenTop();
-
-				// ツールチップを上に向かって表示するスペースがなければ下に向かって出す
-				if (top - window.pageYOffset < 0) {
-					const [left, top] = calcPosWhenBottom();
-					return { left, top, transformOrigin: 'center top' };
-				}
-
-				return { left, top, transformOrigin: 'center bottom' };
-			}
-
-			case 'bottom': {
-				const [left, top] = calcPosWhenBottom();
-				// TODO: ツールチップを下に向かって表示するスペースがなければ上に向かって出す
-				return { left, top, transformOrigin: 'center top' };
-			}
-
-			case 'left': {
-				const [left, top] = calcPosWhenLeft();
-
-				// ツールチップを左に向かって表示するスペースがなければ右に向かって出す
-				if (left - window.pageXOffset < 0) {
-					const [left, top] = calcPosWhenRight();
-					return { left, top, transformOrigin: 'left center' };
-				}
-
-				return { left, top, transformOrigin: 'right center' };
-			}
-
-			case 'right': {
-				const [left, top] = calcPosWhenRight();
-				// TODO: ツールチップを右に向かって表示するスペースがなければ左に向かって出す
-				return { left, top, transformOrigin: 'left center' };
-			}
-		}
-	};
-
-	const { left, top, transformOrigin } = calc();
-	el.value.style.transformOrigin = transformOrigin;
-	el.value.style.left = left + 'px';
-	el.value.style.top = top + 'px';
-};
+	el.value.style.transformOrigin = data.transformOrigin;
+	el.value.style.left = data.left + 'px';
+	el.value.style.top = data.top + 'px';
+}
 
 let loopHandler;
 
diff --git a/packages/client/src/scripts/popup-position.ts b/packages/client/src/scripts/popup-position.ts
new file mode 100644
index 0000000000..e84eebf103
--- /dev/null
+++ b/packages/client/src/scripts/popup-position.ts
@@ -0,0 +1,158 @@
+import { Ref } from 'vue';
+
+export function calcPopupPosition(el: HTMLElement, props: {
+	anchorElement: HTMLElement | null;
+	innerMargin: number;
+	direction: 'top' | 'bottom' | 'left' | 'right';
+	align: 'top' | 'bottom' | 'left' | 'right' | 'center';
+	alignOffset?: number;
+	x?: number;
+	y?: number;
+}): { top: number; left: number; transformOrigin: string; } {
+	const contentWidth = el.offsetWidth;
+	const contentHeight = el.offsetHeight;
+
+	let rect: DOMRect;
+
+	if (props.anchorElement) {
+		rect = props.anchorElement.getBoundingClientRect();
+	}
+
+	const calcPosWhenTop = () => {
+		let left: number;
+		let top: number;
+
+		if (props.anchorElement) {
+			left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2);
+			top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin;
+		} else {
+			left = props.x;
+			top = (props.y - contentHeight) - props.innerMargin;
+		}
+
+		left -= (el.offsetWidth / 2);
+
+		if (left + contentWidth - window.pageXOffset > window.innerWidth) {
+			left = window.innerWidth - contentWidth + window.pageXOffset - 1;
+		}
+
+		return [left, top];
+	};
+
+	const calcPosWhenBottom = () => {
+		let left: number;
+		let top: number;
+
+		if (props.anchorElement) {
+			left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2);
+			top = (rect.top + window.pageYOffset + props.anchorElement.offsetHeight) + props.innerMargin;
+		} else {
+			left = props.x;
+			top = (props.y) + props.innerMargin;
+		}
+
+		left -= (el.offsetWidth / 2);
+
+		if (left + contentWidth - window.pageXOffset > window.innerWidth) {
+			left = window.innerWidth - contentWidth + window.pageXOffset - 1;
+		}
+
+		return [left, top];
+	};
+
+	const calcPosWhenLeft = () => {
+		let left: number;
+		let top: number;
+
+		if (props.anchorElement) {
+			left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin;
+			top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2);
+		} else {
+			left = (props.x - contentWidth) - props.innerMargin;
+			top = props.y;
+		}
+
+		top -= (el.offsetHeight / 2);
+
+		if (top + contentHeight - window.pageYOffset > window.innerHeight) {
+			top = window.innerHeight - contentHeight + window.pageYOffset - 1;
+		}
+
+		return [left, top];
+	};
+
+	const calcPosWhenRight = () => {
+		let left: number;
+		let top: number;
+
+		if (props.anchorElement) {
+			left = (rect.left + props.anchorElement.offsetWidth + window.pageXOffset) + props.innerMargin;
+
+			if (props.align === 'top') {
+				top = rect.top + window.pageYOffset;
+				if (props.alignOffset != null) top += props.alignOffset;
+			} else if (props.align === 'bottom') {
+				// TODO
+			} else { // center
+				top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2);
+				top -= (el.offsetHeight / 2);
+			}
+		} else {
+			left = props.x + props.innerMargin;
+			top = props.y;
+			top -= (el.offsetHeight / 2);
+		}
+
+		if (top + contentHeight - window.pageYOffset > window.innerHeight) {
+			top = window.innerHeight - contentHeight + window.pageYOffset - 1;
+		}
+
+		return [left, top];
+	};
+
+	const calc = (): {
+		left: number;
+		top: number;
+		transformOrigin: string;
+	} => {
+		switch (props.direction) {
+			case 'top': {
+				const [left, top] = calcPosWhenTop();
+
+				// ツールチップを上に向かって表示するスペースがなければ下に向かって出す
+				if (top - window.pageYOffset < 0) {
+					const [left, top] = calcPosWhenBottom();
+					return { left, top, transformOrigin: 'center top' };
+				}
+
+				return { left, top, transformOrigin: 'center bottom' };
+			}
+
+			case 'bottom': {
+				const [left, top] = calcPosWhenBottom();
+				// TODO: ツールチップを下に向かって表示するスペースがなければ上に向かって出す
+				return { left, top, transformOrigin: 'center top' };
+			}
+
+			case 'left': {
+				const [left, top] = calcPosWhenLeft();
+
+				// ツールチップを左に向かって表示するスペースがなければ右に向かって出す
+				if (left - window.pageXOffset < 0) {
+					const [left, top] = calcPosWhenRight();
+					return { left, top, transformOrigin: 'left center' };
+				}
+
+				return { left, top, transformOrigin: 'right center' };
+			}
+
+			case 'right': {
+				const [left, top] = calcPosWhenRight();
+				// TODO: ツールチップを右に向かって表示するスペースがなければ左に向かって出す
+				return { left, top, transformOrigin: 'left center' };
+			}
+		}
+	};
+
+	return calc();
+}
diff --git a/packages/client/src/types/menu.ts b/packages/client/src/types/menu.ts
index ed67e6ab88..972f6db214 100644
--- a/packages/client/src/types/menu.ts
+++ b/packages/client/src/types/menu.ts
@@ -11,10 +11,11 @@ export type MenuA = { type: 'a', href: string, target?: string, download?: strin
 export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction };
 export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean };
 export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction };
+export type MenuParent = { type: 'parent', text: string, icon?: string, children: OuterMenuItem[] };
 
 export type MenuPending = { type: 'pending' };
 
-type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton;
-type OuterPromiseMenuItem = Promise<MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton>;
+type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent;
+type OuterPromiseMenuItem = Promise<MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent>;
 export type MenuItem = OuterMenuItem | OuterPromiseMenuItem;
-export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton;
+export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent;
diff --git a/packages/client/src/ui/_common_/common.vue b/packages/client/src/ui/_common_/common.vue
index 9f7388db53..f32cd3fe0d 100644
--- a/packages/client/src/ui/_common_/common.vue
+++ b/packages/client/src/ui/_common_/common.vue
@@ -1,5 +1,6 @@
 <template>
-<component :is="popup.component"
+<component
+	:is="popup.component"
 	v-for="popup in popups"
 	:key="popup.id"
 	v-bind="popup.props"
@@ -15,56 +16,45 @@
 <div v-if="dev" id="devTicker"><span>DEV BUILD</span></div>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
+<script lang="ts" setup>
+import { defineAsyncComponent } from 'vue';
+import { swInject } from './sw-inject';
 import { popup, popups, pendingApiRequestsCount } from '@/os';
 import { uploads } from '@/scripts/upload';
 import * as sound from '@/scripts/sound';
 import { $i } from '@/account';
-import { swInject } from './sw-inject';
 import { stream } from '@/stream';
 
-export default defineComponent({
-	components: {
-		XStreamIndicator: defineAsyncComponent(() => import('./stream-indicator.vue')),
-		XUpload: defineAsyncComponent(() => import('./upload.vue')),
-	},
+const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue'));
+const XUpload = defineAsyncComponent(() => import('./upload.vue'));
 
-	setup() {
-		const onNotification = notification => {
-			if ($i.mutingNotificationTypes.includes(notification.type)) return;
+const dev = _DEV_;
 
-			if (document.visibilityState === 'visible') {
-				stream.send('readNotification', {
-					id: notification.id
-				});
+const onNotification = notification => {
+	if ($i.mutingNotificationTypes.includes(notification.type)) return;
 
-				popup(defineAsyncComponent(() => import('@/components/notification-toast.vue')), {
-					notification
-				}, {}, 'closed');
-			}
+	if (document.visibilityState === 'visible') {
+		stream.send('readNotification', {
+			id: notification.id,
+		});
 
-			sound.play('notification');
-		};
+		popup(defineAsyncComponent(() => import('@/components/notification-toast.vue')), {
+			notification,
+		}, {}, 'closed');
+	}
 
-		if ($i) {
-			const connection = stream.useChannel('main', null, 'UI');
-			connection.on('notification', onNotification);
+	sound.play('notification');
+};
 
-			//#region Listen message from SW
-			if ('serviceWorker' in navigator) {
-				swInject();
-			}
-		}
+if ($i) {
+	const connection = stream.useChannel('main', null, 'UI');
+	connection.on('notification', onNotification);
 
-		return {
-			uploads,
-			popups,
-			pendingApiRequestsCount,
-			dev: _DEV_,
-		};
-	},
-});
+	//#region Listen message from SW
+	if ('serviceWorker' in navigator) {
+		swInject();
+	}
+}
 </script>
 
 <style lang="scss">
diff --git a/packages/client/src/ui/_common_/navbar-for-mobile.vue b/packages/client/src/ui/_common_/navbar-for-mobile.vue
index d1b4c30b31..f2521cfc72 100644
--- a/packages/client/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/client/src/ui/_common_/navbar-for-mobile.vue
@@ -87,6 +87,36 @@ function openInstanceMenu(ev: MouseEvent) {
 		text: i18n.ts.federation,
 		icon: 'fas fa-globe',
 		to: '/about#federation',
+	}, null, {
+		type: 'parent',
+		text: i18n.ts.help,
+		icon: 'fas fa-question-circle',
+		children: [{
+			type: 'link',
+			to: '/mfm-cheat-sheet',
+			text: i18n.ts._mfm.cheatSheet,
+			icon: 'fas fa-code',
+		}, {
+			type: 'link',
+			to: '/scratchpad',
+			text: i18n.ts.scratchpad,
+			icon: 'fas fa-terminal',
+		}, {
+			type: 'link',
+			to: '/api-console',
+			text: 'API Console',
+			icon: 'fas fa-terminal',
+		}, null, {
+			text: i18n.ts.document,
+			icon: 'fas fa-question-circle',
+			action: () => {
+				window.open('https://misskey-hub.net/help.html', '_blank');
+			},
+		}],
+	}, {
+		type: 'link',
+		text: i18n.ts.aboutMisskey,
+		to: '/about-misskey',
 	}], ev.currentTarget ?? ev.target, {
 		align: 'left',
 	});
diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue
index e18f89113f..7e6065c305 100644
--- a/packages/client/src/ui/_common_/navbar.vue
+++ b/packages/client/src/ui/_common_/navbar.vue
@@ -110,6 +110,36 @@ function openInstanceMenu(ev: MouseEvent) {
 		text: i18n.ts.federation,
 		icon: 'fas fa-globe',
 		to: '/about#federation',
+	}, null, {
+		type: 'parent',
+		text: i18n.ts.help,
+		icon: 'fas fa-question-circle',
+		children: [{
+			type: 'link',
+			to: '/mfm-cheat-sheet',
+			text: i18n.ts._mfm.cheatSheet,
+			icon: 'fas fa-code',
+		}, {
+			type: 'link',
+			to: '/scratchpad',
+			text: i18n.ts.scratchpad,
+			icon: 'fas fa-terminal',
+		}, {
+			type: 'link',
+			to: '/api-console',
+			text: 'API Console',
+			icon: 'fas fa-terminal',
+		}, null, {
+			text: i18n.ts.document,
+			icon: 'fas fa-question-circle',
+			action: () => {
+				window.open('https://misskey-hub.net/help.html', '_blank');
+			},
+		}],
+	}, {
+		type: 'link',
+		text: i18n.ts.aboutMisskey,
+		to: '/about-misskey',
 	}], ev.currentTarget ?? ev.target, {
 		align: 'left',
 	});