diff --git a/CHANGELOG.md b/CHANGELOG.md
index e72d57db3b..b33668ea97 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ You should also include the user name that made the change.
 ### Improvements
 - ユーザーごとにRenoteをミュートできるように
 - ノートごとに絵文字リアクションを受け取るか設定できるように
+- クリップをお気に入りに登録できるように
 - ノート検索の利用可否をロールで制御可能に(デフォルトでオフ)
 - ロールの並び順を設定可能に
 - カスタム絵文字にライセンス情報を付与できるように
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index da10ec6693..c4e86fc64a 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -975,6 +975,8 @@ sensitiveWords: "センシティブワード"
 sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
 notesSearchNotAvailable: "ノート検索は利用できません。"
 license: "ライセンス"
+unfavoriteConfirm: "お気に入り解除しますか?"
+myClips: "自分のクリップ"
 
 _achievements:
   earnedAt: "獲得日時"
diff --git a/packages/backend/migration/1678953978856-clip-favorite.js b/packages/backend/migration/1678953978856-clip-favorite.js
new file mode 100644
index 0000000000..aa5dc93a6e
--- /dev/null
+++ b/packages/backend/migration/1678953978856-clip-favorite.js
@@ -0,0 +1,23 @@
+export class clipFavorite1678953978856 {
+    name = 'clipFavorite1678953978856'
+
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "clip_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "clipId" character varying(32) NOT NULL, CONSTRAINT "PK_1b539f43906f05ebcabe752a977" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_25a31662b0b0cc9af6549a9d71" ON "clip_favorite" ("userId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_b1754a39d0b281e07ed7c078ec" ON "clip_favorite" ("userId", "clipId") `);
+        await queryRunner.query(`ALTER TABLE "clip" ADD "lastClippedAt" TIMESTAMP WITH TIME ZONE`);
+        await queryRunner.query(`CREATE INDEX "IDX_a3eac04ae2aa9e221e7596114a" ON "clip" ("lastClippedAt") `);
+        await queryRunner.query(`ALTER TABLE "clip_favorite" ADD CONSTRAINT "FK_25a31662b0b0cc9af6549a9d711" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "clip_favorite" ADD CONSTRAINT "FK_fce61c7986cee54393e79f1d849" FOREIGN KEY ("clipId") REFERENCES "clip"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "clip_favorite" DROP CONSTRAINT "FK_fce61c7986cee54393e79f1d849"`);
+        await queryRunner.query(`ALTER TABLE "clip_favorite" DROP CONSTRAINT "FK_25a31662b0b0cc9af6549a9d711"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_a3eac04ae2aa9e221e7596114a"`);
+        await queryRunner.query(`ALTER TABLE "clip" DROP COLUMN "lastClippedAt"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_b1754a39d0b281e07ed7c078ec"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_25a31662b0b0cc9af6549a9d71"`);
+        await queryRunner.query(`DROP TABLE "clip_favorite"`);
+    }
+}
diff --git a/packages/backend/src/core/entities/ClipEntityService.ts b/packages/backend/src/core/entities/ClipEntityService.ts
index fde15c8401..33d3c53806 100644
--- a/packages/backend/src/core/entities/ClipEntityService.ts
+++ b/packages/backend/src/core/entities/ClipEntityService.ts
@@ -1,6 +1,6 @@
 import { Inject, Injectable } from '@nestjs/common';
 import { DI } from '@/di-symbols.js';
-import type { ClipsRepository } from '@/models/index.js';
+import type { ClipFavoritesRepository, ClipsRepository, User } from '@/models/index.js';
 import { awaitAll } from '@/misc/prelude/await-all.js';
 import type { Packed } from '@/misc/json-schema.js';
 import type { } from '@/models/entities/Blocking.js';
@@ -14,6 +14,9 @@ export class ClipEntityService {
 		@Inject(DI.clipsRepository)
 		private clipsRepository: ClipsRepository,
 
+		@Inject(DI.clipFavoritesRepository)
+		private clipFavoritesRepository: ClipFavoritesRepository,
+
 		private userEntityService: UserEntityService,
 	) {
 	}
