diff --git a/locales/index.d.ts b/locales/index.d.ts
index 077a0e271e..36818b0065 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -10103,6 +10103,10 @@ export interface Locale extends ILocale {
          * 高さの最大値制限が無効(0)になっています。これが意図した変更ではない場合は、高さの最大値を何らかの値に設定してください。
          */
         "maxHeightWarn": string;
+        /**
+         * プレビュー画面で表示可能な範囲を超えたため、実際に埋め込んだ際とは表示が異なります。
+         */
+        "previewIsNotActual": string;
         /**
          * 角丸にする
          */
@@ -10116,9 +10120,17 @@ export interface Locale extends ILocale {
          */
         "applyToPreview": string;
         /**
-         * プレビュー画面で表示可能な範囲を超えたため、実際に埋め込んだ際とは表示が異なります。
+         * 埋め込みコードを作成
          */
-        "previewIsNotActual": string;
+        "generateCode": string;
+        /**
+         * コードが生成されました
+         */
+        "codeGenerated": string;
+        /**
+         * 生成されたコードをウェブサイトに貼り付けてご利用ください。
+         */
+        "codeGeneratedDescription": string;
     };
 }
 declare const locales: {
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 07f8ac4bac..a909a3df1a 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2695,7 +2695,10 @@ _embedCodeGen:
   maxHeight: "高さの最大値"
   maxHeightDescription: "0で最大値の設定が無効になります。ウィジェットが縦に伸び続けるのを防ぐために、何らかの値に指定してください。"
   maxHeightWarn: "高さの最大値制限が無効(0)になっています。これが意図した変更ではない場合は、高さの最大値を何らかの値に設定してください。"
+  previewIsNotActual: "プレビュー画面で表示可能な範囲を超えたため、実際に埋め込んだ際とは表示が異なります。"
   rounded: "角丸にする"
   border: "外枠に枠線をつける"
   applyToPreview: "プレビューに反映"
-  previewIsNotActual: "プレビュー画面で表示可能な範囲を超えたため、実際に埋め込んだ際とは表示が異なります。"
+  generateCode: "埋め込みコードを作成"
+  codeGenerated: "コードが生成されました"
+  codeGeneratedDescription: "生成されたコードをウェブサイトに貼り付けてご利用ください。"
diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue
index a3c80e743b..7216daffe3 100644
--- a/packages/frontend/src/components/MkCode.vue
+++ b/packages/frontend/src/components/MkCode.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div :class="$style.codeBlockRoot">
-	<button :class="$style.codeBlockCopyButton" class="_button" @click="copy">
+	<button v-if="copyButton" :class="$style.codeBlockCopyButton" class="_button" @click="copy">
 		<i class="ti ti-copy"></i>
 	</button>
 	<Suspense>
@@ -32,12 +32,17 @@ import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import copyToClipboard from '@/scripts/copy-to-clipboard.js';
 
-const props = defineProps<{
+const props = withDefaults(defineProps<{
 	code: string;
+	forceShow?: boolean;
+	copyButton?: boolean;
 	lang?: string;
-}>();
+}>(), {
+	copyButton: true,
+	forceShow: false,
+});
 
-const show = ref(!defaultStore.state.dataSaver.code);
+const show = ref(props.forceShow === true ? true : !defaultStore.state.dataSaver.code);
 
 const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'));
 
diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
index f6b4499c70..8f99cc4bb7 100644
--- a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
+++ b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
@@ -9,60 +9,82 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:width="1000"
 	:height="600"
 	:scroll="false"
-	:withOkButton="true"
+	:withOkButton="false"
 	@close="cancel()"
-	@ok="ok()"
 	@closed="$emit('closed')"
 >
 	<template #header>{{ i18n.ts._embedCodeGen.title }}</template>
 
 	<div :class="$style.embedCodeGenRoot">
-		<div :class="$style.embedCodeGenWrapper">
-			<div
-				:class="$style.embedCodeGenPreviewRoot"
-			>
-				<MkLoading v-if="iframeLoading" :class="$style.embedCodeGenPreviewSpinner"/>
-				<div :class="$style.embedCodeGenPreviewWrapper">
-					<div :class="$style.embedCodeGenPreviewTitle">{{ i18n.ts.preview }}</div>
-					<div ref="resizerRootEl" :class="$style.embedCodeGenPreviewResizerRoot">
-						<div
-							:class="$style.embedCodeGenPreviewResizer"
-							:style="{ transform: iframeStyle }"
-						>
-							<iframe
-								ref="iframeEl"
-								:src="embedPreviewUrl"
-								:class="$style.embedCodeGenPreviewIframe"
-								:style="{ height: `${iframeHeight}px` }"
-								@load="iframeOnLoad"
-							></iframe>
+		<Transition
+			mode="out-in"
+			:enterActiveClass="$style.transition_x_enterActive"
+			:leaveActiveClass="$style.transition_x_leaveActive"
+			:enterFromClass="$style.transition_x_enterFrom"
+			:leaveToClass="$style.transition_x_leaveTo"
+		>
+			<div v-if="phase === 'input'" key="input" :class="$style.embedCodeGenInputRoot">
+				<div
+					:class="$style.embedCodeGenPreviewRoot"
+				>
+					<MkLoading v-if="iframeLoading" :class="$style.embedCodeGenPreviewSpinner"/>
+					<div :class="$style.embedCodeGenPreviewWrapper">
+						<div :class="$style.embedCodeGenPreviewTitle">{{ i18n.ts.preview }}</div>
+						<div ref="resizerRootEl" :class="$style.embedCodeGenPreviewResizerRoot">
+							<div
+								:class="$style.embedCodeGenPreviewResizer"
+								:style="{ transform: iframeStyle }"
+							>
+								<iframe
+									ref="iframeEl"
+									:src="embedPreviewUrl"
+									:class="$style.embedCodeGenPreviewIframe"
+									:style="{ height: `${iframeHeight}px` }"
+									@load="iframeOnLoad"
+								></iframe>
+							</div>
 						</div>
 					</div>
 				</div>
-			</div>
-			<div :class="$style.embedCodeGenSettings" class="_gaps">
-				<MkInput v-if="isEmbedWithScrollbar" v-model="maxHeight" type="number" :min="0">
-					<template #label>{{ i18n.ts._embedCodeGen.maxHeight }}</template>
-					<template #suffix>px</template>
-					<template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template>
-				</MkInput>
-				<MkSelect v-model="colorMode">
-					<template #label>{{ i18n.ts.theme }}</template>
-					<option value="auto">{{ i18n.ts.syncDeviceDarkMode }}</option>
-					<option value="light">{{ i18n.ts.light }}</option>
-					<option value="dark">{{ i18n.ts.dark }}</option>
-				</MkSelect>
-				<MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch>
-				<MkSwitch v-if="isEmbedWithScrollbar" v-model="autoload">{{ i18n.ts._embedCodeGen.autoload }}</MkSwitch>
-				<MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch>
-				<MkSwitch v-model="border">{{ i18n.ts._embedCodeGen.border }}</MkSwitch>
-				<MkInfo v-if="isEmbedWithScrollbar && (!maxHeight || maxHeight <= 0)" warn>{{ i18n.ts._embedCodeGen.maxHeightWarn }}</MkInfo>
-				<MkInfo v-if="typeof maxHeight === 'number' && (maxHeight <= 0 || maxHeight > 700)">{{ i18n.ts._embedCodeGen.previewIsNotActual }}</MkInfo>
-				<div class="_buttons">
-					<MkButton :disabled="iframeLoading" @click="applyToPreview">{{ i18n.ts._embedCodeGen.applyToPreview }}</MkButton>
+				<div :class="$style.embedCodeGenSettings" class="_gaps">
+					<MkInput v-if="isEmbedWithScrollbar" v-model="maxHeight" type="number" :min="0">
+						<template #label>{{ i18n.ts._embedCodeGen.maxHeight }}</template>
+						<template #suffix>px</template>
+						<template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template>
+					</MkInput>
+					<MkSelect v-model="colorMode">
+						<template #label>{{ i18n.ts.theme }}</template>
+						<option value="auto">{{ i18n.ts.syncDeviceDarkMode }}</option>
+						<option value="light">{{ i18n.ts.light }}</option>
+						<option value="dark">{{ i18n.ts.dark }}</option>
+					</MkSelect>
+					<MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch>
+					<MkSwitch v-if="isEmbedWithScrollbar" v-model="autoload">{{ i18n.ts._embedCodeGen.autoload }}</MkSwitch>
+					<MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch>
+					<MkSwitch v-model="border">{{ i18n.ts._embedCodeGen.border }}</MkSwitch>
+					<MkInfo v-if="isEmbedWithScrollbar && (!maxHeight || maxHeight <= 0)" warn>{{ i18n.ts._embedCodeGen.maxHeightWarn }}</MkInfo>
+					<MkInfo v-if="typeof maxHeight === 'number' && (maxHeight <= 0 || maxHeight > 700)">{{ i18n.ts._embedCodeGen.previewIsNotActual }}</MkInfo>
+					<div class="_buttons">
+						<MkButton :disabled="iframeLoading" @click="applyToPreview">{{ i18n.ts._embedCodeGen.applyToPreview }}</MkButton>
+						<MkButton :disabled="iframeLoading" primary @click="generate">{{ i18n.ts._embedCodeGen.generateCode }} <i class="ti ti-arrow-right"></i></MkButton>
+					</div>
 				</div>
 			</div>
-		</div>
+			<div v-else-if="phase === 'result'" key="result" :class="$style.embedCodeGenResultRoot">
+				<div :class="$style.embedCodeGenResultWrapper" class="_gaps">
+					<div class="_gaps_s">
+						<div :class="$style.embedCodeGenResultHeadingIcon"><i class="ti ti-check"></i></div>
+						<div :class="$style.embedCodeGenResultHeading">{{ i18n.ts._embedCodeGen.codeGenerated }}</div>
+						<div :class="$style.embedCodeGenResultDescription">{{ i18n.ts._embedCodeGen.codeGeneratedDescription }}</div>
+					</div>
+					<div class="_gaps_s">
+						<MkCode :code="result" lang="html" :forceShow="true" :copyButton="false" :class="$style.embedCodeGenResultCode"/>
+						<MkButton :class="$style.embedCodeGenResultButtons" rounded primary @click="doCopy"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
+					</div>
+					<MkButton :class="$style.embedCodeGenResultButtons" rounded transparent @click="close">{{ i18n.ts.close }}</MkButton>
+				</div>
+			</div>
+		</Transition>
 	</div>
 </MkModalWindow>
 </template>
@@ -75,6 +97,8 @@ import MkInput from '@/components/MkInput.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkButton from '@/components/MkButton.vue';
+
+import MkCode from '@/components/MkCode.vue';
 import MkInfo from '@/components/MkInfo.vue';
 
 import * as os from '@/os.js';
@@ -86,19 +110,16 @@ import { embedRouteWithScrollbar } from '@/scripts/embed-page.js';
 import type { EmbeddableEntity, EmbedParams } from '@/scripts/embed-page.js';
 
 const emit = defineEmits<{
-	(ev: 'ok', url: string, code: string): void;
+	(ev: 'ok'): void;
 	(ev: 'cancel'): void;
 	(ev: 'closed'): void;
 }>();
 
-const props = withDefaults(defineProps<{
+const props = defineProps<{
 	entity: EmbeddableEntity;
 	idOrUsername: string;
 	params?: EmbedParams;
-	doCopy?: boolean;
-}>(), {
-	doCopy: true,
-});
+}>();
 
 //#region Modalの制御
 const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
@@ -108,17 +129,11 @@ function cancel() {
 	dialogEl.value?.close();
 }
 
-function ok() {
-	const _idOrUsername = props.entity === 'user-timeline' ? '@' + props.idOrUsername : props.idOrUsername;
-	const generatedUrl = `${url}/embed/${props.entity}/${_idOrUsername}?${new URLSearchParams(normalizeEmbedParams(paramsForUrl.value)).toString()}`;
-	const generatedCode = getEmbedCode(`/embed/${props.entity}/${_idOrUsername}`, paramsForUrl.value);
-	if (props.doCopy) {
-		copy(generatedCode);
-		os.success();
-	}
-	emit('ok', generatedUrl, generatedCode);
+function close() {
 	dialogEl.value?.close();
 }
+
+const phase = ref<'input' | 'result'>('input');
 //#endregion
 
 //#region 埋め込みURL生成・カスタマイズ
@@ -143,7 +158,7 @@ const embedPreviewUrl = computed(() => {
 		const maxHeight = parseInt(paramClass.get('maxHeight')!);
 		paramClass.set('maxHeight', maxHeight === 0 ? '500' : Math.min(maxHeight, 700).toString()); // プレビューであまりにも縮小されると見づらいため、700pxまでに制限
 	}
-	return `${url}/embed/${props.entity}/${_idOrUsername}${paramClass.toString() ? '?' + paramClass.toString() : ''}`;
+	return `http://localhost:3000/embed/${props.entity}/${_idOrUsername}${paramClass.toString() ? '?' + paramClass.toString() : ''}`;
 });
 
 const isEmbedWithScrollbar = computed(() => embedRouteWithScrollbar.includes(props.entity));
@@ -174,6 +189,18 @@ function applyToPreview() {
 		}
 	});
 }
