diff --git a/packages/backend/migration/1696548899000-background.js b/packages/backend/migration/1696548899000-background.js
new file mode 100644
index 0000000000..59309b98c2
--- /dev/null
+++ b/packages/backend/migration/1696548899000-background.js
@@ -0,0 +1,19 @@
+export class Background1696548899000 {
+    name = 'Background1696548899000'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" ADD "backgroundId" character varying(32)`);
+        await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "REL_fwvhvbijn8nocsdpqhn012pfo5" UNIQUE ("backgroundId")`);
+        await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_q5lm0tbgejtfskzg0rc4wd7t1n" FOREIGN KEY ("backgroundId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "user" ADD "backgroundUrl" character varying(512)`);
+        await queryRunner.query(`ALTER TABLE "user" ADD "backgroundBlurhash" character varying(128)`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "REL_fwvhvbijn8nocsdpqhn012pfo5"`);
+        await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_q5lm0tbgejtfskzg0rc4wd7t1n"`);
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "backgroundId"`);
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "backgroundUrl"`);
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "backgroundBlurhash"`);
+    }
+}
\ No newline at end of file
diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts
index cecbec9638..4b4872a2d6 100644
--- a/packages/backend/src/core/DriveService.ts
+++ b/packages/backend/src/core/DriveService.ts
@@ -423,6 +423,10 @@ export class DriveService {
 			q.andWhere('file.id != :bannerId', { bannerId: user.bannerId });
 		}
 
+		if (user.backgroundId) {
+			q.andWhere('file.id != :backgroundId', { backgroundId: user.backgroundId });
+		}
+
 		//This selete is hard coded, be careful if change database schema
 		q.addSelect('SUM("file"."size") OVER (ORDER BY "file"."id" DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)', 'acc_usage');
 		q.orderBy('file.id', 'ASC');
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index 155aee39a9..c862bffce5 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -454,9 +454,10 @@ export class ApRendererService {
 		const id = this.userEntityService.genLocalUserUri(user.id);
 		const isSystem = user.username.includes('.');
 
-		const [avatar, banner, profile] = await Promise.all([
+		const [avatar, banner, background, profile] = await Promise.all([
 			user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : undefined,
 			user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : undefined,
+			user.backgroundId ? this.driveFilesRepository.findOneBy({ id: user.backgroundId }) : undefined,
 			this.userProfilesRepository.findOneByOrFail({ userId: user.id }),
 		]);
 
@@ -496,6 +497,7 @@ export class ApRendererService {
 			summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
 			icon: avatar ? this.renderImage(avatar) : null,
 			image: banner ? this.renderImage(banner) : null,
+			backgroundUrl: background ? this.renderImage(background) : null,
 			tag,
 			manuallyApprovesFollowers: user.isLocked,
 			discoverable: user.isExplorable,
@@ -650,6 +652,9 @@ export class ApRendererService {
 					// Firefish
 					firefish: "https://joinfirefish.org/ns#",
 					speakAsCat: "firefish:speakAsCat",
+					// Sharkey
+					sharkey: "https://joinsharkey.org/ns#",
+					backgroundUrl: "sharkey:backgroundUrl",
 					// vcard
 					vcard: 'http://www.w3.org/2006/vcard/ns#',
 				},
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index bbb362646d..639d11add3 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -225,8 +225,8 @@ export class ApPersonService implements OnModuleInit {
 		return null;
 	}
 
-	private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'avatarUrl' | 'bannerUrl' | 'avatarBlurhash' | 'bannerBlurhash'>> {
-		const [avatar, banner] = await Promise.all([icon, image].map(img => {
+	private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'backgroundId' | 'avatarUrl' | 'bannerUrl' | 'backgroundUrl' | 'avatarBlurhash' | 'bannerBlurhash' | 'backgroundBlurhash'>> {
+		const [avatar, banner, background] = await Promise.all([icon, image].map(img => {
 			if (img == null) return null;
 			if (user == null) throw new Error('failed to create user: user is null');
 			return this.apImageService.resolveImage(user, img).catch(() => null);
@@ -235,10 +235,13 @@ export class ApPersonService implements OnModuleInit {
 		return {
 			avatarId: avatar?.id ?? null,
 			bannerId: banner?.id ?? null,
+			backgroundId: background?.id ?? null,
 			avatarUrl: avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null,
 			bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
+			backgroundUrl: background ? this.driveFileEntityService.getPublicUrl(background) : null,
 			avatarBlurhash: avatar?.blurhash ?? null,
 			bannerBlurhash: banner?.blurhash ?? null,
+			backgroundBlurhash: background?.blurhash ?? null
 		};
 	}
 
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 714459d76b..dbad82b1fe 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -308,6 +308,14 @@ export class UserEntityService implements OnModuleInit {
 				bannerBlurhash: banner.blurhash,
 			});
 		}
+		if (user.backgroundId != null && user.backgroundUrl === null) {
+			const background = await this.driveFilesRepository.findOneByOrFail({ id: user.backgroundId });
+			user.backgroundUrl = this.driveFileEntityService.getPublicUrl(background);
+			this.usersRepository.update(user.id, {
+				backgroundUrl: user.backgroundUrl,
+				backgroundBlurhash: background.blurhash,
+			});
+		}
 
 		const meId = me ? me.id : null;
 		const isMe = meId === user.id;
@@ -385,6 +393,8 @@ export class UserEntityService implements OnModuleInit {
 				lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
 				bannerUrl: user.bannerUrl,
 				bannerBlurhash: user.bannerBlurhash,
+				backgroundUrl: user.backgroundUrl,
+				backgroundBlurhash: user.backgroundBlurhash,
 				isLocked: user.isLocked,
 				isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
 				isSuspended: user.isSuspended ?? falsy,
@@ -429,6 +439,7 @@ export class UserEntityService implements OnModuleInit {
 			...(opts.detail && isMe ? {
 				avatarId: user.avatarId,
 				bannerId: user.bannerId,
+				backgroundId: user.backgroundId,
 				isModerator: isModerator,
 				isAdmin: isAdmin,
 				injectFeaturedNote: profile!.injectFeaturedNote,
diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts
index 08c8243421..c5670f635b 100644
--- a/packages/backend/src/models/User.ts
+++ b/packages/backend/src/models/User.ts
@@ -124,6 +124,19 @@ export class MiUser {
 	@JoinColumn()
 	public banner: MiDriveFile | null;
 
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The ID of background DriveFile.',
+	})
+	public backgroundId: MiDriveFile['id'] | null;
+
+	@OneToOne(type => MiDriveFile, {
+		onDelete: 'SET NULL',
+	})
+	@JoinColumn()
+	public background: MiDriveFile | null;
+
 	@Column('varchar', {
 		length: 512, nullable: true,
 	})
@@ -134,6 +147,11 @@ export class MiUser {
 	})
 	public bannerUrl: string | null;
 
+	@Column('varchar', {
+		length: 512, nullable: true,
+	})
+	public backgroundUrl: string | null;
+
 	@Column('varchar', {
 		length: 128, nullable: true,
 	})
@@ -144,6 +162,11 @@ export class MiUser {
 	})
 	public bannerBlurhash: string | null;
 
+	@Column('varchar', {
+		length: 128, nullable: true,
+	})
+	public backgroundBlurhash: string | null;
+
 	@Index()
 	@Column('varchar', {
 		length: 128, array: true, default: '{}',
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index a8fb34acb1..bea2922f5a 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -122,6 +122,15 @@ export const packedUserDetailedNotMeOnlySchema = {
 			type: 'string',
 			nullable: true, optional: false,
 		},
+		backgroundUrl: {
+			type: 'string',
+			format: 'url',
+			nullable: true, optional: false,
+		},
+		backgroundBlurhash: {
+			type: 'string',
+			nullable: true, optional: false,
+		},
 		isLocked: {
 			type: 'boolean',
 			nullable: false, optional: false,
@@ -304,6 +313,11 @@ export const packedMeDetailedOnlySchema = {
 			nullable: true, optional: false,
 			format: 'id',
 		},
+		backgroundId: {
+			type: 'string',
+			nullable: true, optional: false,
+			format: 'id',
+		},
 		injectFeaturedNote: {
 			type: 'boolean',
 			nullable: true, optional: false,
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 93897b9c8f..1011a8d31a 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -60,6 +60,12 @@ export const meta = {
 			id: '0d8f5629-f210-41c2-9433-735831a58595',
 		},
 
+		noSuchBackground: {
+			message: 'No such background file.',
+			code: 'NO_SUCH_BACKGROUND',
+			id: '0d8f5629-f210-41c2-9433-735831a58582',
+		},
+
 		avatarNotAnImage: {
 			message: 'The file specified as an avatar is not an image.',
 			code: 'AVATAR_NOT_AN_IMAGE',
@@ -72,6 +78,12 @@ export const meta = {
 			id: '75aedb19-2afd-4e6d-87fc-67941256fa60',
 		},
 
+		backgroundNotAnImage: {
+			message: 'The file specified as a background is not an image.',
+			code: 'BACKGROUND_NOT_AN_IMAGE',
+			id: '75aedb19-2afd-4e6d-87fc-67941256fa40',
+		},
+
 		noSuchPage: {
 			message: 'No such page.',
 			code: 'NO_SUCH_PAGE',
@@ -133,6 +145,7 @@ export const paramDef = {
 		lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true },
 		avatarId: { type: 'string', format: 'misskey:id', nullable: true },
 		bannerId: { type: 'string', format: 'misskey:id', nullable: true },
+		backgroundId: { type: 'string', format: 'misskey:id', nullable: true },
 		fields: {
 			type: 'array',
 			minItems: 0,
@@ -300,6 +313,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				updates.bannerBlurhash = null;
 			}
 
+			if (ps.backgroundId) {
+				const background = await this.driveFilesRepository.findOneBy({ id: ps.backgroundId });
+
+				if (background == null || background.userId !== user.id) throw new ApiError(meta.errors.noSuchBackground);
+				if (!background.type.startsWith('image/')) throw new ApiError(meta.errors.backgroundNotAnImage);
+
+				updates.backgroundId = background.id;
+				updates.backgroundUrl = this.driveFileEntityService.getPublicUrl(background);
+				updates.backgroundBlurhash = background.blurhash;
+			} else if (ps.backgroundId === null) {
+				updates.backgroundId = null;
+				updates.backgroundUrl = null;
+				updates.backgroundBlurhash = null;
+			}
+
 			if (ps.pinnedPageId) {
 				const page = await this.pagesRepository.findOneBy({ id: ps.pinnedPageId });
 
diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts
index 93fbdaaa32..79da6d682d 100644
--- a/packages/backend/test/e2e/users.ts
+++ b/packages/backend/test/e2e/users.ts
@@ -95,6 +95,8 @@ describe('ユーザー', () => {
 			lastFetchedAt: user.lastFetchedAt,
 			bannerUrl: user.bannerUrl,
 			bannerBlurhash: user.bannerBlurhash,
+			backgroundUrl: user.backgroundUrl,
+			backgroundBlurhash: user.backgroundBlurhash,
 			isLocked: user.isLocked,
 			isSilenced: user.isSilenced,
 			isSuspended: user.isSuspended,
@@ -366,6 +368,8 @@ describe('ユーザー', () => {
 		assert.strictEqual(response.lastFetchedAt, null);
 		assert.strictEqual(response.bannerUrl, null);
 		assert.strictEqual(response.bannerBlurhash, null);
+		assert.strictEqual(response.backgroundUrl, null);
+		assert.strictEqual(response.backgroundBlurhash, null);
 		assert.strictEqual(response.isLocked, false);
 		assert.strictEqual(response.isSilenced, false);
 		assert.strictEqual(response.isSuspended, false);
@@ -561,6 +565,31 @@ describe('ユーザー', () => {
 		assert.deepStrictEqual(response2, expected2, inspect(parameters));
 	});
 
+	test('を書き換えることができる(Background)', async () => {
+		const aliceFile = (await uploadFile(alice)).body;
+		const parameters = { bannerId: aliceFile.id };
+		const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice });
+		assert.match(response.backgroundUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
+		assert.match(response.backgroundBlurhash ?? '.', /[ -~]{54}/);
+		const expected = {
+			...meDetailed(alice, true),
+			backgroundId: aliceFile.id,
+			backgroundBlurhash: response.baackgroundBlurhash,
+			backgroundUrl: response.backgroundUrl,
+		};
+		assert.deepStrictEqual(response, expected, inspect(parameters));
+
+		const parameters2 = { backgroundId: null };
+		const response2 = await successfulApiCall({ endpoint: 'i/update', parameters: parameters2, user: alice });
+		const expected2 = {
+			...meDetailed(alice, true),
+			backgroundId: null,
+			backgroundBlurhash: null,
+			backgroundUrl: null,
+		};
+		assert.deepStrictEqual(response2, expected2, inspect(parameters));
+	});
+
 	//#endregion
 	//#region 自分の情報の更新(i/pin, i/unpin)
 
diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue
index 530859bc00..0b6bde2c3b 100644
--- a/packages/frontend/src/components/MkDrive.file.vue
+++ b/packages/frontend/src/components/MkDrive.file.vue
@@ -22,6 +22,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<img :class="$style.labelImg" src="/client-assets/label.svg"/>
 			<p :class="$style.labelText">{{ i18n.ts.banner }}</p>
 		</div>
+		<div v-if="$i?.backgroundId == file.id" :class="[$style.label]">
+			<img :class="$style.labelImg" src="/client-assets/label.svg"/>
+			<p :class="$style.labelText">Background</p>
+		</div>
 		<div v-if="file.isSensitive" :class="[$style.label, $style.red]">
 			<img :class="$style.labelImg" src="/client-assets/label-red.svg"/>
 			<p :class="$style.labelText">{{ i18n.ts.sensitive }}</p>
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 904ed03ee2..9533707784 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -10,9 +10,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkAvatar :class="$style.avatar" :user="$i" @click="changeAvatar"/>
 			<MkButton primary rounded @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
 		</div>
+		<MkButton primary rounded :class="$style.backgroundEdit" @click="changeBackground">Change Background</MkButton>
 		<MkButton primary rounded :class="$style.bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
 	</div>
 
+
 	<MkInput v-model="profile.name" :max="30" manualSave>
 		<template #label>{{ i18n.ts._profile.name }}</template>
 	</MkInput>
@@ -254,6 +256,31 @@ function changeBanner(ev) {
 	});
 }
 
+function changeBackground(ev) {
+	selectFile(ev.currentTarget ?? ev.target, i18n.ts.banner).then(async (file) => {
+		let originalOrCropped = file;
+
+		const { canceled } = await os.confirm({
+			type: 'question',
+			text: i18n.t('cropImageAsk'),
+			okText: i18n.ts.cropYes,
+			cancelText: i18n.ts.cropNo,
+		});
+
+		if (!canceled) {
+			originalOrCropped = await os.cropImage(file, {
+				aspectRatio: 1,
+			});
+		}
+
+		const i = await os.apiWithDialog('i/update', {
+			backgroundId: originalOrCropped.id,
+		});
+		$i.backgroundId = i.backgroundId;
+		$i.backgroundUrl = i.backgroundUrl;
+	});
+}
+
 const headerActions = $computed(() => []);
 
 const headerTabs = $computed(() => []);
@@ -292,6 +319,11 @@ definePageMetadata({
 	top: 16px;
 	right: 16px;
 }
+.backgroundEdit {
+	position: absolute;
+	top: 103px;
+	right: 16px;
+}
 
 .metadataRoot {
 	container-type: inline-size;
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index a4a4ac2fbf..2c81026de4 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkSpacer :contentMax="narrow ? 800 : 1100">
+<MkSpacer :contentMax="narrow ? 800 : 1100" :style="background">
 	<div ref="rootEl" class="ftskorzw" :class="{ wide: !narrow }" style="container-type: inline-size;">
 		<div class="main _gaps">
 			<!-- TODO -->
@@ -236,6 +236,13 @@ if (props.user.listenbrainz) {
 	}
 }
 
+const background = computed(() => {
+	if (props.user.backgroundUrl == null) return {};
+	return {
+		'--backgroundImageStatic': `url('${props.user.backgroundUrl}')`
+	};
+});
+
 watch($$(moderationNote), async () => {
 	await os.api('admin/update-user-note', { userId: props.user.id, text: moderationNote });
 });
@@ -338,6 +345,24 @@ onUnmounted(() => {
 
 <style lang="scss" scoped>
 .ftskorzw {
+	&::before {
+		content: "";
+		position: fixed;
+		inset: 0;
+		background: var(--backgroundImageStatic);
+		background-size: cover;
+		background-position: center;
+		pointer-events: none;
+		filter: blur(8px) opacity(0.6);
+		// Funny CSS schenanigans to make background escape container
+		padding-left: 20px;
+    	margin-left: -20px;
+		padding-right: 20px;
+    	margin-right: -20px;
+		padding-top: 20px;
+    	margin-top: -20px;
+		background-attachment: fixed;
+	}
 
 	> .main {
 
diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts
index b3837369ec..588de4f8e7 100644
--- a/packages/misskey-js/src/api.types.ts
+++ b/packages/misskey-js/src/api.types.ts
@@ -414,6 +414,7 @@ export type Endpoints = {
 		birthday?: string | null;
 		avatarId?: DriveFile['id'] | null;
 		bannerId?: DriveFile['id'] | null;
+		backgroundId?: DriveFile['id'] | null;
 		fields?: {
 			name: string;
 			value: string;
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index 59df4582de..08d328c5b9 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -35,6 +35,8 @@ export type UserDetailed = UserLite & {
 	bannerBlurhash: string | null;
 	bannerColor: string | null;
 	bannerUrl: string | null;
+	backgroundUrl: string | null;
+	backgroundBlurhash: string | null;
 	birthday: string | null;
 	createdAt: DateString;
 	description: string | null;
@@ -88,6 +90,7 @@ export type UserList = {
 export type MeDetailed = UserDetailed & {
 	avatarId: DriveFile['id'];
 	bannerId: DriveFile['id'];
+	backgroundId: DriveFile['id'];
 	autoAcceptFollowed: boolean;
 	alwaysMarkNsfw: boolean;
 	carefulBot: boolean;