diff --git a/api/funkwhale_api/common/management/commands/load_test_data.py b/api/funkwhale_api/common/management/commands/load_test_data.py
index 26f787f48df7f6704a40549a9c425c1d976992af..9eab5ff083c7843f19e5846baf830f54d23bcc7c 100644
--- a/api/funkwhale_api/common/management/commands/load_test_data.py
+++ b/api/funkwhale_api/common/management/commands/load_test_data.py
@@ -46,16 +46,28 @@ def create_local_accounts(factories, count, dependencies):
     return actors
 
 
-def create_tagged_tracks(factories, count, dependencies):
+def create_taggable_items(dependency):
+    def inner(factories, count, dependencies):
 
-    objs = []
-    for track in dependencies["tracks"]:
-        tag = random.choice(dependencies["tags"])
-        objs.append(factories["tags.TaggedItem"].build(content_object=track, tag=tag))
+        objs = []
+        tagged_objects = dependencies.get(
+            dependency, list(CONFIG_BY_ID[dependency]["model"].objects.all().only("pk"))
+        )
+        tags = dependencies.get("tags", list(tags_models.Tag.objects.all().only("pk")))
+        for i in range(count):
+            tag = random.choice(tags)
+            tagged_object = random.choice(tagged_objects)
+            objs.append(
+                factories["tags.TaggedItem"].build(
+                    content_object=tagged_object, tag=tag
+                )
+            )
+
+        return tags_models.TaggedItem.objects.bulk_create(
+            objs, batch_size=BATCH_SIZE, ignore_conflicts=True
+        )
 
-    return tags_models.TaggedItem.objects.bulk_create(
-        objs, batch_size=BATCH_SIZE, ignore_conflicts=True
-    )
+    return inner
 
 
 CONFIG = [
@@ -110,7 +122,10 @@ CONFIG = [
     {
         "id": "track_tags",
         "model": tags_models.TaggedItem,
-        "handler": create_tagged_tracks,
+        "queryset": tags_models.TaggedItem.objects.filter(
+            content_type__app_label="music", content_type__model="track"
+        ),
+        "handler": create_taggable_items("tracks"),
         "depends_on": [
             {
                 "field": "tag",
@@ -127,6 +142,52 @@ CONFIG = [
             },
         ],
     },
+    {
+        "id": "album_tags",
+        "model": tags_models.TaggedItem,
+        "queryset": tags_models.TaggedItem.objects.filter(
+            content_type__app_label="music", content_type__model="album"
+        ),
+        "handler": create_taggable_items("albums"),
+        "depends_on": [
+            {
+                "field": "tag",
+                "id": "tags",
+                "default_factor": 0.1,
+                "queryset": tags_models.Tag.objects.all(),
+                "set": False,
+            },
+            {
+                "field": "content_object",
+                "id": "albums",
+                "default_factor": 1,
+                "set": False,
+            },
+        ],
+    },
+    {
+        "id": "artist_tags",
+        "model": tags_models.TaggedItem,
+        "queryset": tags_models.TaggedItem.objects.filter(
+            content_type__app_label="music", content_type__model="artist"
+        ),
+        "handler": create_taggable_items("artists"),
+        "depends_on": [
+            {
+                "field": "tag",
+                "id": "tags",
+                "default_factor": 0.1,
+                "queryset": tags_models.Tag.objects.all(),
+                "set": False,
+            },
+            {
+                "field": "content_object",
+                "id": "artists",
+                "default_factor": 1,
+                "set": False,
+            },
+        ],
+    },
 ]
 
 CONFIG_BY_ID = {c["id"]: c for c in CONFIG}
@@ -194,8 +255,8 @@ class Command(BaseCommand):
 
         self.stdout.write("\nFinal state of database:\n\n")
         for row in CONFIG:
-            model = row["model"]
-            total = model.objects.all().count()
+            qs = row.get("queryset", row["model"].objects.all())
+            total = qs.count()
             self.stdout.write("- {} {} objects".format(total, row["id"]))
 
         self.stdout.write("")
diff --git a/api/funkwhale_api/tags/filters.py b/api/funkwhale_api/tags/filters.py
index 4be4afeefa42778a209bfa165cc1bf2c1f04d79a..e0ac9675ab7511f043195aaea83b31a1e9fe462b 100644
--- a/api/funkwhale_api/tags/filters.py
+++ b/api/funkwhale_api/tags/filters.py
@@ -18,4 +18,4 @@ class TagFilter(filters.FilterSet):
 
     class Meta:
         model = models.Tag
-        fields = ["q"]
+        fields = {"q": ["exact"], "name": ["exact", "startswith"]}
diff --git a/api/funkwhale_api/tags/views.py b/api/funkwhale_api/tags/views.py
index d7b1d8aa52880c58c41db130e9eebc490e130a7d..1d052ca7b2d71f7c9a9d351e244bdc2152842853 100644
--- a/api/funkwhale_api/tags/views.py
+++ b/api/funkwhale_api/tags/views.py
@@ -1,6 +1,8 @@
 from django.db.models import functions
 from rest_framework import viewsets
 
+import django_filters.rest_framework
+
 from funkwhale_api.users.oauth import permissions as oauth_permissions
 
 from . import filters
@@ -20,3 +22,4 @@ class TagViewSet(viewsets.ReadOnlyModelViewSet):
     required_scope = "libraries"
     anonymous_policy = "setting"
     filterset_class = filters.TagFilter
+    filter_backends = [django_filters.rest_framework.DjangoFilterBackend]
diff --git a/api/tests/common/test_commands.py b/api/tests/common/test_commands.py
index e2755256e8a32661597fcd0f77e4d048af79dd3a..a6fabfc06f1e39166dffc3903a4e79043d46664a 100644
--- a/api/tests/common/test_commands.py
+++ b/api/tests/common/test_commands.py
@@ -81,11 +81,7 @@ def test_load_test_data_dry_run(factories, mocker):
         ),
         (
             {"create_dependencies": True, "track_tags": 3},
-            [
-                (tags_models.Tag.objects.all(), 1),
-                (tags_models.TaggedItem.objects.all(), 3),
-                (music_models.Track.objects.all(), 3),
-            ],
+            [(tags_models.Tag.objects.all(), 1), (music_models.Track.objects.all(), 3)],
         ),
     ],
 )