+
+const result = ref('');
+function generate() {
+	const _idOrUsername = props.entity === 'user-timeline' ? '@' + props.idOrUsername : props.idOrUsername;
+	result.value = getEmbedCode(`/embed/${props.entity}/${_idOrUsername}`, paramsForUrl.value);
+	phase.value = 'result';
+}
+
+function doCopy() {
+	copy(result.value);
+	os.success();
+}
 //#endregion
 
 //#region プレビューのリサイズ
@@ -235,25 +262,48 @@ onMounted(() => {
 	resizeObserver.observe(resizerRootEl.value);
 });
 
-onDeactivated(() => {
+function reset() {
 	window.removeEventListener('message', windowEventHandler);
 	resizeObserver.disconnect();
+
+	// プレビューのリセット
+	iframeHeight.value = 0;
+	iframeScale.value = 1;
+	iframeLoading.value = true;
+	result.value = '';
+	phase.value = 'input';
+}
+
+onDeactivated(() => {
+	reset();
 });
 
 onUnmounted(() => {
-	window.removeEventListener('message', windowEventHandler);
-	resizeObserver.disconnect();
+	reset();
 });
 //#endregion
 </script>
 
 <style module>
+.transition_x_enterActive,
+.transition_x_leaveActive {
+	transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
+}
+.transition_x_enterFrom {
+	opacity: 0;
+	transform: translateX(50px);
+}
+.transition_x_leaveTo {
+	opacity: 0;
+	transform: translateX(-50px);
+}
+
 .embedCodeGenRoot {
 	container-type: inline-size;
 	height: 100%;
 }
 
-.embedCodeGenWrapper {
+.embedCodeGenInputRoot {
 	height: 100%;
 	display: grid;
 	grid-template-columns: 1fr 400px;
@@ -319,6 +369,47 @@ onUnmounted(() => {
 	overflow-y: scroll;
 }
 
+.embedCodeGenResultRoot {
+	box-sizing: border-box;
+	padding: 24px;
+	height: 100%;
+	max-width: 700px;
+	margin: 0 auto;
+	display: flex;
+	align-items: center;
+}
+
+.embedCodeGenResultHeading {
+	text-align: center;
+	font-size: 1.2em;
+}
+
+.embedCodeGenResultHeadingIcon {
+	margin: 0 auto;
+	background-color: var(--accentedBg);
+	color: var(--accent);
+	text-align: center;
+	height: 64px;
+	width: 64px;
+	font-size: 24px;
+	line-height: 64px;
+	border-radius: 50%;
+}
+
+.embedCodeGenResultDescription {
+	text-align: center;
+	white-space: pre-wrap;
+}
+
+.embedCodeGenResultWrapper,
+.embedCodeGenResultCode {
+	width: 100%;
+}
+
+.embedCodeGenResultButtons {
+	margin: 0 auto;
+}
+
 @container (max-width: 800px) {
 	.embedCodeGenWrapper {
 		grid-template-columns: 1fr;