diff --git a/src/listcontroller.js b/src/listcontroller.js
index 36ef1d58f29ac324945a8e94849610745dd9ee0a..a527ece5ed22bb0f6e86af050e199f838a9beb6f 100644
--- a/src/listcontroller.js
+++ b/src/listcontroller.js
@@ -14,6 +14,7 @@ export default class DatalistController {
     }
     this.query = query || {};
     this.search = null;
+    this.filter = null;
     // state pointer that is counted up every time the table is refreshed so
     // we can tell infinite scroll that the data-version has changed.
     this.stateCounter = Stream(0);
@@ -45,6 +46,7 @@ export default class DatalistController {
     const query = Object.assign({}, this.query);
     query.max_results = 10;
     query.page = pageNum;
+    query.where = { ...this.filter, ...this.query.where };
 
     return new Promise((resolve) => {
       this.handler.get(query).then((data) => {
@@ -100,6 +102,11 @@ export default class DatalistController {
     }
   }
 
+  setFilter(filter) {
+    this.filter = filter;
+    this.refresh();
+  }
+
   setQuery(query) {
     this.query = query;
     this.query.search = this.search;
diff --git a/src/views/elements.js b/src/views/elements.js
index 66358954223cdf02da3a7c98f06f4847e78e1817..24cd5ca32d56d404c620f46a7d87091379d0b75b 100644
--- a/src/views/elements.js
+++ b/src/views/elements.js
@@ -289,33 +289,37 @@ export class chip {
   view({
     attrs: {
       svg,
-      color = '#000000',
-      background = '#dddddd',
-      ...styleAttrs
+      background = '#ffffff',
+      textColor = '#000000',
+      svgColor = '#000000',
+      svgBackground = '#dddddd',
+      ...attrs
     },
     children,
   }) {
     return m('div', {
       style: {
         height: '32px',
-        'background-color': '#ffffff',
+        'background-color': background,
+        color: textColor,
         'border-radius': '16px',
         // if there is a border, things are weirdly shifted
-        padding: styleAttrs.border ? '3px 8px 4px 6px' : '4px 8px',
+        padding: attrs.border ? '3px 8px 4px 6px' : '4px 8px',
         display: 'inline-flex',
-        ...styleAttrs,
+        ...attrs,
       },
+      ...attrs.onclick ? { onclick: attrs.onclick } : {},
     }, [
       svg && m('div', {
         style: {
-          'background-color': background,
+          'background-color': svgBackground,
           'border-radius': '12px',
           margin: '0px 4px 0px -2px',
           height: '24px',
           width: '24px',
           padding: '2px 2px 2px 4px',
         },
-      }, m(Icon, { svg: { content: m.trust(svg) }, size: 'small', style: { color } })),
+      }, m(Icon, { svg: { content: m.trust(svg) }, size: 'small', style: { svgColor } })),
       m('div', { style: { 'line-height': '24px' } }, children),
     ]);
   }
diff --git a/src/views/tableView.js b/src/views/tableView.js
index 2baf072c8b53cefcd8b62b414624530ba2b48eb7..d24975bac2e729314e9c3239c2e662cc13957073 100644
--- a/src/views/tableView.js
+++ b/src/views/tableView.js
@@ -3,22 +3,14 @@ import infinite from 'mithril-infinite';
 import { List, ListTile, Toolbar, Search, Button } from 'polythene-mithril';
 import 'polythene-css';
 import { styler } from 'polythene-core-css';
+import { chip, icons } from './elements';
 
 const tableStyles = [
   {
-    '.tabletool': {
-      display: 'grid',
-      height: '100%',
-      'grid-template-rows': '48px calc(100% - 78px)',
-      'background-color': 'white',
-    },
     '.toolbar': {
       'grid-row': 1,
       display: 'flex',
     },
-    '.scrollTable': {
-      'grid-row': 2,
-    },
     '.tableTile': {
       padding: '10px',
       'border-bottom': '1px solid rgba(0, 0, 0, 0.12)',
@@ -29,6 +21,22 @@ const tableStyles = [
 
 styler.add('tableview', tableStyles);
 
+
+class FilterChip {
+  view({ attrs: { selected = false, onclick = () => {} }, children }) {
+    return m(chip, {
+      'margin-left': '5px',
+      'margin-right': '5px',
+      background: selected ? '#aaaaaa' : '#dddddd',
+      svgBackground: '#aaaaaa',
+      textColor: selected ? '#000000' : '#999999',
+      svgColor: '#000000',
+      svg: selected ? icons.checked : null,
+      onclick,
+    }, children);
+  }
+}
+
 export default class TableView {
   /* Shows a table of objects for a given API resource.
    *
@@ -40,11 +48,16 @@ export default class TableView {
    *       Works with embedded resources, i.e. if you add
    *       { embedded: { event: 1 } } to a list of eventsignups,
    *       you can display event.title_de as a table key
+   *   - filters: list of list of objects, each inner list is a group of mutual exclusive
+   *       filters.
+   *       A filter can have properties 'name', 'query' and optionally 'selected' for
+   *       the initial selection state.
    */
   constructor({
     attrs: {
       keys,
       tileContent,
+      filters = null,
       clickOnRows = (data) => { m.route.set(`/${data._links.self.href}`); },
     },
   }) {
@@ -53,6 +66,9 @@ export default class TableView {
     this.tileContent = tileContent;
     this.clickOnRows = clickOnRows;
     this.searchValue = '';
+    // make a copy of filters so we can toggle the selected status
+    this.filters = filters ? filters.map(filterGroup =>
+      filterGroup.map(filter => Object.assign({}, filter))) : null;
   }
 
   getItemData(data) {
@@ -93,7 +109,15 @@ export default class TableView {
       tableHeight = false,
     },
   }) {
-    return m('div.tabletool', [
+    return m('div.tabletool', {
+      style: {
+        display: 'grid',
+        height: '100%',
+        'grid-template-rows': this.filters ?
+          '48px 40px calc(100% - 78px)' : '48px calc(100% - 78px)',
+        'background-color': 'white',
+      },
+    }, [
       m(Toolbar, {
         className: 'toolbar',
         compact: true,
@@ -119,9 +143,45 @@ export default class TableView {
           }) : '',
         ],
       }),
+      // please beare with this code, it is the only way possible to track the selection
+      // status of all the filters of the same group and make sure that they are really
+      // mutually exclusive (that way when you click on one filter in the group, the other
+      // ones in this group will be deselected)
+      this.filters && m('div', {
+        style: {
+          height: '40px',
+          'overflow-x': 'scroll',
+          'white-space': 'nowrap',
+          padding: '0px 5px',
+        },
+      }, [].concat(['Filters: '], ...[...this.filters.keys()].map(filterGroupIdx =>
+        [...this.filters[filterGroupIdx].keys()].map((filterIdx) => {
+          const thisFilter = this.filters[filterGroupIdx][filterIdx];
+          return m(FilterChip, {
+            selected: thisFilter.selected,
+            onclick: () => {
+              if (!thisFilter.selected) {
+                // set all filters in this group to false
+                [...this.filters[filterGroupIdx].keys()].forEach((i) => {
+                  this.filters[filterGroupIdx][i].selected = false;
+                });
+                // now set this filter to selected
+                this.filters[filterGroupIdx][filterIdx].selected = true;
+                console.log('filter set: ', thisFilter.query);
+                controller.setFilter(thisFilter.query);
+              } else {
+                this.filters[filterGroupIdx][filterIdx].selected = false;
+                controller.setFilter({});
+              }
+            },
+          }, thisFilter.name);
+        })))),
       m(List, {
         className: 'scrollTable',
-        style: tableHeight ? { height: tableHeight } : {},
+        style: {
+          'grid-row': this.filters ? 3 : 2,
+          ...tableHeight ? { height: tableHeight } : {},
+        },
         tiles: [
           m(ListTile, {
             className: 'tableTile',