diff --git a/api/tests/tags/test_views.py b/api/tests/tags/test_views.py
index fd3246adb91301567bc3166b5af267279d9ad188..b42e9ab3713e66dc1759c7bc7e5d76e697ee9616 100644
--- a/api/tests/tags/test_views.py
+++ b/api/tests/tags/test_views.py
@@ -23,18 +23,21 @@ def test_tags_list_ordering_length(factories, logged_in_api_client):
     url = reverse("api:v1:tags-list")
     tags = [
         factories["tags.Tag"](name="iamareallylongtag"),
-        factories["tags.Tag"](name="reallylongtag"),
         factories["tags.Tag"](name="short"),
+        factories["tags.Tag"](name="reallylongtag"),
         factories["tags.Tag"](name="bar"),
     ]
     expected = {
         "count": 4,
         "next": None,
         "previous": None,
-        "results": [serializers.TagSerializer(tag).data for tag in tags],
+        "results": [
+            serializers.TagSerializer(tag).data
+            for tag in [tags[3], tags[1], tags[2], tags[0]]
+        ],
     }
 
-    response = logged_in_api_client.get(url, {"ordering": "-length"})
+    response = logged_in_api_client.get(url, {"ordering": "length"})
 
     assert response.data == expected
 
diff --git a/dev.yml b/dev.yml
index b0ca7fc72b80b8ea0d11f02505b1d81a83d57e78..eaa7ca8d31239b89f0f023049016cc80e6ffab42 100644
--- a/dev.yml
+++ b/dev.yml
@@ -57,6 +57,7 @@ services:
       - "${MUSIC_DIRECTORY_SERVE_PATH-./data/music}:/music:ro"
       - "./data/plugins:/srv/funkwhale/plugins"
       - "./data/staticfiles:/staticfiles"
+      - "./data/media:/data/media"
     environment:
       - "FUNKWHALE_HOSTNAME=${FUNKWHALE_HOSTNAME-localhost}"
       - "FUNKWHALE_HOSTNAME_SUFFIX=funkwhale.test"
@@ -65,6 +66,7 @@ services:
       - "DATABASE_URL=postgresql://postgres@postgres/postgres"
       - "CACHE_URL=redis://redis:6379/0"
       - "STATIC_ROOT=/staticfiles"
+      - "MEDIA_ROOT=/data/media"
 
     depends_on:
       - postgres
@@ -92,10 +94,12 @@ services:
       - "FUNKWHALE_PROTOCOL=${FUNKWHALE_PROTOCOL-http}"
       - "DATABASE_URL=postgresql://postgres@postgres/postgres"
       - "CACHE_URL=redis://redis:6379/0"
+      - "MEDIA_ROOT=/data/media"
     volumes:
       - ./api:/app
       - "${MUSIC_DIRECTORY_SERVE_PATH-./data/music}:/music:ro"
       - "./data/plugins:/srv/funkwhale/plugins"
+      - "./data/media:/data/media"
     networks:
       - internal
   nginx:
@@ -122,9 +126,9 @@ services:
       - ./docker/nginx/entrypoint.sh:/entrypoint.sh:ro
       - "${MUSIC_DIRECTORY_SERVE_PATH-./data/music}:/music:ro"
       - ./deploy/funkwhale_proxy.conf:/etc/nginx/funkwhale_proxy.conf:ro
-      - "${MEDIA_ROOT-./api/funkwhale_api/media}:/protected/media:ro"
       - "./front:/frontend:ro"
       - "./data/staticfiles:/staticfiles:ro"
+      - "./data/media:/protected/media:ro"
     networks:
       - federation
       - internal
diff --git a/front/package.json b/front/package.json
index 54cf3c8e1672d784b57ecf797269db3de8fb4a10..2c8f041f0e66ea2aa2a8689baf611223bf7844a9 100644
--- a/front/package.json
+++ b/front/package.json
@@ -22,6 +22,7 @@
     "masonry-layout": "^4.2.2",
     "moment": "^2.22.2",
     "fomantic-ui-css": "^2.7",
+    "qs": "^6.7.0",
     "showdown": "^1.8.6",
     "vue": "^2.5.17",
     "vue-gettext": "^2.1.0",
diff --git a/front/src/components/audio/album/Widget.vue b/front/src/components/audio/album/Widget.vue
index ecc0a280ed04b26779de42fad4537b77b4243426..609ef6ebcc2cf5d114f32f27732cc4ed9a75e88b 100644
--- a/front/src/components/audio/album/Widget.vue
+++ b/front/src/components/audio/album/Widget.vue
@@ -2,10 +2,11 @@
   <div class="wrapper">
     <h3 class="ui header">
       <slot name="title"></slot>
+      <span v-if="showCount" class="ui tiny circular label">{{ count }}</span>
     </h3>
-    <button :disabled="!previousPage" @click="fetchData(previousPage)" :class="['ui', {disabled: !previousPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle left', 'icon']"></i></button>
-    <button :disabled="!nextPage" @click="fetchData(nextPage)" :class="['ui', {disabled: !nextPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle right', 'icon']"></i></button>
-    <button @click="fetchData('albums/')" :class="['ui', 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'refresh', 'icon']"></i></button>
+    <button v-if="controls" :disabled="!previousPage" @click="fetchData(previousPage)" :class="['ui', {disabled: !previousPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle left', 'icon']"></i></button>
+    <button v-if="controls" :disabled="!nextPage" @click="fetchData(nextPage)" :class="['ui', {disabled: !nextPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle right', 'icon']"></i></button>
+    <button v-if="controls" @click="fetchData('albums/')" :class="['ui', 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'refresh', 'icon']"></i></button>
     <div class="ui hidden divider"></div>
     <div class="ui five cards">
       <div v-if="isLoading" class="ui inverted active dimmer">
@@ -33,6 +34,7 @@
         </div>
       </div>
     </div>
+    <div v-if="!isLoading && albums.length === 0">No results matching your query.</div>
   </div>
 </template>
 
