diff --git a/CHANGELOG.md b/CHANGELOG.md
index 188e146cdd..d90b1425c1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,7 +16,8 @@
 - Enhance: リアクション受け入れが「いいねのみ」の場合はリアクション絵文字一覧を表示しないように
 - Enhance: 設定>プラグインのページからプラグインの簡易的なログやエラーを見られるように
   - 実装の都合により、プラグインは1つエラーを起こした時に即時停止するようになりました
-- Enhance: ページのデザインを変更	
+- Enhance: ページのデザインを変更
+- Enhance: 2要素認証(ワンタイムパスワード)の入力欄を改善
 - Fix: 一部のページ内リンクが正しく動作しない問題を修正
 - Fix: 周年の実績が閏年を考慮しない問題を修正
 - Fix: ローカルURLのプレビューポップアップが左上に表示される
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 70586d7a87..e1250946f3 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -4920,6 +4920,14 @@ export interface Locale extends ILocale {
      * 使用しない場合は空欄にしてください
      */
     "notUsePleaseLeaveBlank": string;
+    /**
+     * ワンタイムパスワードを使う
+     */
+    "useTotp": string;
+    /**
+     * バックアップコードを使う
+     */
+    "useBackupCode": string;
     "_bubbleGame": {
         /**
          * 遊び方
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index cada6d855f..1c4df27d92 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1226,6 +1226,8 @@ loading: "読み込み中"
 surrender: "やめる"
 gameRetry: "リトライ"
 notUsePleaseLeaveBlank: "使用しない場合は空欄にしてください"
+useTotp: "ワンタイムパスワードを使う"
+useBackupCode: "バックアップコードを使う"
 
 _bubbleGame:
   howToPlay: "遊び方"
diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue
index d3cddad15b..88ef4635e6 100644
--- a/packages/frontend/src/components/MkInput.vue
+++ b/packages/frontend/src/components/MkInput.vue
@@ -22,6 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			:autocomplete="autocomplete"
 			:autocapitalize="autocapitalize"
 			:spellcheck="spellcheck"
+			:inputmode="inputmode"
 			:step="step"
 			:list="id"
 			:min="min"
@@ -63,6 +64,7 @@ const props = defineProps<{
 	mfmAutocomplete?: boolean | SuggestionType[],
 	autocapitalize?: string;
 	spellcheck?: boolean;
+	inputmode?: 'none' | 'text' | 'search' | 'email' | 'url' | 'numeric' | 'tel' | 'decimal';
 	step?: any;
 	datalist?: string[];
 	min?: number;
diff --git a/packages/frontend/src/components/MkPasswordDialog.vue b/packages/frontend/src/components/MkPasswordDialog.vue
index c49526d8e2..e749725fea 100644
--- a/packages/frontend/src/components/MkPasswordDialog.vue
+++ b/packages/frontend/src/components/MkPasswordDialog.vue
@@ -19,18 +19,21 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div style="margin-top: 16px;">{{ i18n.ts.authenticationRequiredToContinue }}</div>
 		</div>
 
-		<div class="_gaps">
-			<MkInput ref="passwordInput" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true">
-				<template #prefix><i class="ti ti-password"></i></template>
-			</MkInput>
+		<form @submit.prevent="done">
+			<div class="_gaps">
+				<MkInput ref="passwordInput" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" required :withPasswordToggle="true">
+					<template #prefix><i class="ti ti-password"></i></template>
+				</MkInput>
 
-			<MkInput v-if="$i.twoFactorEnabled" v-model="token" type="text" pattern="^([0-9]{6}|[A-Z0-9]{32})$" autocomplete="one-time-code" :spellcheck="false">
-				<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
-				<template #prefix><i class="ti ti-123"></i></template>
-			</MkInput>
+				<MkInput v-if="$i.twoFactorEnabled" v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'">
+					<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
+					<template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template>
+					<template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template>
+				</MkInput>
 
-			<MkButton :disabled="(password ?? '') == '' || ($i.twoFactorEnabled && (token ?? '') == '')" primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-lock-open"></i> {{ i18n.ts.continue }}</MkButton>
-		</div>
+				<MkButton :disabled="(password ?? '') == '' || ($i.twoFactorEnabled && (token ?? '') == '')" type="submit" primary rounded style="margin: 0 auto;"><i class="ti ti-lock-open"></i> {{ i18n.ts.continue }}</MkButton>
+			</div>
+		</form>
 	</MkSpacer>
 </MkModalWindow>
 </template>
@@ -54,6 +57,7 @@ const emit = defineEmits<{
 const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 const passwordInput = shallowRef<InstanceType<typeof MkInput>>();
 const password = ref('');
+const isBackupCode = ref(false);
 const token = ref<string | null>(null);
 
 function onClose() {
@@ -61,7 +65,7 @@ function onClose() {
 	if (dialog.value) dialog.value.close();
 }
 
-function done(res) {
+function done() {
 	emit('done', { password: password.value, token: token.value });
 	if (dialog.value) dialog.value.close();
 }
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index 852af01b5a..970aff825d 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -31,15 +31,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div v-if="user && user.securityKeys" class="or-hr">
 				<p class="or-msg">{{ i18n.ts.or }}</p>
 			</div>
-			<div class="twofa-group totp-group">
-				<p style="margin-bottom:0;">{{ i18n.ts['2fa'] }}</p>
+			<div class="twofa-group totp-group _gaps">
 				<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required>
 					<template #label>{{ i18n.ts.password }}</template>
 					<template #prefix><i class="ti ti-lock"></i></template>
 				</MkInput>
-				<MkInput v-model="token" type="text" pattern="^([0-9]{6}|[A-Z0-9]{32})$" autocomplete="one-time-code" :spellcheck="false" required>
-					<template #label>{{ i18n.ts.token }}</template>
-					<template #prefix><i class="ti ti-123"></i></template>
+				<MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'">
+					<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
+					<template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template>
+					<template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template>
 				</MkInput>
 				<MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
 			</div>
@@ -70,6 +70,7 @@ const password = ref('');
 const token = ref('');
 const host = ref(toUnicode(configHost));
 const totpLogin = ref(false);
+const isBackupCode = ref(false);
 const queryingKey = ref(false);
 const credentialRequest = ref<CredentialRequestOptions | null>(null);
 
diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
index 2608560cc4..2ef664b9a3 100644
--- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue
+++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
@@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkSpacer :marginMin="20" :marginMax="28">
 						<div class="_gaps">
 							<div>{{ i18n.ts._2fa.step3Title }}</div>
-							<MkInput v-model="token" autocomplete="one-time-code"></MkInput>
+							<MkInput v-model="token" autocomplete="one-time-code" inputmode="numeric"></MkInput>
 							<div>{{ i18n.ts._2fa.step3 }}</div>
 						</div>
 						<div class="_buttonsCenter" style="margin-top: 16px;">
diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue
index d8c5f848fe..975f23cdd1 100644
--- a/packages/frontend/src/pages/settings/2fa.vue
+++ b/packages/frontend/src/pages/settings/2fa.vue
@@ -80,7 +80,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import FormSection from '@/components/form/section.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import * as os from '@/os.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired, updateAccount } from '@/account.js';
 import { i18n } from '@/i18n.js';
 
 const $i = signinRequired();
@@ -116,6 +116,10 @@ async function unregisterTOTP(): Promise<void> {
 	os.apiWithDialog('i/2fa/unregister', {
 		password: auth.result.password,
 		token: auth.result.token,
+	}).then(res => {
+		updateAccount({
+			twoFactorEnabled: false,
+		});
 	}).catch(error => {
 		os.alert({
 			type: 'error',