@@ -21,25 +24,31 @@ export class ClipEntityService {
 	@bindThis
 	public async pack(
 		src: Clip['id'] | Clip,
+		me?: { id: User['id'] } | null | undefined,
 	): Promise<Packed<'Clip'>> {
+		const meId = me ? me.id : null;
 		const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src });
 
 		return await awaitAll({
 			id: clip.id,
 			createdAt: clip.createdAt.toISOString(),
+			lastClippedAt: clip.lastClippedAt ? clip.lastClippedAt.toISOString() : null,
 			userId: clip.userId,
 			user: this.userEntityService.pack(clip.user ?? clip.userId),
 			name: clip.name,
 			description: clip.description,
 			isPublic: clip.isPublic,
+			favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
+			isFavorited: meId ? await this.clipFavoritesRepository.findOneBy({ clipId: clip.id, userId: meId }).then(x => x != null) : undefined,
 		});
 	}
 
 	@bindThis
 	public packMany(
 		clips: Clip[],
+		me?: { id: User['id'] } | null | undefined,
 	) {
-		return Promise.all(clips.map(x => this.pack(x)));
+		return Promise.all(clips.map(x => this.pack(x, me)));
 	}
 }
 
diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts
index 187f930ace..0879735b1d 100644
--- a/packages/backend/src/di-symbols.ts
+++ b/packages/backend/src/di-symbols.ts
@@ -52,6 +52,7 @@ export const DI = {
 	moderationLogsRepository: Symbol('moderationLogsRepository'),
 	clipsRepository: Symbol('clipsRepository'),
 	clipNotesRepository: Symbol('clipNotesRepository'),
+	clipFavoritesRepository: Symbol('clipFavoritesRepository'),
 	antennasRepository: Symbol('antennasRepository'),
 	antennaNotesRepository: Symbol('antennaNotesRepository'),
 	promoNotesRepository: Symbol('promoNotesRepository'),
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index d29b07b020..d00c8813c7 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -1,6 +1,6 @@
 import { Module } from '@nestjs/common';
 import { DI } from '@/di-symbols.js';
-import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment } from './index.js';
+import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite } from './index.js';
 import type { DataSource } from 'typeorm';
 import type { Provider } from '@nestjs/common';
 
@@ -286,6 +286,12 @@ const $clipNotesRepository: Provider = {
 	inject: [DI.db],
 };
 
