diff --git a/api/graphql/generated.go b/api/graphql/generated.go
index 8c32575..8d130f6 100644
--- a/api/graphql/generated.go
+++ b/api/graphql/generated.go
@@ -175,6 +175,7 @@ type ComplexityRoot struct {
 
 	Query struct {
 		Album                      func(childComplexity int, id int) int
+		FaceGroup                  func(childComplexity int, id int) int
 		MapboxToken                func(childComplexity int) int
 		Media                      func(childComplexity int, id int) int
 		MediaList                  func(childComplexity int, ids []int) int
@@ -322,6 +323,7 @@ type QueryResolver interface {
 	ShareTokenValidatePassword(ctx context.Context, token string, password *string) (bool, error)
 	Search(ctx context.Context, query string, limitMedia *int, limitAlbums *int) (*models.SearchResult, error)
 	MyFaceGroups(ctx context.Context, paginate *models.Pagination) ([]*models.FaceGroup, error)
+	FaceGroup(ctx context.Context, id int) (*models.FaceGroup, error)
 }
 type ShareTokenResolver interface {
 	HasPassword(ctx context.Context, obj *models.ShareToken) (bool, error)
@@ -1073,6 +1075,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Query.Album(childComplexity, args["id"].(int)), true
 
+	case "Query.faceGroup":
+		if e.complexity.Query.FaceGroup == nil {
+			break
+		}
+
+		args, err := ec.field_Query_faceGroup_args(context.TODO(), rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.Query.FaceGroup(childComplexity, args["id"].(int)), true
+
 	case "Query.mapboxToken":
 		if e.complexity.Query.MapboxToken == nil {
 			break
@@ -1624,6 +1638,7 @@ type Query {
   search(query: String!, limitMedia: Int, limitAlbums: Int): SearchResult!
 
   myFaceGroups(paginate: Pagination): [FaceGroup!]!
+  faceGroup(id: ID!): FaceGroup!
 }
 
 type Mutation {
@@ -2479,6 +2494,21 @@ func (ec *executionContext) field_Query_album_args(ctx context.Context, rawArgs
 	return args, nil
 }
 
+func (ec *executionContext) field_Query_faceGroup_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 int
+	if tmp, ok := rawArgs["id"]; ok {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id"))
+		arg0, err = ec.unmarshalNID2int(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["id"] = arg0
+	return args, nil
+}
+
 func (ec *executionContext) field_Query_mediaList_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
@@ -6567,6 +6597,48 @@ func (ec *executionContext) _Query_myFaceGroups(ctx context.Context, field graph
 	return ec.marshalNFaceGroup2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐFaceGroupᚄ(ctx, field.Selections, res)
 }
 
+func (ec *executionContext) _Query_faceGroup(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	fc := &graphql.FieldContext{
+		Object:     "Query",
+		Field:      field,
+		Args:       nil,
+		IsMethod:   true,
+		IsResolver: true,
+	}
+
+	ctx = graphql.WithFieldContext(ctx, fc)
+	rawArgs := field.ArgumentMap(ec.Variables)
+	args, err := ec.field_Query_faceGroup_args(ctx, rawArgs)
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	fc.Args = args
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Query().FaceGroup(rctx, args["id"].(int))
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(*models.FaceGroup)
+	fc.Result = res
+	return ec.marshalNFaceGroup2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐFaceGroup(ctx, field.Selections, res)
+}
+
 func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
 	defer func() {
 		if r := recover(); r != nil {
@@ -10093,6 +10165,20 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
 				}
 				return res
 			})
+		case "faceGroup":
+			field := field
+			out.Concurrently(i, func() (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._Query_faceGroup(ctx, field)
+				if res == graphql.Null {
+					atomic.AddUint32(&invalids, 1)
+				}
+				return res
+			})
 		case "__type":
 			out.Values[i] = ec._Query___type(ctx, field)
 		case "__schema":
diff --git a/api/graphql/resolvers/faces.go b/api/graphql/resolvers/faces.go
index 6f863b6..a712c02 100644
--- a/api/graphql/resolvers/faces.go
+++ b/api/graphql/resolvers/faces.go
@@ -63,6 +63,34 @@ func (r faceGroupResolver) ImageFaceCount(ctx context.Context, obj *models.FaceG
 	return int(count), nil
 }
 
+func (r *queryResolver) FaceGroup(ctx context.Context, id int) (*models.FaceGroup, error) {
+	user := auth.UserFromContext(ctx)
+	if user == nil {
+		return nil, errors.New("unauthorized")
+	}
+
+	if err := user.FillAlbums(r.Database); err != nil {
+		return nil, err
+	}
+
+	userAlbumIDs := make([]int, len(user.Albums))
+	for i, album := range user.Albums {
+		userAlbumIDs[i] = album.ID
+	}
+
+	faceGroupQuery := r.Database.
+		Joins("LEFT JOIN image_faces ON image_faces.id = face_groups.id").
+		Where("face_groups.id = ?", id).
+		Where("image_faces.media_id IN (?)", r.Database.Select("media_id").Table("media").Where("media.album_id IN (?)", userAlbumIDs))
+
+	var faceGroup models.FaceGroup
+	if err := faceGroupQuery.Find(&faceGroup).Error; err != nil {
+		return nil, err
+	}
+
+	return &faceGroup, nil
+}
+
 func (r *queryResolver) MyFaceGroups(ctx context.Context, paginate *models.Pagination) ([]*models.FaceGroup, error) {
 	user := auth.UserFromContext(ctx)
 	if user == nil {
@@ -80,7 +108,6 @@ func (r *queryResolver) MyFaceGroups(ctx context.Context, paginate *models.Pagin
 
 	faceGroupQuery := r.Database.
 		Joins("LEFT JOIN image_faces ON image_faces.id = face_groups.id").
-		// Where("face_groups.id IN (?)", faceGroupIDs).
 		Where("image_faces.media_id IN (?)", r.Database.Select("media_id").Table("media").Where("media.album_id IN (?)", userAlbumIDs)).
 		Order("CASE WHEN label IS NULL THEN 1 ELSE 0 END")
 
@@ -91,10 +118,6 @@ func (r *queryResolver) MyFaceGroups(ctx context.Context, paginate *models.Pagin
 		return nil, err
 	}
 
-	// for _, faceGroup := range faceGroups {
-	// 	faceGroup.ImageFaces = faceGroupMap[faceGroup.ID]
-	// }
-
 	return faceGroups, nil
 }
 
diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql
index 7a3d381..6a84522 100644
--- a/api/graphql/schema.graphql
+++ b/api/graphql/schema.graphql
@@ -61,6 +61,7 @@ type Query {
   search(query: String!, limitMedia: Int, limitAlbums: Int): SearchResult!
 
   myFaceGroups(paginate: Pagination): [FaceGroup!]!
+  faceGroup(id: ID!): FaceGroup!
 }
 
 type Mutation {
diff --git a/ui/src/Pages/AlbumPage/AlbumPage.js b/ui/src/Pages/AlbumPage/AlbumPage.js
index 81a0279..70f1d6e 100644
--- a/ui/src/Pages/AlbumPage/AlbumPage.js
+++ b/ui/src/Pages/AlbumPage/AlbumPage.js
@@ -6,7 +6,7 @@ import PropTypes from 'prop-types'
 import Layout from '../../Layout'
 import useURLParameters from '../../hooks/useURLParameters'
 import useScrollPagination from '../../hooks/useScrollPagination'
-import { Loader } from 'semantic-ui-react'
+import PaginateLoader from '../../components/PaginateLoader'
 
 const albumQuery = gql`
   query albumQuery(
@@ -140,13 +140,10 @@ function AlbumPage({ match }) {
         setOrdering={setOrdering}
         ordering={{ orderBy, orderDirection }}
       />
-      <Loader
-        style={{ margin: '42px 0 24px 0' }}
+      <PaginateLoader
         active={!finishedLoadingMore && !loading}
-        inline="centered"
-      >
-        Loading more media
-      </Loader>
+        text="Loading more media"
+      />
     </Layout>
   )
 }
diff --git a/ui/src/Pages/PeoplePage/PeoplePage.js b/ui/src/Pages/PeoplePage/PeoplePage.js
index ef2ffbe..c6bad27 100644
--- a/ui/src/Pages/PeoplePage/PeoplePage.js
+++ b/ui/src/Pages/PeoplePage/PeoplePage.js
@@ -7,10 +7,12 @@ import { Link } from 'react-router-dom'
 import SingleFaceGroup from './SingleFaceGroup/SingleFaceGroup'
 import { Button, Icon, Input } from 'semantic-ui-react'
 import FaceCircleImage from './FaceCircleImage'
+import useScrollPagination from '../../hooks/useScrollPagination'
+import PaginateLoader from '../../components/PaginateLoader'
 
 export const MY_FACES_QUERY = gql`
-  query myFaces {
-    myFaceGroups {
+  query myFaces($limit: Int!, $offset: Int!) {
+    myFaceGroups(paginate: { limit: $limit, offset: $offset }) {
       id
       label
       imageFaceCount
@@ -24,17 +26,11 @@ export const MY_FACES_QUERY = gql`
         }
         media {
           id
-          type
-          title
           thumbnail {
             url
             width
             height
           }
-          highRes {
-            url
-          }
-          favorite
         }
       }
     }
@@ -198,31 +194,33 @@ FaceGroup.propTypes = {
 const FaceGroupsWrapper = styled.div`
   display: flex;
   flex-wrap: wrap;
+  margin-top: 24px;
 `
 
-const PeoplePage = ({ match }) => {
-  const { data, error } = useQuery(MY_FACES_QUERY)
+const PeopleGallery = () => {
+  const { data, error, loading, fetchMore } = useQuery(MY_FACES_QUERY, {
+    variables: {
+      limit: 50,
+      offset: 0,
+    },
+  })
 
   const [
     recognizeUnlabeled,
     { loading: recognizeUnlabeledLoading },
   ] = useMutation(RECOGNIZE_UNLABELED_FACES_MUTATION)
 
+  const { containerElem, finished: finishedLoadingMore } = useScrollPagination({
+    loading,
+    fetchMore,
+    data,
+    getItems: data => data.myFaceGroups,
+  })
+
   if (error) {
     return error.message
   }
 
-  const faceGroup = match.params.person
-  if (faceGroup) {
-    return (
-      <Layout>
-        <SingleFaceGroup
-          faceGroup={data?.myFaceGroups?.find(x => x.id == faceGroup)}
-        />
-      </Layout>
-    )
-  }
-
   let faces = null
   if (data) {
     faces = data.myFaceGroups.map(faceGroup => (
@@ -232,7 +230,6 @@ const PeoplePage = ({ match }) => {
 
   return (
     <Layout title={'People'}>
-      <FaceGroupsWrapper>{faces}</FaceGroupsWrapper>
       <Button
         loading={recognizeUnlabeledLoading}
         disabled={recognizeUnlabeledLoading}
@@ -243,10 +240,28 @@ const PeoplePage = ({ match }) => {
         <Icon name="sync" />
         Recognize unlabeled faces
       </Button>
+      <FaceGroupsWrapper ref={containerElem}>{faces}</FaceGroupsWrapper>
+      <PaginateLoader
+        active={!finishedLoadingMore && !loading}
+        text="Loading more people"
+      />
     </Layout>
   )
 }
 
+const PeoplePage = ({ match }) => {
+  const faceGroup = match.params.person
+  if (faceGroup) {
+    return (
+      <Layout>
+        <SingleFaceGroup faceGroupID={faceGroup} />
+      </Layout>
+    )
+  } else {
+    return <PeopleGallery />
+  }
+}
+
 PeoplePage.propTypes = {
   match: PropTypes.object.isRequired,
 }
diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.js b/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.js
index c2846e2..b51823f 100644
--- a/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.js
+++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.js
@@ -1,12 +1,67 @@
+import { gql, useQuery } from '@apollo/client'
 import PropTypes from 'prop-types'
 import React, { useState } from 'react'
+import PaginateLoader from '../../../components/PaginateLoader'
 import PhotoGallery from '../../../components/photoGallery/PhotoGallery'
+import useScrollPagination from '../../../hooks/useScrollPagination'
 import FaceGroupTitle from './FaceGroupTitle'
 
-const SingleFaceGroup = ({ faceGroup }) => {
+export const SINGLE_FACE_GROUP = gql`
+  query singleFaceGroup($id: ID!, $limit: Int!, $offset: Int!) {
+    faceGroup(id: $id) {
+      id
+      label
+      imageFaces(paginate: { limit: $limit, offset: $offset }) {
+        id
+        rectangle {
+          minX
+          maxX
+          minY
+          maxY
+        }
+        media {
+          id
+          type
+          title
+          thumbnail {
+            url
+            width
+            height
+          }
+          highRes {
+            url
+          }
+          favorite
+        }
+      }
+    }
+  }
+`
+
+const SingleFaceGroup = ({ faceGroupID }) => {
+  const { data, error, loading, fetchMore } = useQuery(SINGLE_FACE_GROUP, {
+    variables: {
+      limit: 2,
+      offset: 0,
+      id: faceGroupID,
+    },
+  })
   const [presenting, setPresenting] = useState(false)
   const [activeIndex, setActiveIndex] = useState(-1)
 
+  const { containerElem, finished: finishedLoadingMore } = useScrollPagination({
+    loading,
+    fetchMore,
+    data,
+    getItems: data => data.faceGroup.imageFaces,
+  })
+
+  const faceGroup = data?.faceGroup
+
+  if (error) {
+    return <div>{error.message}</div>
+  }
+
   let mediaGallery = null
   if (faceGroup) {
     const media = faceGroup.imageFaces.map(x => x.media)
@@ -28,12 +83,16 @@ const SingleFaceGroup = ({ faceGroup }) => {
           nextImage={nextImage}
           previousImage={previousImage}
         />
+        <PaginateLoader
+          active={!finishedLoadingMore && !loading}
+          text="Loading more photos"
+        />
       </div>
     )
   }
 
   return (
-    <div>
+    <div ref={containerElem}>
       <FaceGroupTitle faceGroup={faceGroup} />
       {mediaGallery}
     </div>
@@ -41,7 +100,7 @@ const SingleFaceGroup = ({ faceGroup }) => {
 }
 
 SingleFaceGroup.propTypes = {
-  faceGroup: PropTypes.object,
+  faceGroupID: PropTypes.string,
 }
 
 export default SingleFaceGroup
diff --git a/ui/src/apolloClient.js b/ui/src/apolloClient.js
index 53ab78c..bc0a659 100644
--- a/ui/src/apolloClient.js
+++ b/ui/src/apolloClient.js
@@ -142,9 +142,15 @@ const memoryCache = new InMemoryCache({
         media: paginateCache(['onlyFavorites', 'order']),
       },
     },
+    FaceGroup: {
+      fields: {
+        imageFaces: paginateCache([]),
+      },
+    },
     Query: {
       fields: {
         myTimeline: paginateCache(['onlyFavorites']),
+        myFaceGroups: paginateCache([]),
       },
     },
   },
diff --git a/ui/src/components/PaginateLoader.js b/ui/src/components/PaginateLoader.js
new file mode 100644
index 0000000..19e6615
--- /dev/null
+++ b/ui/src/components/PaginateLoader.js
@@ -0,0 +1,20 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Loader } from 'semantic-ui-react'
+
+const PaginateLoader = ({ active, text }) => (
+  <Loader
+    style={{ margin: '42px 0 24px 0', opacity: active ? '1' : '0' }}
+    inline="centered"
+    active={true}
+  >
+    {text}
+  </Loader>
+)
+
+PaginateLoader.propTypes = {
+  active: PropTypes.bool,
+  text: PropTypes.string,
+}
+
+export default PaginateLoader
diff --git a/ui/src/components/timelineGallery/TimelineGallery.js b/ui/src/components/timelineGallery/TimelineGallery.js
index 8f66f02..38d8299 100644
--- a/ui/src/components/timelineGallery/TimelineGallery.js
+++ b/ui/src/components/timelineGallery/TimelineGallery.js
@@ -8,6 +8,7 @@ import { Loader } from 'semantic-ui-react'
 import useURLParameters from '../../hooks/useURLParameters'
 import { FavoritesCheckbox } from '../AlbumFilter'
 import useScrollPagination from '../../hooks/useScrollPagination'
+import PaginateLoader from '../PaginateLoader'
 
 const MY_TIMELINE_QUERY = gql`
   query myTimeline($onlyFavorites: Boolean, $limit: Int, $offset: Int) {
@@ -208,13 +209,10 @@ const TimelineGallery = () => {
         setOnlyFavorites={setOnlyFavorites}
       />
       <GalleryWrapper ref={containerElem}>{timelineGroups}</GalleryWrapper>
-      <Loader
-        style={{ margin: '42px 0 24px 0' }}
+      <PaginateLoader
         active={!finishedLoadingMore && !loading}
-        inline="centered"
-      >
-        Loading more media
-      </Loader>
+        text="Loading more media"
+      />
       {presenting && (
         <PresentView
           media={
