diff --git a/gui/src/components/editQuantity/QueryEditQuantity.spec.js b/gui/src/components/editQuantity/QueryEditQuantity.spec.js
index 87cd951f0d6b7c8c157afec3822c28050a4ca99f..5fc095d6f572351382c4471eef05e0893997dcf6 100644
--- a/gui/src/components/editQuantity/QueryEditQuantity.spec.js
+++ b/gui/src/components/editQuantity/QueryEditQuantity.spec.js
@@ -34,7 +34,7 @@ const quantityDef = {
 
 const testSearchDialogCancelButton = async () => {
   const dialog = screen.getByTestId('search-dialog')
-  await waitFor(() => expect(screen.queryByText('visibility=visible')).toBeInTheDocument())
+  await waitFor(() => expect(screen.queryByText('visible')).toBeInTheDocument())
 
   // cancel the search
   await userEvent.click(within(dialog).getByRole('button', {name: /cancel/i}))
@@ -43,7 +43,7 @@ const testSearchDialogCancelButton = async () => {
 
 const testSearchDialogOkButton = async () => {
   const dialog = screen.getByTestId('search-dialog')
-  await waitFor(() => expect(screen.queryByText('visibility=visible')).toBeInTheDocument())
+  await waitFor(() => expect(screen.queryByText('visible')).toBeInTheDocument())
 
   // accept the search
   await userEvent.click(within(dialog).getByTestId('search-dialog-ok'))
diff --git a/gui/src/components/search/FilterChip.js b/gui/src/components/search/FilterChip.js
deleted file mode 100644
index 7c0382b4f5ae14a74eed9fa4eb7d609e7bec7d47..0000000000000000000000000000000000000000
--- a/gui/src/components/search/FilterChip.js
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * Copyright The NOMAD Authors.
- *
- * This file is part of NOMAD. See https://nomad-lab.eu for further info.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import React from 'react'
-import clsx from 'clsx'
-import { makeStyles } from '@material-ui/core/styles'
-import LockIcon from '@material-ui/icons/Lock'
-import { Chip, Tooltip, Typography } from '@material-ui/core'
-import PropTypes from 'prop-types'
-import FilterTitle from './FilterTitle'
-
-/**
- * Thin wrapper for MUI Chip that is used for displaying (and possibly removing)
- * filter values.
- */
-const useStyles = makeStyles(theme => ({
-  root: {
-    padding: theme.spacing(0.5),
-    boxSizing: 'border-box',
-    maxWidth: '100%'
-  },
-  chip: {
-    padding: theme.spacing(0.5),
-    maxWidth: '100%'
-  }
-}))
-export const FilterChip = React.memo(({
-  label,
-  onDelete,
-  color,
-  className,
-  locked
-}) => {
-  const styles = useStyles()
-
-  return <div className={clsx(className, styles.root)}>
-    <Tooltip title={locked ? 'This filter is locked in the current search context: it cannot be removed or modified.' : ''}>
-      <Chip
-        label={label}
-        onDelete={locked ? undefined : onDelete}
-        color={locked ? undefined : color}
-        className={styles.chip}
-        icon={locked ? <LockIcon/> : undefined}
-      />
-    </Tooltip>
-  </div>
-})
-
-FilterChip.propTypes = {
-  label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
-  onDelete: PropTypes.func,
-  color: PropTypes.string,
-  className: PropTypes.string,
-  locked: PropTypes.bool
-}
-FilterChip.defaultProps = {
-  color: 'primary'
-}
-
-export default FilterChip
-
-/**
- * Used to group several related filter chips inside one container.
- */
-const useFilterChipGroupStyles = makeStyles(theme => ({
-  root: {
-    borderRadius: theme.spacing(1),
-    boxSizing: 'border-box',
-    width: '100%'
-  },
-  paper: {
-    display: 'flex',
-    flexDirection: 'row',
-    flexWrap: 'wrap',
-    alignItems: 'center',
-    width: '100%',
-    boxSizing: 'border-box'
-  },
-  title: {
-    marginLeft: theme.spacing(0.5),
-    color: theme.palette.grey[600]
-  }
-}))
-export const FilterChipGroup = React.memo(({
-  quantity,
-  className,
-  children
-}) => {
-  const styles = useFilterChipGroupStyles()
-
-  return <div className={clsx(className, styles.root)}>
-    <FilterTitle
-      quantity={quantity}
-      variant="caption"
-      classes={{text: styles.title}}
-    />
-    <div className={styles.paper}>
-      {children}
-    </div>
-  </div>
-})
-
-FilterChipGroup.propTypes = {
-  quantity: PropTypes.string,
-  color: PropTypes.string,
-  className: PropTypes.string,
-  children: PropTypes.node
-}
-FilterChipGroup.defaultProps = {
-  color: 'primary'
-}
-
-/**
- * Operators between filter chips.
- */
-const useFilterOpStyles = makeStyles(theme => ({
-  root: {
-    fontSize: '0.65rem'
-  }
-}))
-const FilterOp = React.memo(({className, children}) => {
-  const styles = useFilterOpStyles()
-  return <Typography variant="caption" className={clsx(className, styles.root)}>{children}</Typography>
-})
-
-FilterOp.propTypes = {
-  className: PropTypes.string,
-  children: PropTypes.node
-}
-
-export const FilterAnd = React.memo(() => {
-  return <FilterOp>AND</FilterOp>
-})
-
-export const FilterOr = React.memo(() => {
-  return <FilterOp>OR</FilterOp>
-})
diff --git a/gui/src/components/search/FilterSummary.js b/gui/src/components/search/FilterSummary.js
deleted file mode 100644
index 16b21329cfe66fbfb261875c7e14315feef577fc..0000000000000000000000000000000000000000
--- a/gui/src/components/search/FilterSummary.js
+++ /dev/null
@@ -1,217 +0,0 @@
-/*
- * Copyright The NOMAD Authors.
- *
- * This file is part of NOMAD. See https://nomad-lab.eu for further info.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import React, { useCallback } from 'react'
-import { makeStyles, useTheme } from '@material-ui/core/styles'
-import PropTypes from 'prop-types'
-import clsx from 'clsx'
-import { isNil, isPlainObject } from 'lodash'
-import { FilterChip, FilterChipGroup, FilterAnd, FilterOr } from './FilterChip'
-import { useSearchContext } from './SearchContext'
-import { useUnitContext } from '../units/UnitContext'
-
-/**
- * Smart component that displays a set of FilterGroups and FilterChips for the
- * given quantities.
- */
-const useStyles = makeStyles(theme => {
-  const paddingVertical = theme.spacing(1)
-  const paddingHorizontal = theme.spacing(1.5)
-  return {
-    root: {
-      boxShadow: 'inset 0 0 8px 1px rgba(0,0,0, 0.06)',
-      backgroundColor: theme.palette.background.default,
-      width: '100%',
-      paddingRight: paddingHorizontal,
-      paddingLeft: paddingHorizontal,
-      paddingTop: paddingVertical,
-      paddingBottom: paddingVertical,
-      boxSizing: 'border-box',
-      display: 'flex',
-      flexDirection: 'row',
-      flexWrap: 'wrap',
-      alignItems: 'center',
-      justifyContent: 'center'
-    },
-    chip: {
-      padding: theme.spacing(0.5)
-    }
-  }
-})
-const FilterSummary = React.memo(({
-  quantities,
-  className,
-  classes
-}) => {
-  const { filterData, filterAbbreviations, useFilters, useUpdateFilter } = useSearchContext()
-  const filters = useFilters(quantities)
-  const updateFilter = useUpdateFilter()
-  const theme = useTheme()
-  const {units} = useUnitContext()
-  const styles = useStyles({classes: classes, theme: theme})
-
-  // Creates a set of chips for a quantity
-  const createChips = useCallback((name, label, filterValue, onDelete, locked, nested) => {
-    const newChips = []
-    if (isNil(filterValue)) {
-      return
-    }
-    // If query has multiple elements, we display a chip for each. For
-    // numerical values we also display the quantity name.
-    const {serializerPretty: serializer, customSerialization} = filterData[name]
-    const isArray = filterValue instanceof Array
-    const isSet = filterValue instanceof Set
-    const isObj = isPlainObject(filterValue)
-    let op = null
-    if (locked) {
-      op = <FilterAnd/>
-    } else if (filterData[name].queryMode === "any") {
-      op = <FilterOr/>
-    } else if (filterData[name].queryMode === "all") {
-      op = <FilterAnd/>
-    }
-    if (customSerialization) {
-      const item = <FilterChip
-        locked={locked}
-        label={serializer(filterValue)}
-        onDelete={() => {
-          onDelete(undefined)
-        }}
-      />
-      newChips.push({comp: item, op})
-    } else if (isArray || isSet) {
-      filterValue.forEach((value, index) => {
-        const displayValue = serializer(value, units)
-        const item = <FilterChip
-          locked={locked}
-          label={nested ? `${label}=${displayValue}` : displayValue}
-          onDelete={() => {
-            let newValue
-            if (isSet) {
-              newValue = new Set(filterValue)
-              newValue.delete(value)
-            } else if (isArray) {
-              newValue = [...filterValue]
-              newValue.splice(index, 1)
-            }
-            onDelete(newValue)
-          }}
-        />
-        newChips.push({comp: item, op})
-      })
-    } else if (isObj) {
-      // Range queries
-      const lte = serializer(filterValue.lte, units)
-      const gte = serializer(filterValue.gte, units)
-      const lt = serializer(filterValue.lt, units)
-      const gt = serializer(filterValue.gt, units)
-      if (!isNil(gte) || !isNil(gt) || !isNil(lte) || !isNil(lt)) {
-        let content
-        if ((!isNil(gte) || !isNil(gt)) && (isNil(lte) && isNil(lt))) {
-          content = `${label}${!isNil(gte) ? ` >= ${gte}` : ''}${!isNil(gt) ? ` > ${gt}` : ''}`
-        } else if ((!isNil(lte) || !isNil(lt)) && (isNil(gte) && isNil(gt))) {
-          content = `${label}${!isNil(lte) ? ` <= ${lte}` : ''}${!isNil(lt) ? ` < ${lt}` : ''}`
-        } else {
-          content = `${!isNil(gte) ? `${gte} <= ` : ''}${!isNil(gt) ? `${gt} < ` : ''}${label}${!isNil(lte) ? ` <= ${lte}` : ''}${!isNil(lt) ? ` < ${lt}` : ''}`
-        }
-        const item = <FilterChip
-          locked={locked}
-          label={content}
-          onDelete={() => {
-            onDelete(undefined)
-          }}
-        />
-        newChips.push({comp: item, op})
-      }
-    } else {
-      const item = <FilterChip
-        locked={locked}
-        label={`${label}=${serializer(filterValue)}`}
-        onDelete={() => {
-          onDelete(undefined)
-        }}
-      />
-      newChips.push({comp: item, op})
-    }
-    return newChips.map((chip, index) => (
-      <React.Fragment key={`${name}${index}`}>
-        {chip.comp}{index !== newChips.length - 1 && chip.op}
-      </React.Fragment>
-    ))
-  }, [filterData, units])
-
-  // Create chips for all of the requested quantities
-  const chips = []
-  for (const quantity of quantities || []) {
-    const filterValue = filters[quantity]
-    if (isNil(filterValue)) {
-      continue
-    }
-    // Nested filters
-    const isSection = filterData[quantity].section
-    let newChips = []
-    if (isSection) {
-      function addChipsForSection(data, locked) {
-        if (isNil(data)) return
-        const entries = Object.entries(data)
-        entries.forEach(([key, value], index) => {
-          const onDelete = (newValue) => {
-            const newSection = {...data}
-            if (newValue === undefined) {
-              delete newSection[key]
-            } else {
-              newSection[key] = newValue
-            }
-            updateFilter([quantity, newSection])
-          }
-          newChips = newChips.concat(createChips(`${quantity}.${key}`, key, value, onDelete, locked, true))
-          if (index !== entries.length - 1) {
-            newChips.push(<FilterAnd key={`${quantity}-and`}/>)
-          }
-        })
-      }
-      addChipsForSection(filterValue, false)
-    // Regular non-nested filters
-    } else {
-      const onDelete = (newValue) => updateFilter([quantity, newValue])
-      const label = filterAbbreviations[quantity]
-      newChips = newChips.concat(createChips(quantity, label, filterValue, onDelete, false))
-    }
-
-    // Place the chips in a group
-    if (newChips.length > 0) {
-      const group = <FilterChipGroup
-        key={quantity}
-        quantity={quantity}
-      >{newChips}
-      </FilterChipGroup>
-      chips.push(group)
-    }
-  }
-
-  return chips.length !== 0 && <div className={clsx(className, styles.root)}>
-    {chips}
-  </div>
-})
-
-FilterSummary.propTypes = {
-  quantities: PropTypes.object, // Set of searchQuantities for which the filters are displayed
-  className: PropTypes.string,
-  classes: PropTypes.object
-}
-
-export default FilterSummary
diff --git a/gui/src/components/search/FilterTitle.js b/gui/src/components/search/FilterTitle.js
index 2fb098425a479aba8af3ef16038a6c43f495cf02..625a58af43503d569999c997b8b6078cc5e9c971 100644
--- a/gui/src/components/search/FilterTitle.js
+++ b/gui/src/components/search/FilterTitle.js
@@ -24,6 +24,7 @@ import { useSearchContext } from './SearchContext'
 import { inputSectionContext } from './input/InputSection'
 import { Unit } from '../units/Unit'
 import { useUnitContext } from '../units/UnitContext'
+import Ellipsis from '../visualization/Ellipsis'
 
 /**
  * Title for a metainfo quantity or section that is used in a search context.
@@ -94,8 +95,18 @@ const FilterTitle = React.memo(({
 
   // Determine the final description
   const finalDescription = description || filterData[quantity]?.description || ''
+  let tooltip = ''
+  if (finalDescription && quantity) {
+    tooltip = (
+      <>
+        <Typography>{finalLabel}</Typography>
+        <b>Description: </b>{finalDescription}<br/>
+        <b>Path: </b>{quantity}
+      </>
+    )
+  }
 
-  return <Tooltip title={finalDescription} placement="bottom" {...(TooltipProps || {})}>
+  return <Tooltip title={tooltip} interactive enterDelay={400} enterNextDelay={400} {...(TooltipProps || {})}>
     <div className={clsx(className, styles.root,
       rotation === 'right' && styles.right,
       rotation === 'down' && styles.down,
@@ -108,7 +119,7 @@ const FilterTitle = React.memo(({
         onMouseDown={onMouseDown}
         onMouseUp={onMouseUp}
       >
-        {finalLabel}
+        <Ellipsis>{finalLabel}</Ellipsis>
       </Typography>
     </div>
   </Tooltip>
@@ -127,6 +138,7 @@ FilterTitle.propTypes = {
   TooltipProps: PropTypes.object, // Properties forwarded to the Tooltip
   onMouseDown: PropTypes.func,
   onMouseUp: PropTypes.func,
+  placement: PropTypes.string,
   noWrap: PropTypes.bool
 }
 
diff --git a/gui/src/components/search/Query.js b/gui/src/components/search/Query.js
new file mode 100644
index 0000000000000000000000000000000000000000..2dc722de8dfde2856377eafcf09aadf03d209f6a
--- /dev/null
+++ b/gui/src/components/search/Query.js
@@ -0,0 +1,395 @@
+/*
+ * Copyright The NOMAD Authors.
+ *
+ * This file is part of NOMAD. See https://nomad-lab.eu for further info.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React, { useMemo } from 'react'
+import { makeStyles, useTheme } from '@material-ui/core/styles'
+import PropTypes from 'prop-types'
+import clsx from 'clsx'
+import { isNil, isEmpty, isPlainObject } from 'lodash'
+import { useSearchContext } from './SearchContext'
+import { useUnitContext } from '../units/UnitContext'
+import { Typography, Box, Chip } from '@material-ui/core'
+import FilterTitle from './FilterTitle'
+import Ellipsis from '../visualization/Ellipsis'
+import ClearIcon from '@material-ui/icons/Clear'
+import ReplayIcon from '@material-ui/icons/Replay'
+import LockIcon from '@material-ui/icons/Lock'
+import CodeIcon from '@material-ui/icons/Code'
+import { Actions, Action } from '../Actions'
+import { SourceApiCall, SourceApiDialogButton, SourceDialogDivider, SourceJsonCode } from '../buttons/SourceDialogButton'
+
+/**
+ * Thin wrapper for MUI Chip that is used for displaying (and possibly removing)
+ * query values.
+ */
+const useQueryChipStyles = makeStyles(theme => ({
+  root: {
+    maxWidth: '100%'
+  },
+  chipRoot: {
+    width: '100%',
+    maxWidth: '100%'
+  },
+  chipLabel: {
+    minWidth: '1rem',
+    maxWidth: '25rem'
+  }
+}))
+export const QueryChip = React.memo(({
+  label,
+  onDelete,
+  color,
+  className,
+  locked
+}) => {
+  const styles = useQueryChipStyles()
+
+  return <div className={clsx(className, styles.root)}>
+    <Chip
+      label={<Ellipsis tooltip={label}>{label}</Ellipsis>}
+      onDelete={locked ? undefined : onDelete}
+      color={locked ? undefined : color}
+      icon={locked ? <LockIcon/> : undefined}
+      classes={{root: styles.chipRoot, label: styles.chipLabel}}
+    />
+  </div>
+})
+
+QueryChip.propTypes = {
+  label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+  onDelete: PropTypes.func,
+  color: PropTypes.string,
+  className: PropTypes.string,
+  locked: PropTypes.bool
+}
+QueryChip.defaultProps = {
+  color: 'primary'
+}
+
+/**
+ * Used to group several related query chips inside one container.
+ */
+export const queryTitleHeight = 2.2
+export const queryGroupHeight = 4.1 + queryTitleHeight
+export const queryGroupSpacing = 0.5
+const useQueryChipGroupStyles = makeStyles(theme => ({
+  root: {
+    position: 'relative',
+    minHeight: theme.spacing(queryGroupHeight),
+    marginLeft: theme.spacing(queryGroupSpacing),
+    marginRight: theme.spacing(queryGroupSpacing)
+  },
+  chips: {
+    marginTop: theme.spacing(queryTitleHeight),
+    display: 'flex',
+    flexDirection: 'row',
+    flexWrap: 'wrap',
+    alignItems: 'center',
+    justifyContent: 'center',
+    backgroundColor: theme.palette.primary.main,
+    borderRadius: theme.spacing(2)
+  },
+  titleRoot: {
+    position: 'absolute',
+    left: theme.spacing(0.4),
+    right: 0,
+    top: 0,
+    height: theme.spacing(queryTitleHeight)
+  },
+  title: {
+    color: theme.palette.grey[600]
+  }
+}))
+export const QueryChipGroup = React.memo(({
+  quantity,
+  className,
+  children
+}) => {
+  const styles = useQueryChipGroupStyles()
+  return <div className={clsx(className, styles.root)}>
+    <FilterTitle
+      quantity={quantity}
+      variant="caption"
+      classes={{root: styles.titleRoot, text: styles.title}}
+      disableUnit
+    />
+    <div className={styles.chips}>
+      {children}
+    </div>
+  </div>
+})
+
+QueryChipGroup.propTypes = {
+  quantity: PropTypes.string,
+  color: PropTypes.string,
+  className: PropTypes.string,
+  children: PropTypes.node
+}
+QueryChipGroup.defaultProps = {
+  color: 'primary'
+}
+
+/**
+ * Operators between query chips.
+ */
+const useQueryOpStyles = makeStyles(theme => ({
+  root: {
+    fontSize: '0.6rem',
+    marginLeft: theme.spacing(0.2),
+    marginRight: theme.spacing(0),
+    color: 'white'
+  }
+}))
+export const QueryOp = React.memo(({className, children}) => {
+  const styles = useQueryOpStyles()
+  return <Typography variant="caption" className={clsx(className, styles.root)}>{children}</Typography>
+})
+
+QueryOp.propTypes = {
+  className: PropTypes.string,
+  children: PropTypes.node
+}
+
+export const QueryAnd = React.memo(() => {
+  return <QueryOp>AND</QueryOp>
+})
+
+export const QueryOr = React.memo(() => {
+  return <QueryOp>OR</QueryOp>
+})
+
+// Custom function for chip creation
+const createChips = (name, filterValue, onDelete, filterData, units) => {
+  if (isNil(filterValue)) return []
+
+  const { serializerPretty: serializer, customSerialization, queryMode } = filterData[name]
+  const isArray = Array.isArray(filterValue)
+  const isSet = filterValue instanceof Set
+  const isObj = isPlainObject(filterValue)
+  const op = queryMode === "any" ? <QueryOr/> : <QueryAnd/>
+  const chips = []
+
+  const createChip = (label, onDelete, single = false) => (
+    <QueryChip key={label} label={label} onDelete={onDelete} single={single}/>
+  )
+
+  if (customSerialization) {
+    chips.push({ comp: createChip(serializer(filterValue), () => onDelete(undefined)), op })
+  } else if (isArray || isSet) {
+    Array.from(filterValue).forEach((value, index) => {
+      chips.push({
+        comp: createChip(serializer(value, units), () => {
+          const newValue = isSet ? new Set(filterValue) : [...filterValue]
+          isSet ? newValue.delete(value) : newValue.splice(index, 1)
+          onDelete(newValue)
+        }),
+        op
+      })
+    })
+  } else if (isObj) {
+    const createRangeChip = (label, comparison) => {
+      const content = `${comparison} ${serializer(filterValue[label], units)}`
+      if (!isNil(filterValue[label])) {
+        let newValue = {...filterValue}
+        delete newValue[label]
+        newValue = isEmpty(newValue) ? undefined : newValue
+        chips.push({ comp: createChip(content, () => onDelete(newValue)), op })
+      }
+    }
+
+    createRangeChip('lte', '<=')
+    createRangeChip('lt', '<')
+    createRangeChip('gte', '>=')
+    createRangeChip('gt', '>')
+  } else {
+    chips.push({ comp: createChip(serializer(filterValue), () => onDelete(undefined)), op })
+  }
+
+  return chips.length ? (
+    <QueryChipGroup key={name} quantity={name}>
+      {chips.map((chip, index) => (
+        <React.Fragment key={index}>
+          {chip.comp}
+          {index < chips.length - 1 && chip.op}
+        </React.Fragment>
+      ))}
+    </QueryChipGroup>
+  ) : null
+}
+
+/*
+ * Displays chips for the current query.
+ */
+const useStyles = makeStyles(theme => ({
+  root: {
+    width: '100%',
+    boxSizing: 'border-box',
+    display: 'flex',
+    flexDirection: 'row',
+    flexWrap: 'wrap',
+    alignItems: 'flex-end',
+    marginLeft: theme.spacing(-queryGroupSpacing),
+    marginRight: theme.spacing(-queryGroupSpacing)
+  },
+  empty: {
+    marginTop: theme.spacing(1.8)
+  },
+  chip: {
+    padding: theme.spacing(0.5)
+  }
+}))
+const QueryChips = React.memo(({ className, classes }) => {
+  const { filterData, useQuery, useUpdateFilter } = useSearchContext()
+  const query = useQuery()
+  const updateFilter = useUpdateFilter()
+  const theme = useTheme()
+  const { units } = useUnitContext()
+  const styles = useStyles({ classes, theme })
+
+  const chips = useMemo(() => {
+    const chips = []
+    // The query chips are created in alphabetical order
+    const keys = Object.keys(query)
+    keys.sort()
+    for (const quantity of keys) {
+      const filterValue = query[quantity]
+      // Each key in a section is mapped into a group
+      const isSection = filterData[quantity].section
+      if (isSection) {
+        const addChipsForSection = (data) => {
+          const newChips = []
+          Object.entries(data).forEach(([key, value], index) => {
+            // Empty filters are skipped
+            if (isEmpty(value)) return
+            const onDelete = (newValue) => {
+              const newSection = { ...data, [key]: newValue }
+              if (newValue === undefined) delete newSection[key]
+              updateFilter([quantity, newSection])
+            }
+            newChips.push(
+              <React.Fragment key={`${quantity}.${key}`}>
+                {createChips(`${quantity}.${key}`, value, onDelete, filterData, units)}
+                {index < Object.entries(data).length - 1 && <QueryAnd/>}
+              </React.Fragment>
+            )
+          })
+          return newChips
+        }
+        const newChips = addChipsForSection(filterValue)
+        newChips.length && chips.push(newChips)
+      // Regular chips get their own group
+      } else {
+        const onDelete = (newValue) => updateFilter([quantity, newValue])
+        chips.push(createChips(quantity, filterValue, onDelete, filterData, units))
+      }
+    }
+
+    return chips
+  }, [query, filterData, units, updateFilter])
+
+  return (
+    <div className={clsx(className, styles.root)}>
+      {chips.length
+        ? chips
+        : <Typography className={styles.empty}><i>Your query will be shown here</i></Typography>
+      }
+    </div>
+  )
+})
+
+QueryChips.propTypes = {
+  className: PropTypes.string,
+  classes: PropTypes.object
+}
+
+const useQueryStyles = makeStyles(theme => ({
+  root: {
+    minHeight: theme.spacing(queryGroupHeight),
+    margin: theme.spacing(1, 0.25),
+    width: '100%',
+    display: 'flex',
+    flexDirection: 'row',
+    alignItems: 'flex-start'
+  },
+  offset: {
+    height: theme.spacing(queryGroupHeight),
+    display: 'flex',
+    flexDirection: 'row',
+    alignItems: 'center',
+    width: 'unset'
+  }
+}))
+
+/**
+ * Displays the current query and actions for it.
+ */
+const Query = React.memo(() => {
+  const {useResetFilters, useRefresh, useApiData} = useSearchContext()
+  const styles = useQueryStyles()
+  const resetFilters = useResetFilters()
+  const refresh = useRefresh()
+  const apiData = useApiData()
+
+  return <Box className={styles.root}>
+    <QueryChips/>
+    <Actions className={styles.offset}>
+      <Action
+        tooltip="Clear query"
+        onClick={() => resetFilters()}
+      >
+        <ClearIcon fontSize="small"/>
+      </Action>
+      <Action
+        tooltip="Refresh results"
+        onClick={() => refresh()}
+      >
+        <ReplayIcon fontSize="small"/>
+      </Action>
+      <Action
+        tooltip=""
+        ButtonComponent={SourceApiDialogButton}
+        ButtonProps={{
+          tooltip: "View API call for the query",
+          maxWidth: "lg",
+          fullWidth: true,
+          icon: <CodeIcon fontSize="small"/>,
+          buttonProps: {
+            size: "small"
+          }
+        }}
+      >
+        <Typography>
+          NOMAD uses the same query format throughout its API. This is the query
+          based on the current filters:
+        </Typography>
+        <SourceJsonCode data={{owner: apiData?.body?.owner, query: apiData?.body?.query}}/>
+        <SourceDialogDivider/>
+        <Typography>
+          One application of the above query is this API call. This is what is currently
+          used to render this page and includes all displayed statistics data
+          (aggregations).
+        </Typography>
+        <SourceApiCall
+          {...apiData}
+        />
+      </Action>
+    </Actions>
+  </Box>
+})
+Query.propTypes = {}
+
+export default Query
diff --git a/gui/src/components/search/FilterSummary.spec.js b/gui/src/components/search/Query.spec.js
similarity index 84%
rename from gui/src/components/search/FilterSummary.spec.js
rename to gui/src/components/search/Query.spec.js
index db890b76e05e7b2f140b56f87e8f9a286759396f..f8c5f8d0b3d57d71b036057fe7d977ae7c20fb8a 100644
--- a/gui/src/components/search/FilterSummary.spec.js
+++ b/gui/src/components/search/Query.spec.js
@@ -18,14 +18,14 @@
 import React from 'react'
 import { ui } from '../../config'
 import { render, screen } from '../conftest.spec'
-import FilterSummary from './FilterSummary'
+import QueryChips from './Query'
 import { SearchContext } from './SearchContext'
 
 test.each([
-  ['integer', 'results.material.n_elements', 12, 'N elements', 'n_elements=12'],
+  ['integer', 'results.material.n_elements', 12, 'N elements', '12'],
   ['string', 'results.material.symmetry.crystal_system', 'cubic', 'Crystal system', 'cubic'],
-  ['float', 'results.method.simulation.precision.k_line_density', 12.3, 'k-line density (Ã…)', 'k_line_density=12.3'],
-  ['datetime', 'upload_create_time', 0, 'Upload create time', 'upload_create_time=01/01/1970'],
+  ['float', 'results.properties.electronic.band_gap.value', '12.3 eV', 'Value', '12.3 eV'],
+  ['datetime', 'upload_create_time', 0, 'Upload create time', '01/01/1970'],
   ['boolean', 'results.properties.electronic.dos_electronic.spin_polarized', 'false', 'Spin-polarized', 'false']
 ])('%s', async (name, quantity, input, title, output) => {
   const context = ui.apps.options.entries
@@ -41,7 +41,7 @@ test.each([
         initialFilterValues={{[quantity]: input}}
         initialSearchSyntaxes={context?.search_syntaxes}
     >
-      <FilterSummary quantities={new Set([quantity])}/>
+      <QueryChips/>
     </SearchContext>
   )
   expect(screen.getByText(title, {exact: false})).toBeInTheDocument()
diff --git a/gui/src/components/search/SearchBar.js b/gui/src/components/search/SearchBar.js
index 221a36a7757bfb83297b9574752fd55c2f50cf47..4437ce145d48af9ee75a5e2a6d05132bb3f0a622 100644
--- a/gui/src/components/search/SearchBar.js
+++ b/gui/src/components/search/SearchBar.js
@@ -1,3 +1,4 @@
+/* eslint-disable no-unused-vars */
 /*
  * Copyright The NOMAD Authors.
  *
@@ -25,7 +26,7 @@ import SearchIcon from '@material-ui/icons/Search'
 import HistoryIcon from '@material-ui/icons/History'
 import CloseIcon from '@material-ui/icons/Close'
 import { HelpButton } from '../../components/Help'
-import { Paper, Tooltip, Chip, List, ListSubheader, Typography, IconButton } from '@material-ui/core'
+import { Paper, Tooltip, Chip, List, ListSubheader, Typography, IconButton, Box } from '@material-ui/core'
 import { parseQuantityName, getSchemaAbbreviation } from '../../utils'
 import { useSuggestions } from '../../hooks'
 import { useSearchContext } from './SearchContext'
@@ -131,11 +132,29 @@ Suggestion.propTypes = {
   tooltip: PropTypes.string
 }
 
+const queryControlsHeight = 5.5
 export const useStyles = makeStyles(theme => ({
   root: {
+    width: '100%',
     display: 'flex',
-    alignItems: 'center',
-    position: 'relative'
+    flexDirection: 'column'
+  },
+  paper: {
+    width: '100%'
+  },
+  offset: {
+    height: theme.spacing(queryControlsHeight),
+    display: 'flex',
+    flexDirection: 'row',
+    alignItems: 'center'
+  },
+  queryContainer: {
+    minHeight: theme.spacing(queryControlsHeight),
+    marginTop: theme.spacing(0.5),
+    width: '100%',
+    display: 'flex',
+    flexDirection: 'row',
+    alignItems: 'flex-start'
   },
   iconButton: {
     padding: 10
@@ -160,8 +179,12 @@ const SearchBar = React.memo(({
     usePushSearchSuggestion,
     useRemoveSearchSuggestion,
     useSetPagination,
-    searchSyntaxes
+    searchSyntaxes,
+    useResetFilters,
+    useRefresh,
+    useApiData
   } = useSearchContext()
+
   const includedFormats = Object
     .keys(SearchSyntaxes)
     .filter((key) => !searchSyntaxes?.exclude?.includes(key))
@@ -410,54 +433,56 @@ const SearchBar = React.memo(({
     setSuggestionInput(input)
   }, [quantitiesSuggestable, filterFullnames, getSuggestionsMatch, clearSuggestions])
 
-  return <Paper className={clsx(className, styles.root)}>
-    <InputText
-      value={inputValue || null}
-      onChange={handleInputChange}
-      onSelect={handleAccept}
-      onAccept={handleAccept}
-      onHighlight={handleHighlight}
-      suggestions={keys}
-      disableAcceptOnBlur
-      autoHighlight={inputValue?.trim() === suggestions[keys[0]]?.input?.trim()}
-      ListboxComponent={ListboxSuggestion}
-      TextFieldProps={{
-        variant: 'outlined',
-        placeholder: 'Type your query or keyword here',
-        label: error || undefined,
-        error: !!error,
-        InputLabelProps: { shrink: true },
-        size: "medium"
-      }}
-      InputProps={{
-        startAdornment: <SearchIcon className={styles.iconButton} color="action" />,
-        endAdornment: <Tooltip title="Search bar syntax help">
-          <HelpButton
-            IconProps={{fontSize: 'small'}}
-            maxWidth="md"
-            size="small"
-            heading="Search bar help"
-            text={`
-The search bar provides a fast way to start formulating queries.
-Once you start typing a keyword or a query, suggestions for queries
-and metainfo names are given based on your search history and the
-available data. This search bar supports the following syntaxes:
-
-${formatReadmeList}`}
-          />
-        </Tooltip>,
-        inputRef: inputRef
-      }}
-      getOptionLabel={option => option}
-      filterOptions={(options) => options}
-      loading={loading}
-      renderOption={(id) => <Suggestion
-        suggestion={suggestions[id]}
-        onDelete={() => removeSuggestion(suggestions[id].key)}
-        tooltip={suggestions[id].type === SuggestionType.Name ? filterData[suggestions[id].input]?.description : undefined}
-      />}
-    />
-  </Paper>
+  return <Box className={clsx(className, styles.root)}>
+    <Paper className={styles.paper}>
+      <InputText
+        value={inputValue || null}
+        onChange={handleInputChange}
+        onSelect={handleAccept}
+        onAccept={handleAccept}
+        onHighlight={handleHighlight}
+        suggestions={keys}
+        disableAcceptOnBlur
+        autoHighlight={inputValue?.trim() === suggestions[keys[0]]?.input?.trim()}
+        ListboxComponent={ListboxSuggestion}
+        TextFieldProps={{
+          variant: 'outlined',
+          placeholder: 'Type your query or keyword here',
+          label: error || undefined,
+          error: !!error,
+          InputLabelProps: { shrink: true },
+          size: "medium"
+        }}
+        InputProps={{
+          startAdornment: <SearchIcon className={styles.iconButton} color="action" />,
+          endAdornment: <Tooltip title="Search bar syntax help">
+            <HelpButton
+              IconProps={{fontSize: 'small'}}
+              maxWidth="md"
+              size="small"
+              heading="Search bar help"
+              text={`
+  The search bar provides a fast way to start formulating queries.
+  Once you start typing a keyword or a query, suggestions for queries
+  and metainfo names are given based on your search history and the
+  available data. This search bar supports the following syntaxes:
+
+  ${formatReadmeList}`}
+            />
+          </Tooltip>,
+          inputRef: inputRef
+        }}
+        getOptionLabel={option => option}
+        filterOptions={(options) => options}
+        loading={loading}
+        renderOption={(id) => <Suggestion
+          suggestion={suggestions[id]}
+          onDelete={() => removeSuggestion(suggestions[id].key)}
+          tooltip={suggestions[id].type === SuggestionType.Name ? filterData[suggestions[id].input]?.description : undefined}
+        />}
+      />
+    </Paper>
+  </Box>
 })
 
 SearchBar.propTypes = {
diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js
index b6c87d255f16b72071fb4e5a604b5d8c36bce7d4..ebcdc52944785aaea22ca82a516ff1e48e9e8113 100644
--- a/gui/src/components/search/SearchContext.js
+++ b/gui/src/components/search/SearchContext.js
@@ -37,6 +37,7 @@ import {
   isPlainObject,
   isNil,
   isSet,
+  isObject,
   isFunction,
   size,
   cloneDeep
@@ -616,7 +617,7 @@ export const SearchContextRaw = React.memo(({
         const query = {}
         for (const key of get(filterNamesState)) {
           const filter = get(queryFamily(key))
-          if (filter !== undefined) {
+          if (!isNil(filter) && (!isObject(filter) || !isEmpty(filter))) {
             query[key] = filter
           }
         }
diff --git a/gui/src/components/search/SearchPage.js b/gui/src/components/search/SearchPage.js
index 4845cb1cf41ec2968da080bd79bca6a7674e13f4..6d366f15af8185b0fbbe267f80a8c321f7488076 100644
--- a/gui/src/components/search/SearchPage.js
+++ b/gui/src/components/search/SearchPage.js
@@ -23,6 +23,7 @@ import { makeStyles } from '@material-ui/core/styles'
 import FilterMainMenu from './menus/FilterMainMenu'
 import { collapsedMenuWidth } from './menus/FilterMenu'
 import SearchBar from './SearchBar'
+import Query from './Query.js'
 import { SearchResultsWithContext } from './SearchResults'
 import Dashboard from './widgets/Dashboard'
 import { useSearchContext } from './SearchContext'
@@ -52,12 +53,13 @@ const useStyles = makeStyles(theme => {
     center: {
       flexGrow: 1,
       height: '100%',
-      overflow: 'auto'
+      overflowY: 'scroll'
     },
     searchBar: {
       display: 'flex',
       flexGrow: 0,
-      zIndex: 1
+      zIndex: 1,
+      marginBottom: theme.spacing(0.3)
     },
     shadow: {
       pointerEvents: 'none',
@@ -103,12 +105,13 @@ const SearchPage = React.memo(({
       />
     </div>
     <div className={styles.center} onClick={() => setIsMenuOpen(false)}>
-      <Box margin={3} paddingBottom={3}>
+      <Box margin={2.5} paddingBottom={3}>
         <Box marginBottom={2}>
           {header}
         </Box>
-        <Box marginBottom={1}>
+        <Box marginBottom={0}>
           <SearchBar className={styles.searchBar} />
+          <Query/>
         </Box>
         <Box marginBottom={1} zIndex={0}>
           <Dashboard/>
diff --git a/gui/src/components/search/conftest.spec.js b/gui/src/components/search/conftest.spec.js
index 2052e282981591deee38acb6a5071cb989dfb13c..1e1fabe57952918207b128a3eba16e76e66d8b99 100644
--- a/gui/src/components/search/conftest.spec.js
+++ b/gui/src/components/search/conftest.spec.js
@@ -19,7 +19,7 @@
 import React from 'react'
 import PropTypes from 'prop-types'
 import assert from 'assert'
-import { within, waitFor } from '@testing-library/dom'
+import { within, waitFor, waitForElementToBeRemoved } from '@testing-library/dom'
 import elementData from '../../elementData.json'
 import { screen, WrapperDefault } from '../conftest.spec'
 import { render } from '@testing-library/react'
@@ -68,15 +68,29 @@ export const renderSearchEntry = (ui, options) =>
 export async function expectFilterTitle(quantity, label, description, unit, disableUnit, root = screen) {
   const data = defaultFilterData[quantity]
   let finalLabel = label || data?.label
-  const finalDescription = description || data?.description
   if (!disableUnit) {
     const finalUnit = unit || (
       data?.unit && new Unit(data?.unit).toSystem(ui.unit_systems.options.Custom.units).label()
     )
     if (finalUnit) finalLabel = `${finalLabel} (${finalUnit})`
   }
-  await root.findAllByText(finalLabel)
-  expect(root.getAllByTooltip(finalDescription)[0]).toBeInTheDocument()
+  const labelElement = root.getAllByText(finalLabel)[0]
+
+  // Test that the tooltip appears after hover. The tooltip is only shown if the
+  // quantity is defined.
+  if (quantity) {
+    const finalDescription = description || data?.description
+    const options = {
+      name: new RegExp(String.raw`${finalDescription.substring(0, 20)}`)
+    }
+
+    await userEvent.hover(labelElement)
+    await waitFor(() => screen.getByRole('tooltip', options))
+
+    // We need to unhover and wait until tooltip disappears to not disturb other tests.
+    await userEvent.unhover(labelElement)
+    await waitForElementToBeRemoved(() => screen.getByRole('tooltip', options))
+  }
 }
 
 /**
diff --git a/gui/src/components/search/menus/FilterMenu.js b/gui/src/components/search/menus/FilterMenu.js
index bfb91ad9da3ea75218cf4a658d473164066eb72a..a53dd697f1f4021606eccc7d4d72d4925214b5ea 100644
--- a/gui/src/components/search/menus/FilterMenu.js
+++ b/gui/src/components/search/menus/FilterMenu.js
@@ -32,18 +32,13 @@ import {
 import ArrowForwardIcon from '@material-ui/icons/ArrowForward'
 import ArrowBackIcon from '@material-ui/icons/ArrowBack'
 import NavigateNextIcon from '@material-ui/icons/NavigateNext'
-import ClearIcon from '@material-ui/icons/Clear'
-import ReplayIcon from '@material-ui/icons/Replay'
 import MoreVert from '@material-ui/icons/MoreVert'
-import CodeIcon from '@material-ui/icons/Code'
 import Scrollable from '../../visualization/Scrollable'
-import FilterSummary from '../FilterSummary'
 import FilterSettings from './FilterSettings'
 import { Actions, ActionHeader, Action } from '../../Actions'
 import { useSearchContext } from '../SearchContext'
 import { pluralize } from '../../../utils'
 import { isNil } from 'lodash'
-import { SourceApiCall, SourceApiDialogButton, SourceDialogDivider, SourceJsonCode } from '../../buttons/SourceDialogButton'
 
 // The menu animations use a transition on the 'transform' property. Notice that
 // animating 'transform' instead of e.g. the 'left' property is much more
@@ -287,14 +282,11 @@ export const FilterMenuItems = React.memo(({
   className,
   children
 }) => {
-  const { useResetFilters, useRefresh, useApiData } = useSearchContext()
+  // const { useResetFilters, useRefresh, useApiData } = useSearchContext()
   const styles = useFilterMenuItemsStyles()
   const { open, onOpenChange, collapsed, onCollapsedChange } = useContext(filterMenuContext)
   const [anchorEl, setAnchorEl] = React.useState(null)
   const isSettingsOpen = Boolean(anchorEl)
-  const resetFilters = useResetFilters()
-  const refresh = useRefresh()
-  const apiData = useApiData()
 
   // Callbacks
   const openMenu = useCallback((event) => {
@@ -316,46 +308,6 @@ export const FilterMenuItems = React.memo(({
         <FilterMenuHeader
           title="Filters"
           actions={<>
-            <Action
-              tooltip="Refresh results"
-              onClick={() => refresh()}
-            >
-              <ReplayIcon fontSize="small"/>
-            </Action>
-            <Action
-              tooltip="Clear filters"
-              onClick={() => resetFilters()}
-            >
-              <ClearIcon fontSize="small"/>
-            </Action>
-            <Action
-              tooltip=""
-              ButtonComponent={SourceApiDialogButton}
-              ButtonProps={{
-                tooltip: "API",
-                maxWidth: "lg",
-                fullWidth: true,
-                icon: <CodeIcon fontSize="small"/>,
-                ButtonProps: {
-                  size: "small"
-                }
-              }}
-            >
-              <Typography>
-                NOMAD uses the same query format throughout its API. This is the query
-                based on the current filters:
-              </Typography>
-              <SourceJsonCode data={{owner: apiData?.body?.owner, query: apiData?.body?.query}}/>
-              <SourceDialogDivider/>
-              <Typography>
-                One application of the above query is this API call. This is what is currently
-                used to render this page and includes all displayed statistics data
-                (aggregations).
-              </Typography>
-              <SourceApiCall
-                {...apiData}
-              />
-            </Action>
             <Action
               tooltip={'Hide filter menu'}
               onClick={() => {
@@ -467,7 +419,6 @@ const useFilterMenuItemStyles = makeStyles(theme => {
 export const FilterMenuItem = React.memo(({
   id,
   label,
-  group,
   onClick,
   actions,
   disableButton,
@@ -475,8 +426,6 @@ export const FilterMenuItem = React.memo(({
 }) => {
   const styles = useFilterMenuItemStyles()
   const theme = useTheme()
-  const {filterGroups} = useSearchContext()
-  const groupFinal = group || filterGroups[id]
   const { selected, open, onChange } = useContext(filterMenuContext)
   const handleClick = disableButton ? undefined : (onClick || onChange)
   const opened = open && id === selected
@@ -508,7 +457,6 @@ export const FilterMenuItem = React.memo(({
     >
       {actions}
     </div>}
-    {groupFinal && <FilterSummary quantities={groupFinal}/>}
     <Divider className={styles.divider}/>
   </div>
 })
@@ -516,7 +464,6 @@ export const FilterMenuItem = React.memo(({
 FilterMenuItem.propTypes = {
   id: PropTypes.string,
   label: PropTypes.string,
-  group: PropTypes.string,
   onClick: PropTypes.func,
   actions: PropTypes.node,
   disableButton: PropTypes.bool,
diff --git a/gui/src/components/search/widgets/Dashboard.spec.js b/gui/src/components/search/widgets/Dashboard.spec.js
index 80078fb1d8f266b70aa74669faf4bd7e007ac734..51e32cdec9a73a3b4d983396c1c11df2213f9d67 100644
--- a/gui/src/components/search/widgets/Dashboard.spec.js
+++ b/gui/src/components/search/widgets/Dashboard.spec.js
@@ -89,7 +89,6 @@ describe('displaying an initial widget and removing it', () => {
       {
         type: 'scatterplot',
         title: 'Test title',
-        description: 'Custom scatter plot',
         x: {quantity: 'results.properties.optoelectronic.solar_cell.open_circuit_voltage'},
         y: {quantity: 'results.properties.optoelectronic.solar_cell.efficiency'},
         markers: {color: {quantity: 'results.properties.optoelectronic.solar_cell.short_circuit_current_density'}},
diff --git a/gui/src/components/search/widgets/WidgetScatterPlot.js b/gui/src/components/search/widgets/WidgetScatterPlot.js
index 727245e9e8b25ca253ef2730611a54a56b42c283..84efcd71be70b6d08dede7e870aed3f0507e8a5f 100644
--- a/gui/src/components/search/widgets/WidgetScatterPlot.js
+++ b/gui/src/components/search/widgets/WidgetScatterPlot.js
@@ -377,7 +377,7 @@ export const WidgetScatterPlot = React.memo((
     <Widget
       id={id}
       title={title || "Scatter plot"}
-      description={description || 'Custom scatter plot'}
+      description={description}
       onEdit={handleEdit}
       actions={actions}
       className={styles.widget}
diff --git a/gui/src/components/visualization/Ellipsis.js b/gui/src/components/visualization/Ellipsis.js
index c7c4eca49ab71b2eaeac55d7e45ff46fcbc86102..806c0b75aa481c21fd4f748a0312afc26ec9c2b7 100644
--- a/gui/src/components/visualization/Ellipsis.js
+++ b/gui/src/components/visualization/Ellipsis.js
@@ -15,14 +15,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import React from 'react'
+import React, { useEffect, useState, useCallback } from 'react'
 import PropTypes from 'prop-types'
 import clsx from 'clsx'
-import { makeStyles } from '@material-ui/core'
+import { makeStyles, Tooltip } from '@material-ui/core'
+import { useResizeDetector } from 'react-resize-detector'
 
 /**
  * Component for displaying text that should be truncated with an Ellipsis
- * either in front of the text or after the text.
+ * either in front of the text or after the text. Can lso show the full text on
+ * hover as a tooltip.
  */
 const useStyles = makeStyles(theme => ({
   root: {
@@ -42,14 +44,39 @@ const useStyles = makeStyles(theme => ({
   }
 }))
 
-const Ellipsis = React.memo(({front, children}) => {
+const Ellipsis = React.memo(({front, children, tooltip}) => {
   const styles = useStyles()
-  return <span className={clsx(styles.root, front && styles.ellipsisFront)}>{children}</span>
+  const {width, ref} = useResizeDetector()
+  const [showTooltip, setShowTooltip] = useState(false)
+
+  // Used to determine whether to show the tooltip or not
+  const compareSize = useCallback(() => {
+    if (!ref?.current) return
+    const compare =
+      ref.current.scrollWidth > ref.current.clientWidth
+    setShowTooltip(compare)
+  }, [ref])
+
+  // Whenever the component width changes, check if tooltip should be shown
+  useEffect(() => { compareSize() }, [width, compareSize])
+
+  // Define state and function to update the value
+  return <Tooltip
+      title={showTooltip ? tooltip : ''}
+      disableHoverListener={!showTooltip}
+    >
+    <span ref={ref} className={clsx(styles.root, front && styles.ellipsisFront)}>{children}</span>
+  </Tooltip>
 })
 
 Ellipsis.propTypes = {
   children: PropTypes.node,
+  tooltip: PropTypes.string,
   front: PropTypes.bool
 }
 
+Ellipsis.defaultProps = {
+  tooltip: ''
+}
+
 export default Ellipsis