+const $clipFavoritesRepository: Provider = {
+	provide: DI.clipFavoritesRepository,
+	useFactory: (db: DataSource) => db.getRepository(ClipFavorite),
+	inject: [DI.db],
+};
+
 const $antennasRepository: Provider = {
 	provide: DI.antennasRepository,
 	useFactory: (db: DataSource) => db.getRepository(Antenna),
@@ -445,6 +451,7 @@ const $roleAssignmentsRepository: Provider = {
 		$moderationLogsRepository,
 		$clipsRepository,
 		$clipNotesRepository,
+		$clipFavoritesRepository,
 		$antennasRepository,
 		$antennaNotesRepository,
 		$promoNotesRepository,
@@ -512,6 +519,7 @@ const $roleAssignmentsRepository: Provider = {
 		$moderationLogsRepository,
 		$clipsRepository,
 		$clipNotesRepository,
+		$clipFavoritesRepository,
 		$antennasRepository,
 		$antennaNotesRepository,
 		$promoNotesRepository,
diff --git a/packages/backend/src/models/entities/Clip.ts b/packages/backend/src/models/entities/Clip.ts
index 57a310ac03..825a32c981 100644
--- a/packages/backend/src/models/entities/Clip.ts
+++ b/packages/backend/src/models/entities/Clip.ts
@@ -12,6 +12,12 @@ export class Clip {
 	})
 	public createdAt: Date;
 
+	@Index()
+	@Column('timestamp with time zone', {
+		nullable: true,
+	})
+	public lastClippedAt: Date | null;
+
 	@Index()
 	@Column({
 		...id(),
diff --git a/packages/backend/src/models/entities/ClipFavorite.ts b/packages/backend/src/models/entities/ClipFavorite.ts
new file mode 100644
index 0000000000..623471e671
--- /dev/null
+++ b/packages/backend/src/models/entities/ClipFavorite.ts
@@ -0,0 +1,33 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { id } from '../id.js';
+import { User } from './User.js';
+import { Clip } from './Clip.js';
+
+@Entity()
+@Index(['userId', 'clipId'], { unique: true })
+export class ClipFavorite {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone')
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE',
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column(id())
+	public clipId: Clip['id'];
+
+	@ManyToOne(type => Clip, {
+		onDelete: 'CASCADE',
+	})
+	@JoinColumn()
+	public clip: Clip | null;
+}
diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts
index 4acb958b04..17083d7a01 100644
--- a/packages/backend/src/models/index.ts
+++ b/packages/backend/src/models/index.ts
@@ -13,6 +13,7 @@ import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js';
 import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js';
 import { Clip } from '@/models/entities/Clip.js';
 import { ClipNote } from '@/models/entities/ClipNote.js';
+import { ClipFavorite } from '@/models/entities/ClipFavorite.js';
 import { DriveFile } from '@/models/entities/DriveFile.js';
 import { DriveFolder } from '@/models/entities/DriveFolder.js';
 import { Emoji } from '@/models/entities/Emoji.js';
@@ -81,6 +82,7 @@ export {
 	ChannelNotePining,
 	Clip,
 	ClipNote,
+	ClipFavorite,
 	DriveFile,
 	DriveFolder,
 	Emoji,
@@ -148,6 +150,7 @@ export type ChannelFollowingsRepository = Repository<ChannelFollowing>;
 export type ChannelNotePiningsRepository = Repository<ChannelNotePining>;
 export type ClipsRepository = Repository<Clip>;
 export type ClipNotesRepository = Repository<ClipNote>;
+export type ClipFavoritesRepository = Repository<ClipFavorite>;
 export type DriveFilesRepository = Repository<DriveFile>;
 export type DriveFoldersRepository = Repository<DriveFolder>;
 export type EmojisRepository = Repository<Emoji>;
diff --git a/packages/backend/src/models/json-schema/clip.ts b/packages/backend/src/models/json-schema/clip.ts
index f0ee2ce0c4..7310e59013 100644
--- a/packages/backend/src/models/json-schema/clip.ts
+++ b/packages/backend/src/models/json-schema/clip.ts
@@ -12,6 +12,11 @@ export const packedClipSchema = {
 			optional: false, nullable: false,
 			format: 'date-time',
 		},
+		lastClippedAt: {
+			type: 'string',
+			optional: false, nullable: true,
+			format: 'date-time',
+		},
 		userId: {
 			type: 'string',
 			optional: false, nullable: false,
@@ -34,5 +39,13 @@ export const packedClipSchema = {
 			type: 'boolean',
 			optional: false, nullable: false,
 		},
+		isFavorited: {
+			type: 'boolean',
+			optional: true, nullable: false,
+		},
+		favoritedCount: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
 	},
 } as const;
diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts
index 741985f3a1..d5428805d1 100644
--- a/packages/backend/src/postgres.ts
+++ b/packages/backend/src/postgres.ts
@@ -21,6 +21,7 @@ import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js';
 import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js';
 import { Clip } from '@/models/entities/Clip.js';
 import { ClipNote } from '@/models/entities/ClipNote.js';
+import { ClipFavorite } from '@/models/entities/ClipFavorite.js';
 import { DriveFile } from '@/models/entities/DriveFile.js';
 import { DriveFolder } from '@/models/entities/DriveFolder.js';
 import { Emoji } from '@/models/entities/Emoji.js';
@@ -165,6 +166,7 @@ export const entities = [
 	ModerationLog,
 	Clip,
 	ClipNote,
+	ClipFavorite,
 	Antenna,
 	AntennaNote,
 	PromoNote,
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 2724649590..76fb8f636b 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -114,6 +114,9 @@ import * as ep___clips_list from './endpoints/clips/list.js';
 import * as ep___clips_notes from './endpoints/clips/notes.js';
 import * as ep___clips_show from './endpoints/clips/show.js';
 import * as ep___clips_update from './endpoints/clips/update.js';
+import * as ep___clips_favorite from './endpoints/clips/favorite.js';
+import * as ep___clips_unfavorite from './endpoints/clips/unfavorite.js';
+import * as ep___clips_myFavorites from './endpoints/clips/my-favorites.js';
 import * as ep___drive from './endpoints/drive.js';
 import * as ep___drive_files from './endpoints/drive/files.js';
 import * as ep___drive_files_attachedNotes from './endpoints/drive/files/attached-notes.js';
@@ -438,6 +441,9 @@ const $clips_list: Provider = { provide: 'ep:clips/list', useClass: ep___clips_l
 const $clips_notes: Provider = { provide: 'ep:clips/notes', useClass: ep___clips_notes.default };
 const $clips_show: Provider = { provide: 'ep:clips/show', useClass: ep___clips_show.default };
 const $clips_update: Provider = { provide: 'ep:clips/update', useClass: ep___clips_update.default };
+const $clips_favorite: Provider = { provide: 'ep:clips/favorite', useClass: ep___clips_favorite.default };
+const $clips_unfavorite: Provider = { provide: 'ep:clips/unfavorite', useClass: ep___clips_unfavorite.default };
+const $clips_myFavorites: Provider = { provide: 'ep:clips/my-favorites', useClass: ep___clips_myFavorites.default };
 const $drive: Provider = { provide: 'ep:drive', useClass: ep___drive.default };
 const $drive_files: Provider = { provide: 'ep:drive/files', useClass: ep___drive_files.default };
 const $drive_files_attachedNotes: Provider = { provide: 'ep:drive/files/attached-notes', useClass: ep___drive_files_attachedNotes.default };
@@ -766,6 +772,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 		$clips_notes,
 		$clips_show,
 		$clips_update,
+		$clips_favorite,
+		$clips_unfavorite,
+		$clips_myFavorites,
 		$drive,
 		$drive_files,
 		$drive_files_attachedNotes,
@@ -1088,6 +1097,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 		$clips_notes,
 		$clips_show,
 		$clips_update,
+		$clips_favorite,
+		$clips_unfavorite,
+		$clips_myFavorites,
 		$drive,
 		$drive_files,
 		$drive_files_attachedNotes,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 58f4fcc8a8..e928b0c2b1 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -114,6 +114,9 @@ import * as ep___clips_list from './endpoints/clips/list.js';
 import * as ep___clips_notes from './endpoints/clips/notes.js';
 import * as ep___clips_show from './endpoints/clips/show.js';
 import * as ep___clips_update from './endpoints/clips/update.js';
+import * as ep___clips_favorite from './endpoints/clips/favorite.js';
+import * as ep___clips_unfavorite from './endpoints/clips/unfavorite.js';
+import * as ep___clips_myFavorites from './endpoints/clips/my-favorites.js';
 import * as ep___drive from './endpoints/drive.js';
 import * as ep___drive_files from './endpoints/drive/files.js';
 import * as ep___drive_files_attachedNotes from './endpoints/drive/files/attached-notes.js';
@@ -436,6 +439,9 @@ const eps = [
 	['clips/notes', ep___clips_notes],
 	['clips/show', ep___clips_show],
 	['clips/update', ep___clips_update],
+	['clips/favorite', ep___clips_favorite],
+	['clips/unfavorite', ep___clips_unfavorite],
+	['clips/my-favorites', ep___clips_myFavorites],
 	['drive', ep___drive],
 	['drive/files', ep___drive_files],
 	['drive/files/attached-notes', ep___drive_files_attachedNotes],
diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts
index f3f9c3477f..b9d8dce47a 100644
--- a/packages/backend/src/server/api/endpoints/clips/add-note.ts
+++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts
@@ -106,6 +106,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				noteId: note.id,
 				clipId: clip.id,
 			});
+
+			await this.clipsRepository.update(clip.id, {
+				lastClippedAt: new Date(),
+			});
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/clips/create.ts b/packages/backend/src/server/api/endpoints/clips/create.ts
index c095de702c..a770dc986d 100644
--- a/packages/backend/src/server/api/endpoints/clips/create.ts
+++ b/packages/backend/src/server/api/endpoints/clips/create.ts
@@ -67,7 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				description: ps.description,
 			}).then(x => this.clipsRepository.findOneByOrFail(x.identifiers[0]));
 
-			return await this.clipEntityService.pack(clip);
+			return await this.clipEntityService.pack(clip, me);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/clips/favorite.ts b/packages/backend/src/server/api/endpoints/clips/favorite.ts
new file mode 100644
index 0000000000..6addf743a2
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/clips/favorite.ts
@@ -0,0 +1,76 @@
+import { Inject, Injectable } from '@nestjs/common';
+import type { ClipsRepository, ClipFavoritesRepository } from '@/models/index.js';
+import { IdService } from '@/core/IdService.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '../../error.js';
+
+export const meta = {
+	tags: ['clip'],
+
+	requireCredential: true,
+
+	kind: 'write:clip-favorite',
+
+	errors: {
+		noSuchClip: {
+			message: 'No such clip.',
+			code: 'NO_SUCH_CLIP',
+			id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5',
+		},
+
+		alreadyFavorited: {
+			message: 'The clip has already been favorited.',
+			code: 'ALREADY_FAVORITED',
+			id: '92658936-c625-4273-8326-2d790129256e',
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		clipId: { type: 'string', format: 'misskey:id' },
+	},
+	required: ['clipId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+	constructor(
+		@Inject(DI.clipsRepository)
+		private clipsRepository: ClipsRepository,
+
+		@Inject(DI.clipFavoritesRepository)
+		private clipFavoritesRepository: ClipFavoritesRepository,
+
+		private idService: IdService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const clip = await this.clipsRepository.findOneBy({ id: ps.clipId });
+			if (clip == null) {
+				throw new ApiError(meta.errors.noSuchClip);
+			}
+			if ((clip.userId !== me.id) && !clip.isPublic) {
+				throw new ApiError(meta.errors.noSuchClip);
+			}
+
+			const exist = await this.clipFavoritesRepository.findOneBy({
+				clipId: clip.id,
+				userId: me.id,
+			});
+
+			if (exist != null) {
+				throw new ApiError(meta.errors.alreadyFavorited);
+			}
+
+			await this.clipFavoritesRepository.insert({
+				id: this.idService.genId(),
+				createdAt: new Date(),
+				clipId: clip.id,
+				userId: me.id,
+			});
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/clips/list.ts b/packages/backend/src/server/api/endpoints/clips/list.ts
index 63ca069364..3b8deab709 100644
--- a/packages/backend/src/server/api/endpoints/clips/list.ts
+++ b/packages/backend/src/server/api/endpoints/clips/list.ts
@@ -42,7 +42,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				userId: me.id,
 			});
 
-			return await Promise.all(clips.map(x => this.clipEntityService.pack(x)));
+			return await this.clipEntityService.packMany(clips, me);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/clips/my-favorites.ts b/packages/backend/src/server/api/endpoints/clips/my-favorites.ts
new file mode 100644
index 0000000000..fc727e93bd
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/clips/my-favorites.ts
@@ -0,0 +1,52 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { ClipFavoritesRepository } from '@/models/index.js';
+import { DI } from '@/di-symbols.js';
+import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
+
+export const meta = {
+	tags: ['account', 'clip'],
+
+	requireCredential: true,
+
+	kind: 'read:clip-favorite',
+
+	res: {
+		type: 'array',
+		optional: false, nullable: false,
+		items: {
+			type: 'object',
+			optional: false, nullable: false,
+			ref: 'Clip',
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+	},
+	required: [],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+	constructor(
+		@Inject(DI.clipFavoritesRepository)
+		private clipFavoritesRepository: ClipFavoritesRepository,
+
+		private clipEntityService: ClipEntityService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const query = this.clipFavoritesRepository.createQueryBuilder('favorite')
+				.andWhere('favorite.userId = :meId', { meId: me.id })
+				.leftJoinAndSelect('favorite.clip', 'clip');
+
+			const favorites = await query
+				.getMany();
+
+			return this.clipEntityService.packMany(favorites.map(x => x.clip!), me);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/clips/show.ts b/packages/backend/src/server/api/endpoints/clips/show.ts
index e6d3f4f1f8..99d630a9b5 100644
--- a/packages/backend/src/server/api/endpoints/clips/show.ts
+++ b/packages/backend/src/server/api/endpoints/clips/show.ts
@@ -58,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				throw new ApiError(meta.errors.noSuchClip);
 			}
 
-			return await this.clipEntityService.pack(clip);
+			return await this.clipEntityService.pack(clip, me);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/clips/unfavorite.ts b/packages/backend/src/server/api/endpoints/clips/unfavorite.ts
new file mode 100644
index 0000000000..244843d50f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/clips/unfavorite.ts
@@ -0,0 +1,65 @@
+import { Inject, Injectable } from '@nestjs/common';
+import type { ClipsRepository, ClipFavoritesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '../../error.js';
+
+export const meta = {
+	tags: ['clip'],
+
+	requireCredential: true,
+
+	kind: 'write:clip-favorite',
+
+	errors: {
+		noSuchClip: {
+			message: 'No such clip.',
+			code: 'NO_SUCH_CLIP',
+			id: '2603966e-b865-426c-94a7-af4a01241dc1',
+		},
+
+		notFavorited: {
+			message: 'You have not favorited the clip.',
+			code: 'NOT_FAVORITED',
+			id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187',
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		clipId: { type: 'string', format: 'misskey:id' },
+	},
+	required: ['clipId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+	constructor(
+		@Inject(DI.clipsRepository)
+		private clipsRepository: ClipsRepository,
+
+		@Inject(DI.clipFavoritesRepository)
+		private clipFavoritesRepository: ClipFavoritesRepository,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const clip = await this.clipsRepository.findOneBy({ id: ps.clipId });
+			if (clip == null) {
+				throw new ApiError(meta.errors.noSuchClip);
+			}
+
+			const exist = await this.clipFavoritesRepository.findOneBy({
+				clipId: clip.id,
+				userId: me.id,
+			});
+
+			if (exist == null) {
+				throw new ApiError(meta.errors.notFavorited);
+			}
+
+			await this.clipFavoritesRepository.delete(exist.id);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts
index 597b67c442..a103c3f7d3 100644
--- a/packages/backend/src/server/api/endpoints/clips/update.ts
+++ b/packages/backend/src/server/api/endpoints/clips/update.ts
@@ -64,7 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				isPublic: ps.isPublic,
 			});
 
-			return await this.clipEntityService.pack(clip.id);
+			return await this.clipEntityService.pack(clip.id, me);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/notes/clips.ts b/packages/backend/src/server/api/endpoints/notes/clips.ts
index d5caec6e1d..0a5542f497 100644
--- a/packages/backend/src/server/api/endpoints/notes/clips.ts
+++ b/packages/backend/src/server/api/endpoints/notes/clips.ts
@@ -4,8 +4,8 @@ import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
 import { DI } from '@/di-symbols.js';
-import { ApiError } from '../../error.js';
 import { GetterService } from '@/server/api/GetterService.js';
+import { ApiError } from '../../error.js';
 
 export const meta = {
 	tags: ['clips', 'notes'],
@@ -67,7 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				isPublic: true,
 			});
 
-			return await Promise.all(clips.map(x => this.clipEntityService.pack(x)));
+			return await this.clipEntityService.packMany(clips, me);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/users/clips.ts b/packages/backend/src/server/api/endpoints/users/clips.ts
index e3fd0920c9..c5aa93baaf 100644
--- a/packages/backend/src/server/api/endpoints/users/clips.ts
+++ b/packages/backend/src/server/api/endpoints/users/clips.ts
@@ -51,7 +51,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				.take(ps.limit)
 				.getMany();
 
-			return await this.clipEntityService.packMany(clips);
+			return await this.clipEntityService.packMany(clips, me);
 		});
 	}
 }
diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue
index d66088d33a..7515a9122a 100644
--- a/packages/frontend/src/pages/clip.vue
+++ b/packages/frontend/src/pages/clip.vue
@@ -7,6 +7,8 @@
 				<div v-if="clip.description" class="description">
 					<Mfm :text="clip.description" :is-note="false" :i="$i"/>
 				</div>
+				<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" as-like class="button" rounded primary @click="unfavorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
+				<MkButton v-else v-tooltip="i18n.ts.favorite" as-like class="button" rounded @click="favorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
 				<div class="user">
 					<MkAvatar :user="clip.user" class="avatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
 				</div>
@@ -27,12 +29,14 @@ import { i18n } from '@/i18n';
 import * as os from '@/os';
 import { definePageMetadata } from '@/scripts/page-metadata';
 import { url } from '@/config';
+import MkButton from '@/components/MkButton.vue';
 
 const props = defineProps<{
 	clipId: string,
 }>();
 
 let clip: misskey.entities.Clip = $ref<misskey.entities.Clip>();
+let favorited = $ref(false);
 const pagination = {
 	endpoint: 'clips/notes' as const,
 	limit: 10,
@@ -47,12 +51,34 @@ watch(() => props.clipId, async () => {
 	clip = await os.api('clips/show', {
 		clipId: props.clipId,
 	});
+	favorited = clip.isFavorited;
 }, {
 	immediate: true,
 }); 
 
 provide('currentClipPage', $$(clip));
 
+function favorite() {
+	os.apiWithDialog('clips/favorite', {
+		clipId: props.clipId,
+	}).then(() => {
+		favorited = true;
+	});
+}
+
+async function unfavorite() {
+	const confirm = await os.confirm({
+		type: 'warning',
+		text: i18n.ts.unfavoriteConfirm,
+	});
+	if (confirm.canceled) return;
+	os.apiWithDialog('clips/unfavorite', {
+		clipId: props.clipId,
+	}).then(() => {
+		favorited = false;
+	});
+}
+
 const headerActions = $computed(() => clip && isOwned ? [{
 	icon: 'ti ti-pencil',
 	text: i18n.ts.edit,
diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue
index a79601f32f..ad5f95e607 100644
--- a/packages/frontend/src/pages/my-clips/index.vue
+++ b/packages/frontend/src/pages/my-clips/index.vue
@@ -1,9 +1,9 @@
 <template>
 <MkStickyContainer>
-	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
+	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="700">
-		<div class="qtcaoidl">
-			<MkButton primary class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
+		<div v-if="tab === 'my'" class="qtcaoidl">
+			<MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
 
 			<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="list">
 				<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _margin">
@@ -12,12 +12,22 @@
 				</MkA>
 			</MkPagination>
 		</div>
+		<div v-else-if="tab === 'favorites'" class="_gaps">
+			<MkA v-for="item in favorites" :key="item.id" :to="`/clips/${item.id}`" :class="$style.clip" class="_panel">
+				<b>{{ item.name }}</b>
+				<div v-if="item.description" :class="$style.clipDescription">{{ item.description }}</div>
+				<div v-if="item.lastClippedAt">{{ i18n.ts.updatedAt }}: <MkTime :time="item.lastClippedAt" mode="detail"/></div>
+				<div :class="$style.clipUser">
+					<MkAvatar :user="item.user" :class="$style.clipUserAvatar" indicator link preview/> <MkUserName :user="item.user" :nowrap="false"/>
+				</div>
+			</MkA>
+		</div>
 	</MkSpacer>
 </MkStickyContainer>
 </template>
 
 <script lang="ts" setup>
-import { } from 'vue';
+import { watch } from 'vue';
 import MkPagination from '@/components/MkPagination.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os';
@@ -29,8 +39,15 @@ const pagination = {
 	limit: 10,
 };
 
+let tab = $ref('my');
+let favorites = $ref();
+
 const pagingComponent = $shallowRef<InstanceType<typeof MkPagination>>();
 
+watch($$(tab), async () => {
+	favorites = await os.api('clips/my-favorites');
+});
+
 async function create() {
 	const { canceled, result } = await os.form(i18n.ts.createNewClip, {
 		name: {
@@ -66,7 +83,15 @@ function onClipDeleted() {
 
 const headerActions = $computed(() => []);
 
-const headerTabs = $computed(() => []);
+const headerTabs = $computed(() => [{
+	key: 'my',
+	title: i18n.ts.myClips,
+	icon: 'ti ti-paperclip',
+}, {
+	key: 'favorites',
+	title: i18n.ts.favorites,
+	icon: 'ti ti-heart',
+}]);
 
 definePageMetadata({
 	title: i18n.ts.clip,
@@ -98,3 +123,24 @@ definePageMetadata({
 	}
 }
 </style>
+
+<style lang="scss" module>
+.clip {
+	display: block;
+	padding: 16px;
+}
+
+.clipDescription {
+	padding: 8px 0;
+}
+
+.clipUser {
+	padding-top: 16px;
+	border-top: solid 0.5px var(--divider);
+}
+
+.clipUserAvatar {
+	width: 32px;
+	height: 32px;
+}
+</style>