@@ -43,7 +45,9 @@ import PlayButton from '@/components/audio/PlayButton'
 
 export default {
   props: {
-    filters: {type: Object, required: true}
+    filters: {type: Object, required: true},
+    controls: {type: Boolean, default: true},
+    showCount: {type: Boolean, default: false},
   },
   components: {
     PlayButton
@@ -52,6 +56,7 @@ export default {
     return {
       albums: [],
       limit: 12,
+      count: 0,
       isLoading: false,
       errors: null,
       previousPage: null,
@@ -76,6 +81,7 @@ export default {
         self.nextPage = response.data.next
         self.isLoading = false
         self.albums = response.data.results
+        self.count = response.data.count
       }, error => {
         self.isLoading = false
         self.errors = error.backendErrors
diff --git a/front/src/components/audio/artist/Widget.vue b/front/src/components/audio/artist/Widget.vue
new file mode 100644
index 0000000000000000000000000000000000000000..1b88feae76121b868b103139a6eb3dbb9ddf047c
--- /dev/null
+++ b/front/src/components/audio/artist/Widget.vue
@@ -0,0 +1,172 @@
+<template>
+  <div class="wrapper">
+    <h3 class="ui header">
+      <slot name="title"></slot>
+      <span class="ui tiny circular label">{{ count }}</span>
+    </h3>
+    <button v-if="controls" :disabled="!previousPage" @click="fetchData(previousPage)" :class="['ui', {disabled: !previousPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle left', 'icon']"></i></button>
+    <button v-if="controls" :disabled="!nextPage" @click="fetchData(nextPage)" :class="['ui', {disabled: !nextPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle right', 'icon']"></i></button>
+    <button v-if="controls" @click="fetchData('artists/')" :class="['ui', 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'refresh', 'icon']"></i></button>
+    <div class="ui hidden divider"></div>
+    <div class="ui three cards">
+      <div v-if="isLoading" class="ui inverted active dimmer">
+        <div class="ui loader"></div>
+      </div>
+      <div class="flat inline card" v-for="object in objects" :key="object.id">
+        <div :class="['ui', 'image', 'with-overlay', {'default-cover': !getCover(object).original}]" v-lazy:background-image="getImageUrl(object)">
+          <play-button class="play-overlay" :icon-only="true" :is-playable="object.is_playable" :button-classes="['ui', 'circular', 'large', 'orange', 'icon', 'button']" :artist="object"></play-button>
+        </div>
+        <div class="content">
+          <router-link :title="object.name" :to="{name: 'library.artists.detail', params: {id: object.id}}">
+            {{ object.name|truncate(30) }}
+          </router-link>
+          <div>
+            <i class="small sound icon"></i>
+            <translate translate-context="Content/Artist/Card" :translate-params="{count: object.albums.length}" :translate-n="object.albums.length" translate-plural="%{ count } albums">1 album</translate>
+          </div>
+          <tags-list label-classes="tiny" :truncate-size="20" :limit="2" :show-more="false" :tags="object.tags"></tags-list>
+
+          <play-button
+            class="play-button basic icon"
+            :dropdown-only="true"
+            :is-playable="object.is_playable"
+            :dropdown-icon-classes="['ellipsis', 'vertical', 'large', 'grey']"
+            :artist="object"></play-button>
+        </div>
+      </div>
+    </div>
+    <div v-if="!isLoading && objects.length === 0">No results matching your query.</div>
+  </div>
+</template>
+
+<script>
+import _ from '@/lodash'
+import axios from 'axios'
+import PlayButton from '@/components/audio/PlayButton'
+import TagsList from "@/components/tags/List"
+
+export default {
+  props: {
+    filters: {type: Object, required: true},
+    controls: {type: Boolean, default: true},
+  },
+  components: {
+    PlayButton,
+    TagsList
+  },
+  data () {
+    return {
+      objects: [],
+      limit: 12,
+      count: 0,
+      isLoading: false,
+      errors: null,
+      previousPage: null,
+      nextPage: null
+    }
+  },
+  created () {
+    this.fetchData('artists/')
+  },
+  methods: {
+    fetchData (url) {
+      if (!url) {
+        return
+      }
+      this.isLoading = true
+      let self = this
+      let params = _.clone(this.filters)
+      params.page_size = this.limit
+      params.offset = this.offset
+      axios.get(url, {params: params}).then((response) => {
+        self.previousPage = response.data.previous
+        self.nextPage = response.data.next
+        self.isLoading = false
+        self.objects = response.data.results
+        self.count = response.data.count
+      }, error => {
+        self.isLoading = false
+        self.errors = error.backendErrors
+      })
+    },
+    updateOffset (increment) {
+      if (increment) {
+        this.offset += this.limit
+      } else {
+        this.offset = Math.max(this.offset - this.limit, 0)
+      }
+    },
+    getImageUrl (object) {
+      let url = '../../../assets/audio/default-cover.png'
+      let cover = this.getCover(object)
+      if (cover.original) {
+        url = this.$store.getters['instance/absoluteUrl'](cover.medium_square_crop)
+      } else {
+        return null
+      }
+      return url
+    },
+    getCover (object) {
+      return object.albums.map((a) => {
+        return a.cover
+      }).filter((c) => {
+        return !!c
+      })[0] || {}
+    }
+  },
+  watch: {
+    offset () {
+      this.fetchData()
+    },
+    "$store.state.moderation.lastUpdate": function () {
+      this.fetchData('objects/')
+    }
+  }
+}
+</script>
+<style scoped lang="scss">
+@import "../../../style/vendor/media";
+
+.default-cover {
+  background-image: url("../../../assets/audio/default-cover.png") !important;
+}
+
+.wrapper {
+  width: 100%;
+}
+.ui.cards {
+  justify-content: flex-start;
+}
+.play-button {
+  position: absolute;
+  right: 0;
+  bottom: 0;
+}
+
+.ui.three.cards .card {
+  width: 100%;
+}
+@include media(">tablet") {
+  .ui.three.cards .card {
+    width: 25em;
+  }
+}
+.with-overlay {
+  background-size: cover !important;
+  background-position: center !important;
+  height: 8em;
+  width: 8em;
+  display: flex !important;
+  justify-content: center !important;
+  align-items: center !important;
+}
+.flat.card .with-overlay.image {
+  border-radius: 50% !important;
+  margin: 0 auto;
+}
+</style>
+<style>
+.ui.cards .ui.button {
+  margin-right: 0px;
+}
+</style>
diff --git a/front/src/components/audio/track/Widget.vue b/front/src/components/audio/track/Widget.vue
index b8ad3c639c66eb756ca7f3d50148d08aa5e7c5df..ecb967ab82a1cca1db1de7304a8992e750a71956 100644
--- a/front/src/components/audio/track/Widget.vue
+++ b/front/src/components/audio/track/Widget.vue
@@ -2,12 +2,13 @@
   <div>
     <h3 class="ui header">
       <slot name="title"></slot>
+      <span v-if="showCount" class="ui tiny circular label">{{ count }}</span>
     </h3>
     <button :disabled="!previousPage" @click="fetchData(previousPage)" :class="['ui', {disabled: !previousPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle up', 'icon']"></i></button>
     <button :disabled="!nextPage" @click="fetchData(nextPage)" :class="['ui', {disabled: !nextPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle down', 'icon']"></i></button>
     <button @click="fetchData(url)" :class="['ui', 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'refresh', 'icon']"></i></button>
     <div class="ui divided unstackable items">
-      <div class="item" v-for="object in objects" :key="object.id">
+      <div :class="['item', itemClasses]" v-for="object in objects" :key="object.id">
         <div class="ui tiny image">
           <img v-if="object.track.album.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](object.track.album.cover.medium_square_crop)">
           <img v-else src="../../../assets/audio/default-cover.png">
@@ -28,7 +29,9 @@
                   </router-link>
                 </span>
               </div>
-              <div class="extra">
+              <tags-list label-classes="tiny" :truncate-size="20" :limit="2" :show-more="false" :tags="object.track.tags"></tags-list>
+
+              <div class="extra" v-if="isActivity">
                 <span class="left floated">@{{ object.user.username }}</span>
                 <span class="right floated"><human-date :date="object.creation_date" /></span>
               </div>
@@ -50,19 +53,25 @@
 import _ from '@/lodash'
 import axios from 'axios'
 import PlayButton from '@/components/audio/PlayButton'
+import TagsList from "@/components/tags/List"
 
 export default {
   props: {
     filters: {type: Object, required: true},
-    url: {type: String, required: true}
+    url: {type: String, required: true},
+    isActivity: {type: Boolean, default: true},
+    showCount: {type: Boolean, default: false},
+    limit: {type: Number, default: 5},
+    itemClasses: {type: String, default: ''},
   },
   components: {
-    PlayButton
+    PlayButton,
+    TagsList
   },
   data () {
     return {
       objects: [],
-      limit: 5,
+      count: 0,
       isLoading: false,
       errors: null,
       previousPage: null,
@@ -86,7 +95,15 @@ export default {
         self.previousPage = response.data.previous
         self.nextPage = response.data.next
         self.isLoading = false
-        self.objects = response.data.results
+        self.count = response.data.count
+        if (self.isActivity) {
+          // we have listening/favorites objects, not directly tracks
+          self.objects = response.data.results
+        } else {
+          self.objects = response.data.results.map((r) => {
+            return {track: r}
+          })
+        }
       }, error => {
         self.isLoading = false
         self.errors = error.backendErrors
@@ -129,4 +146,18 @@ export default {
 .ui.divided.items > .item:last-child {
   padding-bottom: 1em !important;
 }
+
+@include media(">tablet") {
+  .divided.items > .track-item.inline {
+    width: 25em;
+    float: left;
+    border-top: none;
+    &,
+    &:first-child {
+      margin-top: 0.5em !important;
+      margin-right: 0.5em !important;
+      padding: 1em 0 !important;
+    }
+  }
+}
 </style>
diff --git a/front/src/components/library/AlbumBase.vue b/front/src/components/library/AlbumBase.vue
index 016be2c3779e99ddc14c9dcd1ca585ea8e462a59..083e5547b09f883bfb8f00bf44ad79273d87b195 100644
--- a/front/src/components/library/AlbumBase.vue
+++ b/front/src/components/library/AlbumBase.vue
@@ -13,6 +13,7 @@
               <div v-html="subtitle"></div>
             </div>
           </h2>
+          <tags-list v-if="object.tags && object.tags.length > 0" :tags="object.tags"></tags-list>
           <div class="ui hidden divider"></div>
           <div class="header-buttons">
 
@@ -103,6 +104,7 @@ import backend from "@/audio/backend"
 import PlayButton from "@/components/audio/PlayButton"
 import EmbedWizard from "@/components/audio/EmbedWizard"
 import Modal from '@/components/semantic/Modal'
+import TagsList from "@/components/tags/List"
 
 const FETCH_URL = "albums/"
 
@@ -123,7 +125,8 @@ export default {
   components: {
     PlayButton,
     EmbedWizard,
-    Modal
+    Modal,
+    TagsList,
   },
   data() {
     return {
diff --git a/front/src/components/library/Albums.vue b/front/src/components/library/Albums.vue
index 9817af830770142065f1e84f4253cbdda23c41e6..ed97f7a4e0127063fc7f28e272de707c63d160d1 100644
--- a/front/src/components/library/Albums.vue
+++ b/front/src/components/library/Albums.vue
@@ -20,6 +20,10 @@
               </option>
             </select>
           </div>
+          <div class="field">
+            <label><translate translate-context="*/*/*/Noun">Tags</translate></label>
+            <tags-selector v-model="tags"></tags-selector>
+          </div>
           <div class="field">
             <label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label>
             <select class="ui dropdown" v-model="orderingDirection">
@@ -70,6 +74,7 @@
 </template>
 
 <script>
+import qs from 'qs'
 import axios from "axios"
 import _ from "@/lodash"
 import $ from "jquery"
@@ -81,17 +86,20 @@ import PaginationMixin from "@/components/mixins/Pagination"
 import TranslationsMixin from "@/components/mixins/Translations"
 import AlbumCard from "@/components/audio/album/Card"
 import Pagination from "@/components/Pagination"
+import TagsSelector from '@/components/library/TagsSelector'
 
 const FETCH_URL = "albums/"
 
 export default {
   mixins: [OrderingMixin, PaginationMixin, TranslationsMixin],
   props: {
-    defaultQuery: { type: String, required: false, default: "" }
+    defaultQuery: { type: String, required: false, default: "" },
+    defaultTags: { type: Array, required: false, default: () => { return [] } },
   },
   components: {
     AlbumCard,
-    Pagination
+    Pagination,
+    TagsSelector,
   },
   data() {
     let defaultOrdering = this.getOrderingFromString(
@@ -102,6 +110,7 @@ export default {
       result: null,
       page: parseInt(this.defaultPage),
       query: this.defaultQuery,
+      tags: this.defaultTags.filter((t) => { return t.length > 0 }) || [],
       paginateBy: parseInt(this.defaultPaginateBy || 25),
       orderingDirection: defaultOrdering.direction || "+",
       ordering: defaultOrdering.field,
@@ -130,6 +139,7 @@ export default {
         query: {
           query: this.query,
           page: this.page,
+          tag: this.tags,
           paginateBy: this.paginateBy,
           ordering: this.getOrderingAsString()
         }
@@ -144,10 +154,19 @@ export default {
         page_size: this.paginateBy,
         q: this.query,
         ordering: this.getOrderingAsString(),
-        playable: "true"
+        playable: "true",
+        tag: this.tags,
       }
       logger.default.debug("Fetching albums")
-      axios.get(url, { params: params }).then(response => {
+      axios.get(
+        url,
+        {
+          params: params,
+          paramsSerializer: function(params) {
+            return qs.stringify(params, { indices: false })
+          }
+        }
+      ).then(response => {
         self.result = response.data
         self.isLoading = false
       })
@@ -177,6 +196,10 @@ export default {
       this.updateQueryString()
       this.fetchData()
     },
+    tags() {
+      this.updateQueryString()
+      this.fetchData()
+    },
     "$store.state.moderation.lastUpdate": function () {
       this.fetchData()
     }
diff --git a/front/src/components/library/ArtistBase.vue b/front/src/components/library/ArtistBase.vue
index 3c21f603fe150109ade24448510bb42805ac4663..5472ee307cf76ddf65f6c203f23fb379d9aad62a 100644
--- a/front/src/components/library/ArtistBase.vue
+++ b/front/src/components/library/ArtistBase.vue
@@ -21,6 +21,7 @@
               </div>
             </div>
           </h2>
+          <tags-list v-if="object.tags && object.tags.length > 0" :tags="object.tags"></tags-list>
           <div class="ui hidden divider"></div>
           <div class="header-buttons">
             <div class="ui buttons">
@@ -123,17 +124,20 @@ import PlayButton from "@/components/audio/PlayButton"
 import EmbedWizard from "@/components/audio/EmbedWizard"
 import Modal from '@/components/semantic/Modal'
 import RadioButton from "@/components/radios/Button"
+import TagsList from "@/components/tags/List"
 
 const FETCH_URL = "albums/"
 
 
+
 export default {
   props: ["id"],
   components: {
     PlayButton,
     EmbedWizard,
     Modal,
-    RadioButton
+    RadioButton,
+    TagsList,
   },
   data() {
     return {
diff --git a/front/src/components/library/Artists.vue b/front/src/components/library/Artists.vue
index 5f4102ab1c269cc6610bf8cec3fa95b33b9d5b83..f16a6740f8954c7381cf53ef656b18d37e8952c2 100644
--- a/front/src/components/library/Artists.vue
+++ b/front/src/components/library/Artists.vue
@@ -12,6 +12,10 @@
             </label>
             <input type="text" name="search" v-model="query" :placeholder="labels.searchPlaceholder"/>
           </div>
+          <div class="field">
+            <label><translate translate-context="*/*/*/Noun">Tags</translate></label>
+            <tags-selector v-model="tags"></tags-selector>
+          </div>
           <div class="field">
             <label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
             <select class="ui dropdown" v-model="ordering">
@@ -67,6 +71,7 @@
 </template>
 
 <script>
+import qs from 'qs'
 import axios from "axios"
 import _ from "@/lodash"
 import $ from "jquery"
@@ -78,17 +83,20 @@ import PaginationMixin from "@/components/mixins/Pagination"
 import TranslationsMixin from "@/components/mixins/Translations"
 import ArtistCard from "@/components/audio/artist/Card"
 import Pagination from "@/components/Pagination"
+import TagsSelector from '@/components/library/TagsSelector'
 
 const FETCH_URL = "artists/"
 
 export default {
   mixins: [OrderingMixin, PaginationMixin, TranslationsMixin],
   props: {
-    defaultQuery: { type: String, required: false, default: "" }
+    defaultQuery: { type: String, required: false, default: "" },
+    defaultTags: { type: Array, required: false, default: () => { return [] } },
   },
   components: {
     ArtistCard,
-    Pagination
+    Pagination,
+    TagsSelector,
   },
   data() {
     let defaultOrdering = this.getOrderingFromString(
@@ -99,6 +107,7 @@ export default {
       result: null,
       page: parseInt(this.defaultPage),
       query: this.defaultQuery,
+      tags: this.defaultTags.filter((t) => { return t.length > 0 }) || [],
       paginateBy: parseInt(this.defaultPaginateBy || 12),
       orderingDirection: defaultOrdering.direction || "+",
       ordering: defaultOrdering.field,
@@ -127,6 +136,7 @@ export default {
         query: {
           query: this.query,
           page: this.page,
+          tag: this.tags,
           paginateBy: this.paginateBy,
           ordering: this.getOrderingAsString()
         }
@@ -141,10 +151,19 @@ export default {
         page_size: this.paginateBy,
         name__icontains: this.query,
         ordering: this.getOrderingAsString(),
-        playable: "true"
+        playable: "true",
+        tag: this.tags,
       }
       logger.default.debug("Fetching artists")
-      axios.get(url, { params: params }).then(response => {
+      axios.get(
+        url,
+        {
+          params: params,
+          paramsSerializer: function(params) {
+            return qs.stringify(params, { indices: false })
+          }
+        }
+      ).then(response => {
         self.result = response.data
         self.isLoading = false
       })
@@ -174,6 +193,10 @@ export default {
       this.updateQueryString()
       this.fetchData()
     },
+    tags() {
+      this.updateQueryString()
+      this.fetchData()
+    },
     "$store.state.moderation.lastUpdate": function () {
       this.fetchData()
     }
diff --git a/front/src/components/library/EditCard.vue b/front/src/components/library/EditCard.vue
index e72823aa74d7aa406e14bb7e87eea1d7ff3c0179..bb8676ceebd7f5df507c494e6dd6882190752042 100644
--- a/front/src/components/library/EditCard.vue
+++ b/front/src/components/library/EditCard.vue
@@ -49,7 +49,7 @@
           </tr>
         </thead>
         <tbody>
-          <tr v-for="field in getUpdatedFields(obj.payload, previousState)" :key="field.id">
+          <tr v-for="field in updatedFields" :key="field.id">
             <td>{{ field.id }}</td>
 
             <td v-if="field.diff">
@@ -61,12 +61,12 @@
               <translate translate-context="*/*/*">N/A</translate>
             </td>
 
-            <td v-if="field.diff">
+            <td v-if="field.diff" :title="field.newRepr">
               <span v-if="!part.removed" v-for="part in field.diff" :class="['diff', {added: part.added}]">
                 {{ part.value }}
               </span>
             </td>
-            <td v-else>{{ field.new }}</td>
+            <td v-else :title="field.newRepr">{{ field.newRepr }}</td>
           </tr>
         </tbody>
       </table>
@@ -126,6 +126,7 @@ export default {
     }
   },
   computed: {
+    configs: edits.getConfigs,
     canApprove: edits.getCanApprove,
     canDelete: edits.getCanDelete,
     previousState () {
@@ -154,6 +155,32 @@ export default {
         namespace = 'library.artists.edit.detail'
       }
       return this.$router.resolve({name: namespace, params: {id, editId: this.obj.uuid}}).href
+    },
+
+    updatedFields () {
+      let payload = this.obj.payload
+      let previousState = this.previousState
+      let fields = Object.keys(payload)
+      let self = this
+      return fields.map((f) => {
+        let fieldConfig = edits.getFieldConfig(self.configs, this.obj.target.type, f)
+        let dummyRepr = (v) => { return v }
+        let getValueRepr = fieldConfig.getValueRepr || dummyRepr
+        let d = {
+          id: f,
+        }
+        if (previousState && previousState[f]) {
+          d.old = previousState[f]
+          d.oldRepr = castValue(getValueRepr(d.old.value))
+        }
+        d.new = payload[f]
+        d.newRepr = castValue(getValueRepr(d.new))
+        if (d.old) {
+          // we compute the diffs between the old and new values
+          d.diff = diffWordsWithSpace(d.oldRepr, d.newRepr)
+        }
+        return d
+      })
     }
   },
   methods: {
@@ -184,26 +211,6 @@ export default {
         self.isLoading = false
       })
     },
-    getUpdatedFields (payload, previousState) {
-      let fields = Object.keys(payload)
-      return fields.map((f) => {
-        let d = {
-          id: f,
-        }
-        if (previousState && previousState[f]) {
-          d.old = previousState[f]
-        }
-        d.new = payload[f]
-        if (d.old) {
-          // we compute the diffs between the old and new values
-
-          let oldValue = castValue(d.old.value)
-          let newValue = castValue(d.new)
-          d.diff = diffWordsWithSpace(oldValue, newValue)
-        }
-        return d
-      })
-    }
   }
 }
 </script>
diff --git a/front/src/components/library/EditForm.vue b/front/src/components/library/EditForm.vue
index 617917c6812f03924934c48ec0e7d92079f95a72..ee462a1e20305eff2aa50b605d91fad1e6f8371b 100644
--- a/front/src/components/library/EditForm.vue
+++ b/front/src/components/library/EditForm.vue
@@ -77,10 +77,22 @@
           </button>
 
         </template>
+        <template v-else-if="fieldConfig.type === 'tags'">
+          <label :for="fieldConfig.id">{{ fieldConfig.label }}</label>
+          <tags-selector
+            ref="tags"
+            v-model="values[fieldConfig.id]"
+            :id="fieldConfig.id"
+            required="fieldConfig.required"></tags-selector>
+          <button class="ui tiny basic left floated button" form="noop" @click.prevent="values[fieldConfig.id] = []">
+            <i class="x icon"></i>
+            <translate translate-context="Content/Library/Button.Label">Clear</translate>
+          </button>
+        </template>
         <div v-if="values[fieldConfig.id] != initialValues[fieldConfig.id]">
           <button class="ui tiny basic right floated reset button" form="noop" @click.prevent="values[fieldConfig.id] = initialValues[fieldConfig.id]">
             <i class="undo icon"></i>
-            <translate translate-context="Content/Library/Button.Label" :translate-params="{value: initialValues[fieldConfig.id] || ''}">Reset to initial value: %{ value }</translate>
+            <translate translate-context="Content/Library/Button.Label">Reset to initial value</translate>
           </button>
         </div>
       </div>
@@ -110,13 +122,17 @@ import _ from '@/lodash'
 import axios from "axios"
 import EditList from '@/components/library/EditList'
 import EditCard from '@/components/library/EditCard'
+import TagsSelector from '@/components/library/TagsSelector'
 import edits from '@/edits'
 
+import lodash from '@/lodash'
+
 export default {
   props: ["objectType", "object", "licenses"],
   components: {
     EditList,
-    EditCard
+    EditCard,
+    TagsSelector
   },
   data() {
     return {
@@ -159,7 +175,7 @@ export default {
     mutationPayload () {
       let self = this
       let changedFields = this.config.fields.filter(f => {
-        return self.values[f.id] != self.initialValues[f.id]
+        return !lodash.isEqual(self.values[f.id], self.initialValues[f.id])
       })
       if (changedFields.length === 0) {
         return null
diff --git a/front/src/components/library/TagDetail.vue b/front/src/components/library/TagDetail.vue
new file mode 100644
index 0000000000000000000000000000000000000000..86ad1562a342397fbb9e2fdadbc20647318ce79d
--- /dev/null
+++ b/front/src/components/library/TagDetail.vue
@@ -0,0 +1,79 @@
+<template>
+  <main v-title="labels.title">
+    <section class="ui vertical stripe segment">
+      <h2 class="ui header">
+        <span class="ui circular huge hashtag label">
+          {{ labels.title }}
+        </span>
+      </h2>
+      <div class="ui hidden divider"></div>
+      <div class="ui row">
+        <artist-widget :controls="false" :filters="{playable: true, ordering: '-creation_date', tag: id}">
+          <template slot="title">
+            <router-link :to="{name: 'library.artists.browse', query: {tag: id}}">
+              <translate translate-context="*/*/*">Artists</translate>
+            </router-link>
+          </template>
+        </artist-widget>
+        <div class="ui hidden divider"></div>
+        <div class="ui hidden divider"></div>
+        <album-widget :show-count="true" :controls="false" :filters="{playable: true, ordering: '-creation_date', tag: id}">
+          <template slot="title">
+            <router-link :to="{name: 'library.albums.browse', query: {tag: id}}">
+              <translate translate-context="*/*/*">Albums</translate>
+            </router-link>
+          </template>
+        </album-widget>
+        <div class="ui hidden divider"></div>
+        <div class="ui hidden divider"></div>
+        <track-widget :show-count="true" :limit="12" item-classes="track-item inline" :url="'/tracks/'" :is-activity="false" :filters="{playable: true, ordering: '-creation_date', tag: id}">
+          <template slot="title">
+            <translate translate-context="*/*/*">Tracks</translate>
+          </template>
+        </track-widget>
+        <div class="ui clearing hidden divider"></div>
+      </div>
+    </section>
+  </main>
+</template>
+
+<script>
+
+
+import TrackWidget from "@/components/audio/track/Widget"
+import AlbumWidget from "@/components/audio/album/Widget"
+import ArtistWidget from "@/components/audio/artist/Widget"
+
+export default {
+  props: {
+    id: { type: String, required: true }
+  },
+  components: {
+    ArtistWidget,
+    AlbumWidget,
+    TrackWidget,
+  },
+  computed: {
+    labels() {
+      let title = `#${this.id}`
+      return {
+        title
+      }
+    },
+    isAuthenticated () {
+      return this.$store.state.auth.authenticated
+    },
+    hasFavorites () {
+      return this.$store.state.favorites.count > 0
+    },
+  },
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+.ui.circular.label {
+  padding-left: 1em !important;
+  padding-right: 1em !important;
+}
+</style>
diff --git a/front/src/components/library/TagsSelector.vue b/front/src/components/library/TagsSelector.vue
new file mode 100644
index 0000000000000000000000000000000000000000..47cb83faeb27d01af8a0665ec9c4ab4c18e7b2e1
--- /dev/null
+++ b/front/src/components/library/TagsSelector.vue
@@ -0,0 +1,68 @@
+<template>
+  <div ref="dropdown" class="ui multiple search selection dropdown">
+    <input type="hidden">
+    <i class="dropdown icon"></i>
+    <input type="text" class="search">
+    <div class="default text">
+      <translate translate-context="*/Dropdown/Placeholder/Verb">Search for tags…</translate>
+    </div>
+  </div>
+</template>
+<script>
+import $ from 'jquery'
+
+import lodash from '@/lodash'
+export default {
+  props: ['value'],
+  mounted () {
+    this.$nextTick(() => {
+      this.initDropdown()
+
+    })
+  },
+  methods: {
+    initDropdown () {
+      let self = this
+      let handleUpdate = () => {
+        let value = $(self.$refs.dropdown).dropdown('get value').split(',')
+        self.$emit('input', value)
+        return value
+      }
+      let settings = {
+        saveRemoteData: false,
+        filterRemoteData: true,
+        apiSettings: {
+          url: this.$store.getters['instance/absoluteUrl']('/api/v1/tags/?name__startswith={query}&ordering=length&page_size=5'),
+          beforeXHR: function (xhrObject) {
+            xhrObject.setRequestHeader('Authorization', self.$store.getters['auth/header'])
+            return xhrObject
+          },
+        },
+        fields: {
+          remoteValues: 'results',
+          value: 'name'
+        },
+        allowAdditions: true,
+        minCharacters: 1,
+        onAdd: handleUpdate,
+        onRemove: handleUpdate,
+        onLabelRemove: handleUpdate,
+        onChange: handleUpdate,
+      }
+      $(this.$refs.dropdown).dropdown(settings)
+      $(this.$refs.dropdown).dropdown('set exactly', this.value)
+    }
+  },
+  watch: {
+    value: {
+      handler (v) {
+        let current = $(this.$refs.dropdown).dropdown('get value').split(',').sort()
+        if (!lodash.isEqual([...v].sort(), current)) {
+          $(this.$refs.dropdown).dropdown('set exactly', v)
+        }
+      },
+      deep: true
+    }
+  }
+}
+</script>
diff --git a/front/src/components/library/TrackBase.vue b/front/src/components/library/TrackBase.vue
index 2f3cc51a9d74b1b855e2edefa6288704a6bcb0db..32942958ba44a091207c98cfa96c2d5721b06a1a 100644
--- a/front/src/components/library/TrackBase.vue
+++ b/front/src/components/library/TrackBase.vue
@@ -17,6 +17,8 @@
               <div class="sub header" v-html="subtitle"></div>
             </div>
           </h2>
+          <tags-list v-if="track.tags && track.tags.length > 0" :tags="track.tags"></tags-list>
+          <div class="ui hidden divider"></div>
           <div class="header-buttons">
             <div class="ui buttons">
               <play-button class="orange" :track="track">
@@ -121,6 +123,7 @@ import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon"
 import TrackPlaylistIcon from "@/components/playlists/TrackPlaylistIcon"
 import Modal from '@/components/semantic/Modal'
 import EmbedWizard from "@/components/audio/EmbedWizard"
+import TagsList from "@/components/tags/List"
 
 const FETCH_URL = "tracks/"
 
@@ -131,7 +134,8 @@ export default {
     TrackPlaylistIcon,
     TrackFavoriteIcon,
     Modal,
-    EmbedWizard
+    EmbedWizard,
+    TagsList,
   },
   data() {
     return {
diff --git a/front/src/components/tags/List.vue b/front/src/components/tags/List.vue
new file mode 100644
index 0000000000000000000000000000000000000000..6c280822bd9bc6c5dc1859d288dd4983cbdb1600
--- /dev/null
+++ b/front/src/components/tags/List.vue
@@ -0,0 +1,48 @@
+<template>
+  <div class="tag-list">
+    <router-link
+      :to="{name: 'library.tags.detail', params: {id: tag}}"
+      :class="['ui', 'circular', 'hashtag', 'label', labelClasses]"
+      v-for="tag in toDisplay"
+      :title="tag"
+      :key="tag">
+      #{{ tag|truncate(truncateSize) }}
+    </router-link>
+    <div role="button" @click.prevent="honorLimit = false" class="ui circular inverted teal label" v-if="showMore && toDisplay.length < tags.length">
+      <translate translate-context="Content/*/Button/Label/Verb" :translate-params="{count: tags.length - toDisplay.length}" :translate-n="tags.length - toDisplay.length" translate-plural="Show %{ count } more tags">Show 1 more tag</translate>
+    </div>
+  </div>
+</template>
+<script>
+export default {
+  props: {
+    tags: {type: Array, required: true},
+    showMore: {type: Boolean, default: true},
+    truncateSize: {type: Number, default: 25},
+    limit: {type: Number, default: 5},
+    labelClasses: {type: String, default: ''},
+  },
+  data () {
+    return {
+      honorLimit: true,
+    }
+  },
+  computed: {
+    toDisplay () {
+      if (!this.honorLimit) {
+        return this.tags
+      }
+      return (this.tags || []).slice(0, this.limit)
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.ui.circular.label {
+  padding-left: 1em !important;
+  padding-right: 1em !important;
+}
+.hashtag {
+  margin: 0.25em;
+}
+</style>
diff --git a/front/src/edits.js b/front/src/edits.js
index 76e92e8418b5ea4cfcfbf68e72476eae3f946bfd..5c9e9be880e90cdae1041e7ef96f1c1d3bca5324 100644
--- a/front/src/edits.js
+++ b/front/src/edits.js
@@ -1,3 +1,10 @@
+function getTagsValueRepr (val) {
+  if (!val) {
+    return ''
+  }
+  return val.slice().sort().join('\n')
+}
+
 export default {
   getConfigs () {
     return {
@@ -10,6 +17,14 @@ export default {
             label: this.$pgettext('*/*/*/Noun', 'Name'),
             getValue: (obj) => { return obj.name }
           },
+          {
+            id: 'tags',
+            type: 'tags',
+            required: true,
+            label: this.$pgettext('*/*/*/Noun', 'Tags'),
+            getValue: (obj) => { return obj.tags },
+            getValueRepr: getTagsValueRepr
+          }
         ]
       },
       album: {
@@ -28,6 +43,14 @@ export default {
             label: this.$pgettext('Content/*/*/Noun', 'Release date'),
             getValue: (obj) => { return obj.release_date }
           },
+          {
+            id: 'tags',
+            type: 'tags',
+            required: true,
+            label: this.$pgettext('*/*/*/Noun', 'Tags'),
+            getValue: (obj) => { return obj.tags },
+            getValueRepr: getTagsValueRepr
+          }
         ]
       },
       track: {
@@ -61,6 +84,14 @@ export default {
             label: this.$pgettext('Content/*/*/Noun', 'License'),
             getValue: (obj) => { return obj.license },
           },
+          {
+            id: 'tags',
+            type: 'tags',
+            required: true,
+            label: this.$pgettext('*/*/*/Noun', 'Tags'),
+            getValue: (obj) => { return obj.tags },
+            getValueRepr: getTagsValueRepr
+          }
         ]
       }
     }
@@ -69,7 +100,12 @@ export default {
   getConfig () {
     return this.configs[this.objectType]
   },
-
+  getFieldConfig (configs, type, fieldId) {
+    let c = configs[type]
+    return c.fields.filter((f) => {
+      return f.id == fieldId
+    })[0]
+  },
   getCurrentState () {
     let self = this
     let s = {}
diff --git a/front/src/lodash.js b/front/src/lodash.js
index 8cd3ed92f41e86b0eff9b88bd5af923742b1864e..31fdbaa58c7275ec94fc559e28b2d1d40c620dd9 100644
--- a/front/src/lodash.js
+++ b/front/src/lodash.js
@@ -12,4 +12,5 @@ export default {
   uniq: require('lodash/uniq'),
   remove: require('lodash/remove'),
   reverse: require('lodash/reverse'),
+  isEqual: require('lodash/isEqual'),
 }
diff --git a/front/src/router/index.js b/front/src/router/index.js
index 6a9ba6112f7680766b22945bfd41097db3f143aa..1d49967467a7cbfa15bcfe19f9b567d6cafb640b 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -401,6 +401,7 @@ export default new Router({
           props: (route) => ({
             defaultOrdering: route.query.ordering,
             defaultQuery: route.query.query,
+            defaultTags: Array.isArray(route.query.tag || []) ? route.query.tag : [route.query.tag],
             defaultPaginateBy: route.query.paginateBy,
             defaultPage: route.query.page
           })
@@ -413,6 +414,7 @@ export default new Router({
           props: (route) => ({
             defaultOrdering: route.query.ordering,
             defaultQuery: route.query.query,
+            defaultTags: Array.isArray(route.query.tag || []) ? route.query.tag : [route.query.tag],
             defaultPaginateBy: route.query.paginateBy,
             defaultPage: route.query.page
           })
@@ -471,6 +473,13 @@ export default new Router({
             id: route.params.id,
             defaultEdit: route.query.mode === 'edit' })
         },
+        {
+          path: 'tags/:id',
+          name: 'library.tags.detail',
+          component: () =>
+            import(/* webpackChunkName: "core" */ "@/components/library/TagDetail"),
+          props: true,
+        },
         {
           path: 'artists/:id',
           component: () =>
diff --git a/front/src/style/_main.scss b/front/src/style/_main.scss
index a15f339566e902646e327cb1f09c906a86a80b49..9a2fe1722a2889468881305c581f5745bf67d397 100644
--- a/front/src/style/_main.scss
+++ b/front/src/style/_main.scss
@@ -342,6 +342,20 @@ td.align.right {
   word-wrap: break-word;
 }
 
+.ui.cards > .flat.card, .flat.card {
+  box-shadow: none;
+  .content {
+    border: none;
+  }
+}
+
+.ui.cards > .inline.card {
+  flex-direction: row;
+  .content {
+    padding: 0.5em 0.75em;
+  }
+}
+
 .ui.checkbox label {
   cursor: pointer;
 }
@@ -355,6 +369,9 @@ input + .help {
 }
 
 
+.tag-list {
+  margin-top: 0.5em;
+}
 
 @import "./themes/_light.scss";
 @import "./themes/_dark.scss";
diff --git a/front/yarn.lock b/front/yarn.lock
index 1959e9e1a99365dc02c6c9bd550592dce33854e0..68e128385b4c2f92d5b3fa9bbbc4228d9f0551f7 100644
--- a/front/yarn.lock
+++ b/front/yarn.lock
@@ -7324,7 +7324,7 @@ q@^1.1.2:
   resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
   integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
 
-qs@6.7.0:
+qs@6.7.0, qs@^6.7.0:
   version "6.7.0"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
   integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==