diff --git a/front/src/components/audio/artist/Widget.vue b/front/src/components/audio/artist/Widget.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b78bcbf943947652e28f0c43c386e1b19a88b4b0
--- /dev/null
+++ b/front/src/components/audio/artist/Widget.vue
@@ -0,0 +1,166 @@
+<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: 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/tags/List.vue b/front/src/components/tags/List.vue
index 9ffb0f1f080752787e8df137a2f6046c9cec737e..6c280822bd9bc6c5dc1859d288dd4983cbdb1600 100644
--- a/front/src/components/tags/List.vue
+++ b/front/src/components/tags/List.vue
@@ -1,23 +1,29 @@
 <template>
-  <div>
+  <div class="tag-list">
     <router-link
       :to="{name: 'library.tags.detail', params: {id: tag}}"
-      class="ui circular hashtag label"
+      :class="['ui', 'circular', 'hashtag', 'label', labelClasses]"
       v-for="tag in toDisplay"
+      :title="tag"
       :key="tag">
-      #{{ tag }}
+      #{{ tag|truncate(truncateSize) }}
     </router-link>
-    <div role="button" @click.prevent="honorLimit = false" class="ui circular inverted teal label" v-if="toDisplay.length < tags.length">
+    <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'],
+  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 {
-      limit: 5,
       honorLimit: true,
     }
   },
@@ -36,4 +42,7 @@ export default {
   padding-left: 1em !important;
   padding-right: 1em !important;
 }
+.hashtag {
+  margin: 0.25em;
+}
 </style>
diff --git a/front/src/style/_main.scss b/front/src/style/_main.scss
index a15f339566e902646e327cb1f09c906a86a80b49..90fdd8936bde3caf3b023127398099a93d46e2b3 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: 1em;
+}
 
 @import "./themes/_light.scss";
 @import "./themes/_dark.scss";