diff --git a/docs/develop/setup.md b/docs/develop/setup.md
index 4fb94c3b8f64e711067db56369b5d8bda1895d35..2f37234b5730294b03d789fa13bffd7ea4dc8596 100644
--- a/docs/develop/setup.md
+++ b/docs/develop/setup.md
@@ -385,25 +385,15 @@ pytest -sv tests
 ```
 
 !!! note
-    Some of these tests will fail because a few large files are not included in the Git
-    repository. You may ignore these for local testing, they are still checked by the
-    CI/CD pipeline:
-
-    ```text
-    FAILED tests/archive/test_archive.py::test_read_springer - AttributeError: 'NoneType' object has no attribute 'seek'
-    FAILED tests/normalizing/test_material.py::test_material_bulk - assert None
-    FAILED tests/normalizing/test_system.py::test_springer_normalizer - IndexError: list index out of range
-    ```
-
     If you excluded plugins in your [NOMAD config](### `nomad.yaml`), then those tests
-    will also fail.
+    will fail.
 
 We use Ruff and Mypy to maintain code quality. Additionally, we recommend installing the Ruff [plugins](https://docs.astral.sh/ruff/integrations/) for your code editor to streamline the process. To execute Ruff and Mypy from the command line, you can utilize the following command:
+
 ```shell
 nomad dev qa --skip-tests
 ```
 
-
 To run all tests and code QA:
 
 ```shell
diff --git a/examples/data/docs/tabular-parser_3_row_current-entry_to-path.archive.yaml b/examples/data/docs/tabular-parser_3_row_current-entry_to-path.archive.yaml
index 6fe345133787b42a5a262dd9fedfcdaca38c9531..db3cc7eebd1b85cd4c85313a6963a3324a2baa72 100644
--- a/examples/data/docs/tabular-parser_3_row_current-entry_to-path.archive.yaml
+++ b/examples/data/docs/tabular-parser_3_row_current-entry_to-path.archive.yaml
@@ -32,7 +32,7 @@ definitions:
       m_annotations:
         eln:
       more:
-        label_quantity: my_quantity_1
+        label_quantity: '#/data/my_quantity_1'
       quantities:
         my_quantity_1:
           type: str
diff --git a/examples/data/docs/tabular-parser_5_row_single-new-entry_to-path.archive.yaml b/examples/data/docs/tabular-parser_5_row_single-new-entry_to-path.archive.yaml
index bd5f910dd3d5be679c8e22ba8a83eb2095eff8ad..b005f6854fd4d0e12e545906111b778f986965ba 100644
--- a/examples/data/docs/tabular-parser_5_row_single-new-entry_to-path.archive.yaml
+++ b/examples/data/docs/tabular-parser_5_row_single-new-entry_to-path.archive.yaml
@@ -41,7 +41,7 @@ definitions:
       m_annotations:
         eln:
       more:
-        label_quantity: my_quantity_1
+        label_quantity: '#/data/my_quantity_1'
       sub_sections:
         my_repeated_sub_section:
           repeats: true
diff --git a/examples/data/docs/tabular-parser_6_row_multiple-new-entries_to-root.archive.yaml b/examples/data/docs/tabular-parser_6_row_multiple-new-entries_to-root.archive.yaml
index d90e5a3910383e4579047a59e3443a36bbec429e..5d71172d680084b6815c685bc79742a7d4c67992 100644
--- a/examples/data/docs/tabular-parser_6_row_multiple-new-entries_to-root.archive.yaml
+++ b/examples/data/docs/tabular-parser_6_row_multiple-new-entries_to-root.archive.yaml
@@ -8,7 +8,7 @@ definitions:
       m_annotations:
         eln:
       more:
-        label_quantity: my_quantity_1
+        label_quantity: '#/data/my_quantity_1'
       quantities:
         data_file:
           type: str
diff --git a/examples/data/docs/tabular-parser_7_row_multiple-new-entries_to-path.archive.yaml b/examples/data/docs/tabular-parser_7_row_multiple-new-entries_to-path.archive.yaml
index 4fa7ac96f330584dc9754ef31c19ce5123e5416c..430da805977ab0fdbde1af71871950c88fd6ec0d 100644
--- a/examples/data/docs/tabular-parser_7_row_multiple-new-entries_to-path.archive.yaml
+++ b/examples/data/docs/tabular-parser_7_row_multiple-new-entries_to-path.archive.yaml
@@ -42,7 +42,7 @@ definitions:
       m_annotations:
         eln:
       more:
-        label_quantity: my_quantity_1
+        label_quantity: '#/data/my_quantity_1'
       quantities:
         my_quantity_1:
           type: str
diff --git a/examples/data/docs/tabular-parser_8_row_current-entry_to-path_subsubsection.archive.yaml b/examples/data/docs/tabular-parser_8_row_current-entry_to-path_subsubsection.archive.yaml
index 23b7b67891f593a833bdcbb311e6c3e5fee0971f..8ed86ab3e4cc2d26e455281f96116fb3231b9286 100644
--- a/examples/data/docs/tabular-parser_8_row_current-entry_to-path_subsubsection.archive.yaml
+++ b/examples/data/docs/tabular-parser_8_row_current-entry_to-path_subsubsection.archive.yaml
@@ -32,7 +32,7 @@ definitions:
       m_annotations:
         eln:
       more:
-        label_quantity: my_quantity_1
+        label_quantity: '#/data/my_quantity_1'
       quantities:
         my_quantity_1:
           type: str
diff --git a/examples/data/tabular/README.md b/examples/data/tabular/README.md
index 283c55d49149753b37d57563c3e22dc2f0bd91ca..6fa314b49611dc36a9be22d6eb60da8e0b313455 100644
--- a/examples/data/tabular/README.md
+++ b/examples/data/tabular/README.md
@@ -1,6 +1,6 @@
-This upload demonstrates the used of tabular data. In this example we use an *xlsx* file in combination with a custom schema. The schema describes what the columns in the excel file mean and NOMAD can parse everything accordingly to produce a **FAIR** dataset.
+This upload demonstrates the use of tabular data. In this example we use an *xlsx* file in combination with a custom schema. The schema describes what columns in the excel file mean and how NOMAD is expected to parse and map the content accordingly in order to produce a **FAIR** dataset.
 
-The schema is meant as a starting point. You can download the schema file and
+This schema is meant as a starting point. You can download the schema file and
 extend the schema for your own tables.
 
-Consult our [documentation on the NOMAD Archive and Metainfo](https://nomad-lab.eu/prod/v1/docs/archive.html) to learn more about schemas.
+Consult our [documentation on the NOMAD Archive and Metainfo](https://nomad-lab.eu/prod/v1/staging/docs/) to learn more about schemas.
diff --git a/examples/data/tabular/periodic-table.archive.xlsx b/examples/data/tabular/data.xlsx
similarity index 100%
rename from examples/data/tabular/periodic-table.archive.xlsx
rename to examples/data/tabular/data.xlsx
diff --git a/examples/data/tabular/periodic-table.archive.yaml b/examples/data/tabular/periodic-table.archive.yaml
index 736b2f78a3ba66230ed2c2e79ecd2de333d23d6f..926e605fc97e4c77497a442ef6b50d8c718c7f33 100644
--- a/examples/data/tabular/periodic-table.archive.yaml
+++ b/examples/data/tabular/periodic-table.archive.yaml
@@ -4,20 +4,37 @@ definitions:
   name: Periodic Table
   sections:
     Element:
+      more:
+        label_quantity: '#/data/name'
       base_sections:
-        # We use ElnBaseSection here. This provides a few quantities (name, description, tags)
+        # We use ElnBaseSection here. This provides a few quantities (name, ags)description, t
         # that are added to the search index. If we map table columns to these quantities,
         # we can make those cells available for search.
         - nomad.datamodel.metainfo.eln.ElnBaseSection
         # Schemas that are used to directly parse table files (.csv, .xlsx), need to
-        # have the first definition to extend nomad.parsing.tabular.TableRow.
-        - nomad.parsing.tabular.TableRow
+        # have the first definition to extend nomad.parsing.tabular.TableData.
+        - nomad.parsing.tabular.TableData
       m_annotations:
         # We might not want to show all ElnBaseSection quantities.
         eln:
           hide:
             - lab_id
       quantities:
+        # data_file contains the information on how to parse the excel/csv file. Here we want to create
+        # as many entries as there are rows in the excel file and map the quantities annotated with 'tabular' from
+        # the tabular data into the nomad schema of each entry.
+        data_file:
+          type: str
+          default: data.xlsx
+          m_annotations:
+            tabular_parser:
+              parsing_options:
+                comment: '#'
+              mapping_options:
+                - mapping_mode: row
+                  file_mode: multiple_new_entries
+                  sections:
+                    - '#root'
         # Tags will be picked up by ElnBaseSection and put into search. We do not really
         # use this to edit the tags, but we define a default that is then add to
         # all row data.
diff --git a/gui/package.json b/gui/package.json
index 5665c3dcc01630c4b3e016b2a62c312b6b71f41b..6504496fa555ef380e75f091cb86d1b0d4b4d521 100644
--- a/gui/package.json
+++ b/gui/package.json
@@ -17,6 +17,7 @@
     "@material-ui/pickers": "^3.3.10",
     "@navjobs/upload": "^3.2.0",
     "@react-keycloak/web": "^3.4.0",
+    "@testing-library/react-hooks": "^8.0.1",
     "@tinymce/tinymce-react": "^4.1.0",
     "autosuggest-highlight": "^3.1.1",
     "base-64": "^1.0.0",
diff --git a/gui/src/components/Actions.js b/gui/src/components/Actions.js
index 59016454911a81e3fb4122b5f82811bdd68a73a8..52e56248fdcadb4b9bec51df3f2fb751895bd036 100644
--- a/gui/src/components/Actions.js
+++ b/gui/src/components/Actions.js
@@ -160,7 +160,7 @@ Action.propTypes = {
   onMouseUp: PropTypes.func,
   tooltip: PropTypes.string,
   TooltipProps: PropTypes.object,
-  ButtonComponent: PropTypes.object,
+  ButtonComponent: PropTypes.elementType,
   ButtonProps: PropTypes.object,
   className: PropTypes.string,
   classes: PropTypes.object,
diff --git a/gui/src/components/App.js b/gui/src/components/App.js
index 834c0b8306d6970e4f9c3ce7bad761fbbd31fc67..b6320753bab2041dc9e89367c857f02ad357db14 100644
--- a/gui/src/components/App.js
+++ b/gui/src/components/App.js
@@ -33,6 +33,7 @@ import Navigation from './nav/Navigation'
 import GUIMenu from './GUIMenu'
 import { APIProvider, GlobalLoginRequired, onKeycloakEvent } from './api'
 import DataStore from './DataStore'
+import { UnitProvider } from './units/UnitContext'
 import { GlobalMetainfo } from './archive/metainfo'
 
 const keycloak = new Keycloak({
@@ -57,21 +58,26 @@ export default function App() {
           <MuiPickersUtilsProvider utils={DateFnsUtils}>
             <ErrorSnacks>
               <ErrorBoundary>
-                <DataStore>
-                  <GlobalMetainfo>
-                    <Router history={history}>
-                      <QueryParamProvider ReactRouterRoute={Route}>
-                        <MuiThemeProvider theme={nomadTheme}>
-                          <CssBaseline />
-                          <GlobalLoginRequired>
-                            <Navigation />
-                            <GUIMenu/>
-                          </GlobalLoginRequired>
-                        </MuiThemeProvider>
-                      </QueryParamProvider>
-                    </Router>
-                  </GlobalMetainfo>
-                </DataStore>
+                <UnitProvider
+                  initialUnitSystems={ui?.unit_systems?.options}
+                  initialSelected={ui?.unit_systems?.selected}
+                  >
+                  <DataStore>
+                    <GlobalMetainfo>
+                      <Router history={history}>
+                        <QueryParamProvider ReactRouterRoute={Route}>
+                          <MuiThemeProvider theme={nomadTheme}>
+                            <CssBaseline />
+                            <GlobalLoginRequired>
+                              <Navigation />
+                              <GUIMenu/>
+                            </GlobalLoginRequired>
+                          </MuiThemeProvider>
+                        </QueryParamProvider>
+                      </Router>
+                    </GlobalMetainfo>
+                  </DataStore>
+                </UnitProvider>
               </ErrorBoundary>
             </ErrorSnacks>
           </MuiPickersUtilsProvider>
diff --git a/gui/src/components/Help.js b/gui/src/components/Help.js
index d86891016a0516107bf23513ed49638eca6f88d2..b888dea9e81ea6782ec0603fff7c5489255859fa 100644
--- a/gui/src/components/Help.js
+++ b/gui/src/components/Help.js
@@ -15,67 +15,57 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import React from 'react'
-import { Button, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, Tooltip } from '@material-ui/core'
+import React, { useCallback, useState } from 'react'
+import { Button, IconButton, Dialog, DialogTitle, DialogContent, DialogActions } from '@material-ui/core'
 import Markdown from './Markdown'
 import PropTypes from 'prop-types'
 import HelpIcon from '@material-ui/icons/Help'
 
-export const HelpContext = React.createContext()
+export const HelpButton = ({title, content, maxWidth, IconProps, children, ...IconButtonProps}) => {
+  const [open, setOpen] = useState(false)
+  const handleToggleOpen = useCallback(() => {
+    setOpen(old => !old)
+    IconButtonProps?.onClick?.()
+  }, [IconButtonProps])
 
-class HelpDialog extends React.Component {
-  static propTypes = {
-    title: PropTypes.string,
-    content: PropTypes.string.isRequired,
-    children: PropTypes.node,
-    maxWidth: PropTypes.string
-  }
-
-  state = {
-    isOpen: false
-  }
-
-  constructor(props) {
-    super(props)
-    this.handleOpen = this.handleOpen.bind(this)
-    this.handleClose = this.handleClose.bind(this)
-  }
-
-  handleClose() {
-    this.setState({isOpen: false})
-  }
+  return <>
+    <IconButton {...IconButtonProps} onClick={handleToggleOpen}>
+      {children || <HelpIcon {...IconProps}/>}
+    </IconButton>
+    <HelpDialog title={title} content={content} open={open} onClose={handleToggleOpen} maxWidth={maxWidth} />
+  </>
+}
 
-  handleOpen() {
-    this.setState({isOpen: true})
-  }
+HelpButton.propTypes = {
+  title: PropTypes.string,
+  content: PropTypes.string,
+  maxWidth: PropTypes.string,
+  IconProps: PropTypes.object,
+  children: PropTypes.node
+}
 
-  render() {
-    const {title, content, children, maxWidth, ...rest} = this.props
-    return (
-      <React.Fragment>
-        <Tooltip title={title}>
-          <IconButton {...rest} onClick={this.handleOpen}>
-            {children || <HelpIcon/>}
-          </IconButton>
-        </Tooltip>
-        <Dialog
-          maxWidth={maxWidth}
-          onClose={this.handleClose}
-          open={this.state.isOpen}
-        >
-          <DialogTitle>{title || 'Help'}</DialogTitle>
-          <DialogContent>
-            <Markdown>{content}</Markdown>
-          </DialogContent>
-          <DialogActions>
-            <Button onClick={() => this.handleClose()} color="primary">
-              Close
-            </Button>
-          </DialogActions>
-        </Dialog>
-      </React.Fragment>
-    )
-  }
+const HelpDialog = ({title, content, maxWidth, open, onClose}) => {
+  return <Dialog
+    maxWidth={maxWidth}
+    onClose={onClose}
+    open={open}
+  >
+    <DialogTitle>{title || 'Help'}</DialogTitle>
+    <DialogContent>
+      <Markdown>{content}</Markdown>
+    </DialogContent>
+    <DialogActions>
+      <Button onClick={onClose} color="primary">
+        Close
+      </Button>
+    </DialogActions>
+  </Dialog>
 }
 
-export default HelpDialog
+HelpDialog.propTypes = {
+  title: PropTypes.string,
+  content: PropTypes.string,
+  maxWidth: PropTypes.string,
+  open: PropTypes.bool,
+  onClose: PropTypes.func
+}
diff --git a/gui/src/components/Quantity.js b/gui/src/components/Quantity.js
index c50365480e81548d1db0de7e32f61f40cdda8bf8..a01ca182d2d00aa7571191f2aef90a6f26615aa5 100644
--- a/gui/src/components/Quantity.js
+++ b/gui/src/components/Quantity.js
@@ -41,7 +41,9 @@ import Placeholder from './visualization/Placeholder'
 import Ellipsis from './visualization/Ellipsis'
 import NoData from './visualization/NoData'
 import { formatNumber, formatTimestamp, authorList, serializeMetainfo } from '../utils'
-import { Quantity as Q, Unit, useUnits } from '../units'
+import { Quantity as Q } from './units/Quantity'
+import { Unit } from './units/Unit'
+import { useUnitContext } from './units/UnitContext'
 import { defaultFilterData } from './search/FilterRegistry'
 import { MaterialLink, RouteLink } from './nav/Routes'
 
@@ -157,7 +159,7 @@ const Quantity = React.memo((props) => {
     ((presets.renderValue && !isNil(value)) && presets.renderValue(value)) ||
     ((quantity?.name && !isNil(quantity.type)) && getRenderFromType(quantity, data))
 
-  const units = useUnits()
+  const {units} = useUnitContext()
   let content = null
   let clipboardContent = null
 
diff --git a/gui/src/components/UnitSelector.js b/gui/src/components/UnitSelector.js
deleted file mode 100644
index b93a69c446e2821389469fbdf89c97cfb85c8cef..0000000000000000000000000000000000000000
--- a/gui/src/components/UnitSelector.js
+++ /dev/null
@@ -1,189 +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 { useRecoilState } from 'recoil'
-import { isNil } from 'lodash'
-import { makeStyles } from '@material-ui/core/styles'
-import {
-  Button,
-  Menu,
-  MenuItem,
-  FormControl,
-  InputLabel,
-  Select,
-  FormLabel,
-  FormControlLabel,
-  RadioGroup,
-  Radio,
-  Tooltip
-} from '@material-ui/core'
-import SettingsIcon from '@material-ui/icons/Settings'
-import PropTypes from 'prop-types'
-import clsx from 'clsx'
-import { unitMap, unitSystems, unitsState, dimensionMap } from '../units'
-
-/**
- * Unit selection menu with dropdowns for each dimension and presets for
- * different unit systems.
- */
-const useStyles = makeStyles((theme) => {
-  return {
-    root: {
-    },
-    menuItem: {
-      width: '15rem'
-    },
-    systems: {
-      margin: theme.spacing(2),
-      marginTop: theme.spacing(1),
-      marginBottom: theme.spacing(1)
-    }
-  }
-})
-const UnitSelector = React.memo(({
-  className,
-  classes,
-  onUnitChange,
-  onSystemChange
-}) => {
-  // States
-  const [anchorEl, setAnchorEl] = React.useState(null)
-  const open = Boolean(anchorEl)
-  const [units, setUnits] = useRecoilState(unitsState)
-  const styles = useStyles({classes: classes})
-
-  // Callbacks
-  const openMenu = useCallback((event) => {
-    setAnchorEl(event.currentTarget)
-  }, [])
-  const closeMenu = useCallback(() => {
-    setAnchorEl(null)
-  }, [])
-
-  // Used to handle unit system change.
-  const handleSystemChange = useCallback((event) => {
-    setUnits(unitSystems[event.target.value])
-    onSystemChange && onSystemChange(event)
-  }, [onSystemChange, setUnits])
-
-  // Used to handle unit change for a specific dimensionality. The changes are
-  // stored for each system separately.
-  const handleUnitChange = useCallback(event => {
-    const dimension = event.target.name
-    const unit = event.target.value
-    setUnits(old => {
-      const newSystem = {
-        ...old,
-        units: {
-          ...old.units,
-          ...{[dimension]: {...old.units[dimension], name: unit}}
-        }
-      }
-      unitSystems[old.label] = newSystem
-      return unitSystems[old.label]
-    })
-    onUnitChange && onUnitChange(event)
-  }, [onUnitChange, setUnits])
-
-  // Ordered list of controllable units. The 'dimensionless' unit cannot be
-  // changed.
-  const dimensions = Object.entries(dimensionMap)
-    .filter(([dimension, info]) => dimension !== 'dimensionless')
-
-  return <>
-    <Button
-      aria-controls="customized-menu"
-      aria-haspopup="true"
-      variant="text"
-      color="primary"
-      onClick={openMenu}
-      className={clsx(styles.root, className)}
-      startIcon={<SettingsIcon/>}
-    >
-      Units
-    </Button>
-    <Menu
-      id="select-unit"
-      anchorEl={anchorEl}
-      getContentAnchorEl={null}
-      anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
-      transformOrigin={{ vertical: 'top', horizontal: 'right' }}
-      keepMounted
-      open={open}
-      onClose={closeMenu}
-    >
-      <FormControl
-        component="fieldset"
-        classes={{root: styles.systems}}
-      >
-        <FormLabel component="legend">Unit system</FormLabel>
-        <RadioGroup name="unit-system" value={units.label} onChange={handleSystemChange}>
-          {Object.values(unitSystems).map(system => {
-            return <Tooltip key={system.label} title={system.description}>
-              <FormControlLabel value={system.label} control={<Radio />} label={system.label} />
-            </Tooltip>
-          })}
-        </RadioGroup>
-      </FormControl>
-      {dimensions.map(([dimension, unitInfo]) => {
-        const unitDef = units.units[dimension]
-        if (isNil(unitDef)) {
-          return null
-        }
-        const selectedUnit = unitDef.name
-        const disabled = unitDef.fixed
-        return <MenuItem
-          key={dimension}
-        >
-          <FormControl disabled={disabled}>
-            <InputLabel id="demo-simple-select-label">{unitInfo.label}</InputLabel>
-            <Select
-              classes={{root: styles.menuItem}}
-              labelId="demo-simple-select-label"
-              id="demo-simple-select"
-              name={dimension}
-              value={selectedUnit}
-              onChange={handleUnitChange}
-            >
-              {unitInfo.units.map((unit) => {
-                const unitLabel = unitMap[unit].label
-                const unitAbbreviation = unitMap[unit].abbreviation
-                return <MenuItem key={unit} value={unit}>{`${unitLabel} (${unitAbbreviation})`}</MenuItem>
-              })}
-            </Select>
-          </FormControl>
-        </MenuItem>
-      })}
-    </Menu>
-  </>
-})
-
-UnitSelector.propTypes = {
-  /**
-   * Callback for unit selection.
-   */
-  onUnitChange: PropTypes.func,
-  /**
-   * Callback for unit system selection.
-   */
-  onSystemChange: PropTypes.func,
-  className: PropTypes.string,
-  classes: PropTypes.object
-}
-
-export default UnitSelector
diff --git a/gui/src/components/archive/ArchiveBrowser.js b/gui/src/components/archive/ArchiveBrowser.js
index 950d72babd887020707445297abdbf879c34169e..50e8065de41e583eb3f8883fdeba527695d2ff14 100644
--- a/gui/src/components/archive/ArchiveBrowser.js
+++ b/gui/src/components/archive/ArchiveBrowser.js
@@ -35,7 +35,8 @@ import { ArchiveTitle, DefinitionLabel, metainfoAdaptorFactory } from './Metainf
 import { Matrix, Number } from './visualizations'
 import Markdown from '../Markdown'
 import { Overview } from './Overview'
-import { Quantity as Q, useUnits } from '../../units'
+import { Quantity as Q } from '../units/Quantity'
+import { useUnitContext } from '../units/UnitContext'
 import ArrowRightIcon from '@material-ui/icons/ArrowRight'
 import ArrowDownIcon from '@material-ui/icons/ArrowDropDown'
 import DownloadIcon from '@material-ui/icons/CloudDownload'
@@ -648,7 +649,7 @@ const convertComplexArray = (real, imag) => {
 }
 
 function QuantityItemPreview({value, def}) {
-  const units = useUnits()
+  const {units} = useUnitContext()
   if (isReference(def)) {
     return <Box component="span" fontStyle="italic">
       <Typography component="span">reference ...</Typography>
@@ -725,7 +726,7 @@ QuantityItemPreview.propTypes = ({
 })
 
 const QuantityValue = React.memo(function QuantityValue({value, def, ...more}) {
-  const units = useUnits()
+  const {units} = useUnitContext()
 
   const getRenderValue = useCallback(value => {
     let finalValue
diff --git a/gui/src/components/archive/Overview.js b/gui/src/components/archive/Overview.js
index fb1523305fbeb0c21e9ba23d1200aa9c689bd36e..f592a549d164ac866022147dbe04c094ce528435 100644
--- a/gui/src/components/archive/Overview.js
+++ b/gui/src/components/archive/Overview.js
@@ -12,7 +12,8 @@ import BrillouinZone from '../visualization/BrillouinZone'
 import BandStructure from '../visualization/BandStructure'
 import Spectra from '../visualization/Spectra'
 import DOS from '../visualization/DOS'
-import { Quantity, useUnits } from '../../units'
+import { Quantity } from '../units/Quantity'
+import { useUnitContext } from '../units/UnitContext'
 import { electronicRange } from '../../config'
 import EnergyVolumeCurve from '../visualization/EnergyVolumeCurve'
 
@@ -262,7 +263,7 @@ OverviewEquationOfState.propTypes = ({
 
 export const Overview = React.memo((props) => {
   const {def} = props
-  const units = useUnits()
+  const {units} = useUnitContext()
   const path = window.location.href.split('/').pop().split(':')[0]
 
   if (def.name === 'BandStructure' && path === 'band_structure_electronic') {
diff --git a/gui/src/components/archive/PlotlyFigure.js b/gui/src/components/archive/PlotlyFigure.js
index 2addff548c452b21758d9db165d9b31113908dd1..793d59e4cb8812df7c0cce657a55f9b42999b182 100644
--- a/gui/src/components/archive/PlotlyFigure.js
+++ b/gui/src/components/archive/PlotlyFigure.js
@@ -1,6 +1,7 @@
 import React, {useMemo} from 'react'
 import {Box} from '@material-ui/core'
-import {Quantity as Q, useUnits} from '../../units'
+import {Quantity as Q} from '../units/Quantity'
+import {useUnitContext} from '../units/UnitContext'
 import {titleCase, resolveInternalRef} from '../../utils'
 import {cloneDeep, merge} from 'lodash'
 import Plot from '../plotting/Plot'
@@ -29,7 +30,7 @@ const traverse = (value, callback, parent = null, key = null) => {
 }
 
 const PlotlyFigure = React.memo(function PlotlyFigure({plot, section, sectionDef, title, metaInfoLink}) {
-  const units = useUnits()
+  const {units} = useUnitContext()
 
   const plotlyGraphObj = useMemo(() => {
     if (!sectionDef?._properties) {
diff --git a/gui/src/components/archive/XYPlot.js b/gui/src/components/archive/XYPlot.js
index b4776e8ca679aec7ca9586ee01bd82efb1acc848..6046127018e0da1b3252cfe7f39a07ff616ae37d 100644
--- a/gui/src/components/archive/XYPlot.js
+++ b/gui/src/components/archive/XYPlot.js
@@ -1,6 +1,7 @@
 import React, {useMemo} from 'react'
 import {Box, useTheme} from '@material-ui/core'
-import {Quantity as Q, useUnits} from '../../units'
+import {Quantity as Q} from '../units/Quantity'
+import { useUnitContext } from '../units/UnitContext'
 import {titleCase, resolveInternalRef} from '../../utils'
 import {getLineStyles} from '../plotting/common'
 import { merge } from 'lodash'
@@ -17,7 +18,7 @@ class XYPlotError extends Error {
 
 const XYPlot = React.memo(function XYPlot({plot, section, sectionDef, title}) {
   const theme = useTheme()
-  const units = useUnits()
+  const {units} = useUnitContext()
   const xAxis = plot.x || plot['x_axis'] || plot['xAxis']
   const yAxis = plot.y || plot['y_axis'] || plot['yAxis']
 
diff --git a/gui/src/components/buttons/DownloadSystemButton.js b/gui/src/components/buttons/DownloadSystemButton.js
index dc10f252573717bed2ec8979fcccfa13823ce4fa..a7f87253a7bf1abe3cfbe26e9aa33b414ab68188 100644
--- a/gui/src/components/buttons/DownloadSystemButton.js
+++ b/gui/src/components/buttons/DownloadSystemButton.js
@@ -20,19 +20,24 @@ import PropTypes from 'prop-types'
 import {
   Menu,
   MenuItem,
-  InputLabel,
   FormControl,
+  FormLabel,
+  FormControlLabel,
   IconButton,
-  Select,
+  TextField,
+  Typography,
   DialogActions,
-  Box
+  Box,
+  Tooltip,
+  Radio,
+  RadioGroup
 } from '@material-ui/core'
 import LoadingButton from './LoadingButton'
 import { download } from '../../utils'
 import { useErrors } from '../errors'
 import { useApi } from '../api'
 
-// TODO: The available formats could be passed down to the GUI via pydantic
+// TODO: The available options could be passed down to the GUI via pydantic
 // model serialization.
 const formats = {
   cif: {
@@ -45,6 +50,54 @@ const formats = {
     label: 'PDB'
   }
 }
+export const wrapModes = {
+  original: {
+    key: 'original',
+    label: 'Original',
+    description: 'Original positions'
+  },
+  wrap: {
+    key: 'wrap',
+    label: 'Wrap',
+    description: 'Positions are wrapped to be inside the cell respecting periodic boundary conditions'
+  },
+  unwrap: {
+    key: 'unwrap',
+    label: 'Unwrap',
+    description: `Positions are reconstructed so that the structure is not split
+    by periodic cell boundaries. Note that this produces meaningful results only
+    if the system dimensions are smaller than the unit cell.`
+  }
+}
+
+export const WrapModeRadio = ({value, onChange, disabled, className}) => {
+  return <FormControl key='wrap' component="fieldset" className={className}>
+    <FormLabel component="legend">Wrap mode</FormLabel>
+    <RadioGroup
+      value={value}
+      onChange={onChange}
+      >
+      {Object.entries(wrapModes).map(([key, data]) =>
+        <FormControlLabel
+          key={key}
+          value={key}
+          control={<Radio color="primary" disabled={disabled}/>}
+          label={<Tooltip
+            title={wrapModes[key].description}>
+              <span>{data.label}</span>
+          </Tooltip>}
+        />
+      )}
+    </RadioGroup>
+  </FormControl>
+}
+
+WrapModeRadio.propTypes = {
+  value: PropTypes.string,
+  onChange: PropTypes.func,
+  disabled: PropTypes.bool,
+  className: PropTypes.string
+}
 
 /*
  * Menu for downloading a specific system.
@@ -53,6 +106,7 @@ export const DownloadSystemMenu = React.memo(React.forwardRef(({entryId, path, a
   const {api} = useApi()
   const {raiseError} = useErrors()
   const [format, setFormat] = useState('cif')
+  const [wrapMode, setWrapMode] = useState('original')
   const [loading, setLoading] = useState(false)
   const open = Boolean(anchorEl)
 
@@ -60,10 +114,14 @@ export const DownloadSystemMenu = React.memo(React.forwardRef(({entryId, path, a
     setFormat(event.target.value)
   }, [])
 
+  const handleChangeWrapMode = useCallback((event) => {
+    setWrapMode(event.target.value)
+  }, [])
+
   const handleClickDownload = useCallback(() => {
     setLoading(true)
     api.get(
-      `systems/${entryId}?path=${path}&format=${format}`,
+      `systems/${entryId}?path=${path}&format=${format}&wrap_mode=${wrapMode}`,
       undefined,
       {responseType: 'blob', fullResponse: true}
     )
@@ -74,7 +132,7 @@ export const DownloadSystemMenu = React.memo(React.forwardRef(({entryId, path, a
       })
       .finally(() => setLoading(false))
       .catch(raiseError)
-  }, [entryId, path, format, api, raiseError])
+  }, [entryId, path, format, wrapMode, api, raiseError])
 
   return <Menu
       anchorEl={anchorEl}
@@ -85,19 +143,18 @@ export const DownloadSystemMenu = React.memo(React.forwardRef(({entryId, path, a
       open={open}
       onClose={onClose}
     >
-      <Box minWidth="10rem" paddingLeft={2} paddingRight={2} paddingTop={1}>
-        <FormControl fullWidth>
-          <InputLabel>Format</InputLabel>
-          <Select
-            name="Format"
-            value={format}
-            onChange={handleChangeFormat}
-          >
-            {Object.entries(formats).map(([key, value]) => {
-              return <MenuItem key={key} value={key}>{value.label}</MenuItem>
-            })}
-          </Select>
-        </FormControl>
+      <Box minWidth="13rem" paddingLeft={2} paddingRight={2} paddingTop={1}>
+        <Typography variant='h6' fontSize='0.9rem'>
+          Download system
+        </Typography>
+        <Box mt={1} />
+        <TextField select value={format} onChange={handleChangeFormat} size='small' variant="filled" label="Format" fullWidth>
+          {Object.entries(formats).map(([key, value]) => {
+            return <MenuItem key={key} value={key}>{value.label}</MenuItem>
+          })}
+        </TextField>
+        <Box mt={2} />
+        <WrapModeRadio value={wrapMode} onChange={handleChangeWrapMode} />
         <Box marginRight={-1} marginBottom={-1}>
           <DialogActions>
             <LoadingButton onClick={handleClickDownload} color="primary" loading={loading}>
diff --git a/gui/src/components/conftest.spec.js b/gui/src/components/conftest.spec.js
index e1cc8dc8756a7535a0b3928940d2fbf72a22d146..9596e0cecf8508669141332edbcb10ced6374178 100644
--- a/gui/src/components/conftest.spec.js
+++ b/gui/src/components/conftest.spec.js
@@ -39,10 +39,11 @@ import { seconds, server } from '../setupTests'
 import { Router, MemoryRouter } from 'react-router-dom'
 import { createBrowserHistory } from 'history'
 import { APIProvider } from './api'
+import { UnitProvider } from './units/UnitContext'
 import { ErrorSnacks, ErrorBoundary } from './errors'
 import DataStore from './DataStore'
 import { defaultFilterData } from './search/FilterRegistry'
-import { keycloakBase, searchQuantities } from '../config'
+import { keycloakBase, searchQuantities, ui } from '../config'
 import { useKeycloak } from '@react-keycloak/web'
 import { GlobalMetainfo } from './archive/metainfo'
 
@@ -104,15 +105,20 @@ export const WrapperDefault = ({children}) => {
       <MuiPickersUtilsProvider utils={DateFnsUtils}>
         <ErrorSnacks>
           <ErrorBoundary>
-            <DataStore>
-              <GlobalMetainfo>
-                <Router history={createBrowserHistory({basename: process.env.PUBLIC_URL})}>
-                  <MemoryRouter>
-                    {children}
-                  </MemoryRouter>
-                </Router>
-              </GlobalMetainfo>
-            </DataStore>
+            <UnitProvider
+              initialUnitSystems={ui?.unit_systems?.options}
+              initialSelected={ui?.unit_systems?.selected}
+              >
+              <DataStore>
+                <GlobalMetainfo>
+                  <Router history={createBrowserHistory({basename: process.env.PUBLIC_URL})}>
+                    <MemoryRouter>
+                      {children}
+                    </MemoryRouter>
+                  </Router>
+                </GlobalMetainfo>
+              </DataStore>
+            </UnitProvider>
           </ErrorBoundary>
         </ErrorSnacks>
       </MuiPickersUtilsProvider>
@@ -140,7 +146,12 @@ export const WrapperNoAPI = ({children}) => {
         <MemoryRouter>
           <ErrorSnacks>
             <ErrorBoundary>
-              {children}
+              <UnitProvider
+                initialUnitSystems={ui?.unit_systems?.options}
+                initialSelected={ui?.unit_systems?.selected}
+                >
+                {children}
+              </UnitProvider>
             </ErrorBoundary>
           </ErrorSnacks>
         </MemoryRouter>
diff --git a/gui/src/components/editQuantity/NumberEditQuantity.js b/gui/src/components/editQuantity/NumberEditQuantity.js
index b9d688c27e282cfae3779217949cc3991cb0d7d6..262affa808076eef318924288104d53d1fd592ca 100644
--- a/gui/src/components/editQuantity/NumberEditQuantity.js
+++ b/gui/src/components/editQuantity/NumberEditQuantity.js
@@ -19,7 +19,9 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
 import {TextField, makeStyles, Box, Checkbox, Tooltip} from '@material-ui/core'
 import Autocomplete from '@material-ui/lab/Autocomplete'
 import PropTypes from 'prop-types'
-import {getUnits, Unit, Quantity, useUnits, parseQuantity} from '../../units'
+import {Quantity, parseQuantity} from '../units/Quantity'
+import {Unit} from '../units/Unit'
+import {useUnitContext, getUnits} from '../units/UnitContext'
 import {debounce, isNil} from 'lodash'
 import {TextFieldWithHelp, getFieldProps} from './StringEditQuantity'
 import {useErrors} from '../errors'
@@ -203,7 +205,7 @@ NumberField.propTypes = {
 
 export const NumberEditQuantity = React.memo((props) => {
   const {quantityDef, value, onChange, ...otherProps} = props
-  const systemUnits = useUnits()
+  const {units} = useUnitContext()
   const {raiseError} = useErrors()
   const defaultUnit = useMemo(() => quantityDef.unit && new Unit(quantityDef.unit), [quantityDef])
   const dimension = defaultUnit && defaultUnit.dimension(false)
@@ -229,7 +231,7 @@ export const NumberEditQuantity = React.memo((props) => {
 
   const [unit, setUnit] = useState(
     defaultDisplayUnitObj ||
-    (systemUnits[dimension]?.name && new Unit(systemUnits[dimension]?.name)) ||
+    (units[dimension]?.definition && new Unit(units[dimension]?.definition)) ||
     defaultUnit
   )
 
@@ -256,7 +258,6 @@ export const NumberEditQuantity = React.memo((props) => {
   // Handle a change in the unit dialog
   const handleUnitChange = useCallback((newUnit) => {
     if (!checked && quantityDef.unit && newUnit && !isNil(value)) {
-      // const displayedValue = new Quantity(value, quantityDef.unit).to(unit).value()
       const storedValue = new Quantity(Number(displayedValue), newUnit).to(quantityDef.unit).value()
       onChange(storedValue)
     }
diff --git a/gui/src/components/editQuantity/SliderEditQuantity.js b/gui/src/components/editQuantity/SliderEditQuantity.js
index e647fd9dba2a2f76bc1b3a569da637dff4ddf586..4c6b9c4bdb6a60c71c4c155d100333b74e558807 100644
--- a/gui/src/components/editQuantity/SliderEditQuantity.js
+++ b/gui/src/components/editQuantity/SliderEditQuantity.js
@@ -22,7 +22,9 @@ import {
   FormLabel, Slider
 } from '@material-ui/core'
 import PropTypes from 'prop-types'
-import {Quantity, Unit, useUnits} from '../../units'
+import {Quantity} from '../units/Quantity'
+import {Unit} from '../units/Unit'
+import {useUnitContext} from '../units/UnitContext'
 import {UnitSelect} from './NumberEditQuantity'
 import {getFieldProps} from './StringEditQuantity'
 
@@ -30,10 +32,10 @@ export const SliderEditQuantity = React.memo((props) => {
   const {quantityDef, value, onChange, minValue, maxValue, ...sliderProps} = props
   const {label} = getFieldProps(quantityDef)
 
-  const systemUnits = useUnits()
+  const {units} = useUnitContext()
   const defaultUnit = useMemo(() => quantityDef.unit && new Unit(quantityDef.unit), [quantityDef])
   const dimension = defaultUnit && defaultUnit.dimension()
-  const [unit, setUnit] = useState(systemUnits[dimension] || quantityDef.unit)
+  const [unit, setUnit] = useState(units[dimension]?.definition || quantityDef.unit)
   const minValueConverted = useMemo(() => {
     return unit
       ? new Quantity(minValue, quantityDef.unit).to(unit).value()
diff --git a/gui/src/components/entry/conftest.spec.js b/gui/src/components/entry/conftest.spec.js
index 485a27c22d0e8b69daa99e1142f0266af94d91f4..ff1d62ca5b843a52833c0b832e4842059c95909e 100644
--- a/gui/src/components/entry/conftest.spec.js
+++ b/gui/src/components/entry/conftest.spec.js
@@ -22,7 +22,7 @@ import userEvent from '@testing-library/user-event'
 import { expectQuantity, screen } from '../conftest.spec'
 import { expectPlotButtons } from '../visualization/conftest.spec'
 import { traverseDeep, serializeMetainfo } from '../../utils'
-import { unitSystems } from '../../units'
+import { ui } from '../../config'
 
 /*****************************************************************************/
 // Expects
@@ -115,7 +115,7 @@ export async function expectMethodologyItem(
     expect(root.getByText(title)).toBeInTheDocument()
     for (const [key, value] of traverseDeep(data, true)) {
       const quantity = `${path}.${key.join('.')}`
-      expectQuantity(quantity, serializeMetainfo(quantity, value, unitSystems.Custom.units))
+      expectQuantity(quantity, serializeMetainfo(quantity, value, ui.unit_systems.options.Custom.units))
     }
   }
 }
diff --git a/gui/src/components/entry/properties/MechanicalPropertiesCard.js b/gui/src/components/entry/properties/MechanicalPropertiesCard.js
index 8e1f1e439935389ba97d0d172ca6637ab0dfcec0..46ca35928c1b1be7f60aad584aa6185fb0ff39ee 100644
--- a/gui/src/components/entry/properties/MechanicalPropertiesCard.js
+++ b/gui/src/components/entry/properties/MechanicalPropertiesCard.js
@@ -18,7 +18,7 @@
 import React from 'react'
 import PropTypes from 'prop-types'
 import { PropertyCard } from './PropertyCard'
-import { useUnits } from '../../../units'
+import { useUnitContext } from '../../units/UnitContext'
 import { getLocation, resolveInternalRef } from '../../../utils'
 import { refPath } from '../../archive/metainfo'
 import MechanicalProperties from '../../visualization/MechanicalProperties'
@@ -27,7 +27,7 @@ import MechanicalProperties from '../../visualization/MechanicalProperties'
  * Card displaying mechanical properties.
  */
 const MechanicalPropertiesCard = React.memo(({index, properties, archive}) => {
-  const units = useUnits()
+  const {units} = useUnitContext()
   const urlPrefix = `${getLocation()}/data`
 
   // Find out which properties are present
diff --git a/gui/src/components/entry/properties/VibrationalPropertiesCard.js b/gui/src/components/entry/properties/VibrationalPropertiesCard.js
index 4ce812cdfcee4cf1bf41d104d1c4d66dc7522d41..6fc49aac6aff192bc81eae0666b61f5209df088d 100644
--- a/gui/src/components/entry/properties/VibrationalPropertiesCard.js
+++ b/gui/src/components/entry/properties/VibrationalPropertiesCard.js
@@ -18,7 +18,7 @@
 import React from 'react'
 import PropTypes from 'prop-types'
 import { PropertyCard } from './PropertyCard'
-import { useUnits } from '../../../units'
+import { useUnitContext } from '../../units/UnitContext'
 import { getLocation, resolveInternalRef } from '../../../utils'
 import { refPath } from '../../archive/metainfo'
 import VibrationalProperties from '../../visualization/VibrationalProperties'
@@ -27,7 +27,7 @@ import VibrationalProperties from '../../visualization/VibrationalProperties'
  * Card displaying vibrational properties.
  */
 const VibrationalPropertiesCard = React.memo(({index, properties, archive}) => {
-  const units = useUnits()
+  const {units} = useUnitContext()
   const urlPrefix = `${getLocation()}/data`
 
   // Find out which properties are present
diff --git a/gui/src/components/nav/AppBar.js b/gui/src/components/nav/AppBar.js
index 51a5085a16db5a91ff31f3f904019533144cc97f..d990a75fd32b5f4cc705dca8ae68a0430e761f3d 100644
--- a/gui/src/components/nav/AppBar.js
+++ b/gui/src/components/nav/AppBar.js
@@ -26,7 +26,7 @@ import {
   makeStyles
 } from '@material-ui/core'
 import LoginLogout from '../LoginLogout'
-import UnitSelector from '../UnitSelector'
+import UnitMenu from '../units/UnitMenu'
 import MainMenu from './MainMenu'
 import { useLoading } from '../api'
 import { guiBase, oasis } from '../../config'
@@ -64,7 +64,6 @@ const useStyles = makeStyles(theme => ({
   toolbar: {
     display: 'flex',
     flexDirection: 'row'
-    // paddingRight: theme.spacing(3)
   },
   logoImg: {
     height: 44,
@@ -87,8 +86,6 @@ const useStyles = makeStyles(theme => ({
   navigation: {
     flexGrow: 1,
     marginRight: theme.spacing(1),
-    // marginBottom: theme.spacing(1),
-    // marginTop: theme.spacing(0.25),
     display: 'flex',
     flexDirection: 'column',
     alignItems: 'flex-start',
@@ -124,7 +121,7 @@ export default function AppBar() {
       </div>
       <div className={styles.actions}>
         <LoginLogout color="primary" classes={{button: styles.menuItem}} />
-        <UnitSelector className={styles.menuItem}></UnitSelector>
+        <UnitMenu className={styles.menuItem} />
       </div>
     </Toolbar>
     <LoadingIndicator className={styles.progress}/>
diff --git a/gui/src/components/nav/Breadcrumbs.js b/gui/src/components/nav/Breadcrumbs.js
index b6dea180c31fc01e83ef3792a8e8f1b32cf38dab..038438e23bb09a3de59fd319dc1229ca96904bd6 100644
--- a/gui/src/components/nav/Breadcrumbs.js
+++ b/gui/src/components/nav/Breadcrumbs.js
@@ -18,9 +18,8 @@
 
 import React, { useCallback, useMemo } from 'react'
 import { matchPath, useLocation, Link as RouterLink } from 'react-router-dom'
-import { Typography, Breadcrumbs as MUIBreadcrumbs, Link, Box, makeStyles } from '@material-ui/core'
-import HelpDialog from '../Help'
-import HelpIcon from '@material-ui/icons/Help'
+import { Typography, Breadcrumbs as MUIBreadcrumbs, Link, Box, makeStyles, Tooltip } from '@material-ui/core'
+import { HelpButton } from '../Help'
 import { allRoutes } from './Routes'
 import {useDataStore} from "../DataStore"
 
@@ -32,9 +31,6 @@ const useStyles = makeStyles(theme => ({
   help: {
     marginLeft: theme.spacing(0.5)
   },
-  helpIcon: {
-    fontSize: 18
-  },
   ellipsis: {
     direction: 'rtl',
     textAlign: 'left',
@@ -89,9 +85,11 @@ const Breadcrumbs = React.memo(function Breadcrumbs() {
           return <Box key={i} display="flex" flexDirection="row" alignItems="center">
             {title}
             {route.help && (
-              <HelpDialog className={styles.help} size="small" {...route.help}>
-                <HelpIcon className={styles.helpIcon} />
-              </HelpDialog>
+              <Tooltip title={route?.help?.title || ""}>
+                <span>
+                  <HelpButton className={styles.help} size="small" IconProps={{fontSize: 'small'}} {...route.help} />
+                </span>
+              </Tooltip>
             )}
           </Box>
         } else {
diff --git a/gui/src/components/plotting/PlotAxis.js b/gui/src/components/plotting/PlotAxis.js
index c0ecebd43fc18579f3389295c7d72e17b400a36a..fedb11293b1bef788ca5fe27fe166af3447a90c8 100644
--- a/gui/src/components/plotting/PlotAxis.js
+++ b/gui/src/components/plotting/PlotAxis.js
@@ -22,7 +22,9 @@ import { makeStyles } from '@material-ui/core/styles'
 import { isArray, isNil } from 'lodash'
 import { useResizeDetector } from 'react-resize-detector'
 import { getScaler, getTicks } from './common'
-import { useUnits, Quantity, Unit } from '../../units'
+import { Quantity } from '../units/Quantity'
+import { Unit } from '../units/Unit'
+import { useUnitContext } from '../units/UnitContext'
 import { formatNumber, DType } from '../../utils'
 import PlotLabel from './PlotLabel'
 import PlotTick from './PlotTick'
@@ -93,7 +95,7 @@ const PlotAxis = React.memo(({
   classes,
   'data-testid': testID}) => {
   const styles = usePlotAxisStyles(classes)
-  const units = useUnits()
+  const {units} = useUnitContext()
   const unitObj = useMemo(() => new Unit(unit), [unit])
   const {height, width, ref} = useResizeDetector()
   const orientation = {
diff --git a/gui/src/components/plotting/PlotHistogram.js b/gui/src/components/plotting/PlotHistogram.js
index 97822c37c715d6e56073c47943ad360548897751..a52074a9a5294ba892477feb663f6805ddfe4e70 100644
--- a/gui/src/components/plotting/PlotHistogram.js
+++ b/gui/src/components/plotting/PlotHistogram.js
@@ -22,7 +22,7 @@ import { useRecoilValue } from 'recoil'
 import { Slider } from '@material-ui/core'
 import { makeStyles } from '@material-ui/core/styles'
 import { pluralize, formatInteger } from '../../utils'
-import { Unit } from '../../units'
+import { Unit } from '../units/Unit'
 import InputUnavailable from '../search/input/InputUnavailable'
 import Placeholder from '../visualization/Placeholder'
 import PlotAxis from './PlotAxis'
diff --git a/gui/src/components/plotting/PlotScatter.js b/gui/src/components/plotting/PlotScatter.js
index fa14120e56086fc0c2f8fe7d79b8837db4bb848c..33b950ad4c4c15e8db1a9c50bca3d61d77f13114 100644
--- a/gui/src/components/plotting/PlotScatter.js
+++ b/gui/src/components/plotting/PlotScatter.js
@@ -19,7 +19,9 @@ import React, {useState, useEffect, useMemo, useCallback, forwardRef} from 'reac
 import PropTypes from 'prop-types'
 import { makeStyles, useTheme } from '@material-ui/core'
 import { getDeep, hasWebGLSupport, parseQuantityName } from '../../utils'
-import { useUnits, Quantity, Unit } from '../../units'
+import { Quantity } from '../units/Quantity'
+import { Unit } from '../units/Unit'
+import { useUnitContext } from '../units/UnitContext'
 import * as d3 from 'd3'
 import { isArray, isNil } from 'lodash'
 import FilterTitle from '../search/FilterTitle'
@@ -108,7 +110,7 @@ const PlotScatter = React.memo(forwardRef((
   const styles = useStyles()
   const theme = useTheme()
   const [finalData, setFinalData] = useState(!data ? data : undefined)
-  const units = useUnits()
+  const {units} = useUnitContext()
   const { filterData } = useSearchContext()
   const history = useHistory()
 
diff --git a/gui/src/components/search/Filter.js b/gui/src/components/search/Filter.js
index 81d3241f265bbec58ace36c3e5b55e68d5281057..d646189117b573ffb949b61b324289ceb5c4153b 100644
--- a/gui/src/components/search/Filter.js
+++ b/gui/src/components/search/Filter.js
@@ -25,7 +25,7 @@ import {
   DType,
   multiTypes
 } from '../../utils'
-import { Unit } from '../../units'
+import { Unit } from '../units/Unit'
 
 /**
  * Filter is a wrapper for metainfo (quantity or section) that can be searched.
diff --git a/gui/src/components/search/FilterSummary.js b/gui/src/components/search/FilterSummary.js
index ebd396bc1e92ca89dddbdcaa5db2bb3b95badb6a..16b21329cfe66fbfb261875c7e14315feef577fc 100644
--- a/gui/src/components/search/FilterSummary.js
+++ b/gui/src/components/search/FilterSummary.js
@@ -22,7 +22,7 @@ import clsx from 'clsx'
 import { isNil, isPlainObject } from 'lodash'
 import { FilterChip, FilterChipGroup, FilterAnd, FilterOr } from './FilterChip'
 import { useSearchContext } from './SearchContext'
-import { useUnits } from '../../units'
+import { useUnitContext } from '../units/UnitContext'
 
 /**
  * Smart component that displays a set of FilterGroups and FilterChips for the
@@ -61,7 +61,7 @@ const FilterSummary = React.memo(({
   const filters = useFilters(quantities)
   const updateFilter = useUpdateFilter()
   const theme = useTheme()
-  const units = useUnits()
+  const {units} = useUnitContext()
   const styles = useStyles({classes: classes, theme: theme})
 
   // Creates a set of chips for a quantity
diff --git a/gui/src/components/search/FilterTitle.js b/gui/src/components/search/FilterTitle.js
index 65a3df0c0b4af0bc7027c960bb7e70460ea10121..5e53bcbec174c427e7c7ed5545f3965423715b03 100644
--- a/gui/src/components/search/FilterTitle.js
+++ b/gui/src/components/search/FilterTitle.js
@@ -22,7 +22,8 @@ import PropTypes from 'prop-types'
 import clsx from 'clsx'
 import { useSearchContext } from './SearchContext'
 import { inputSectionContext } from './input/InputSection'
-import { useUnits, Unit } from '../../units'
+import { Unit } from '../units/Unit'
+import { useUnitContext } from '../units/UnitContext'
 
 /**
  * Title for a metainfo quantity or section that is used in a search context.
@@ -68,7 +69,7 @@ const FilterTitle = React.memo(({
   const styles = useStaticStyles({classes: classes})
   const { filterData } = useSearchContext()
   const sectionContext = useContext(inputSectionContext)
-  const units = useUnits()
+  const {units} = useUnitContext()
   const section = sectionContext?.section
 
   // Create the final label
diff --git a/gui/src/components/search/SearchBar.js b/gui/src/components/search/SearchBar.js
index 336353c054af4222a976b0d26cfe1f5d2292d9ff..f08765dc5813e77b4e775d521f11e875f8b31cab 100644
--- a/gui/src/components/search/SearchBar.js
+++ b/gui/src/components/search/SearchBar.js
@@ -32,7 +32,6 @@ import {
   ListItemText
 } from '@material-ui/core'
 import IconButton from '@material-ui/core/IconButton'
-import { useUnits } from '../../units'
 import { DType, getSchemaAbbreviation } from '../../utils'
 import { useSuggestions } from '../../hooks'
 import { useSearchContext } from './SearchContext'
@@ -94,7 +93,6 @@ const SearchBar = React.memo(({
   className
 }) => {
   const styles = useStyles()
-  const units = useUnits()
   const {
     filters,
     filterData,
@@ -188,7 +186,7 @@ const SearchBar = React.memo(({
     const presence = inputValue.match(new RegExp(`^\\s*(${reString})\\s*=\\s*\\*\\s*$`))
     if (presence) {
       quantityFullname = `quantities`
-      queryValue = parseQuery(quantityFullname, presence[1], units) // are units still necessary?
+      queryValue = parseQuery(quantityFullname, presence[1])
       valid = true
     }
 
@@ -204,7 +202,7 @@ const SearchBar = React.memo(({
           return
         }
         try {
-          queryValue = parseQuery(quantityFullname, equals[2], units)
+          queryValue = parseQuery(quantityFullname, equals[2])
         } catch (error) {
           setError(`Invalid value for this metainfo. Please check your syntax.`)
           return
@@ -240,7 +238,7 @@ const SearchBar = React.memo(({
         }
         let quantityValue
         try {
-          quantityValue = parseQuery(quantityFullname, value, units, undefined, false)
+          quantityValue = parseQuery(quantityFullname, value, undefined, false)
         } catch (error) {
           console.log(error)
           setError(`Invalid value for this metainfo. Please check your syntax.`)
@@ -274,8 +272,8 @@ const SearchBar = React.memo(({
         }
         queryValue = {}
         try {
-          queryValue[opMapReverse[op1]] = parseQuery(quantityFullname, a, units, undefined, false)
-          queryValue[opMap[op2]] = parseQuery(quantityFullname, c, units, undefined, false)
+          queryValue[opMapReverse[op1]] = parseQuery(quantityFullname, a, undefined, false)
+          queryValue[opMap[op2]] = parseQuery(quantityFullname, c, undefined, false)
         } catch (error) {
           setError(`Invalid value for this metainfo. Please check your syntax.`)
           return
@@ -303,7 +301,7 @@ const SearchBar = React.memo(({
     } else {
       setError(`Invalid query`)
     }
-  }, [inputValue, checkMetainfo, units, updateFilter, filterData, parseQuery, filtersLocked])
+  }, [inputValue, checkMetainfo, updateFilter, filterData, parseQuery, filtersLocked])
 
   // Handle clear button
   const handleClose = useCallback(() => {
diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js
index edda4e0c2b49e74f25260fe10938e017fb48087d..a35fa6e3d6a47f965f87b1e1b3b6b343a33aa63b 100644
--- a/gui/src/components/search/SearchContext.js
+++ b/gui/src/components/search/SearchContext.js
@@ -61,13 +61,15 @@ import {
   rsplit,
   parseOperator
 } from '../../utils'
-import { Quantity, Unit } from '../../units'
+import { Quantity } from '../units/Quantity'
+import { Unit } from '../units/Unit'
 import { useErrors } from '../errors'
 import { combinePagination, addColumnDefaults } from '../datatable/Datatable'
 import UploadStatusIcon from '../uploads/UploadStatusIcon'
 import { getWidgetsObject } from './widgets/Widget'
 import { inputSectionContext } from './input/InputSection'
 import { withFilters } from './FilterRegistry'
+import { useUnitContext } from '../units/UnitContext'
 
 const useWidthConstrainedStyles = makeStyles(theme => ({
   root: {
@@ -200,6 +202,7 @@ export const SearchContextRaw = React.memo(({
   children
 }) => {
   const {api, user} = useApi()
+  const {units} = useUnitContext()
   const {raiseError} = useErrors()
   const oldQuery = useRef(undefined)
   const oldPagination = useRef(undefined)
@@ -1518,7 +1521,7 @@ export const SearchContextRaw = React.memo(({
      * */
     const useParseQuery = () => {
       return useCallback(
-        (key, value, units, path, multiple) => parseQuery(key, value, filtersData, units, path, multiple),
+        (key, value, path, multiple) => parseQuery(key, value, filtersData, units, path, multiple),
         []
       )
     }
@@ -1632,7 +1635,8 @@ export const SearchContextRaw = React.memo(({
     updateAggsResponse,
     setPagination,
     setResults,
-    setApiData
+    setApiData,
+    units
   ])
 
   return <searchContext.Provider value={values}>
diff --git a/gui/src/components/search/SearchContext.spec.js b/gui/src/components/search/SearchContext.spec.js
index 166b913c79b592bca1e4cfa20ae182217bdc9b78..2f2fc8e5faefcb866e0413a96874c20c564e7535 100644
--- a/gui/src/components/search/SearchContext.spec.js
+++ b/gui/src/components/search/SearchContext.spec.js
@@ -15,30 +15,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import React from 'react'
-import { renderSearchEntry } from './conftest.spec'
+import { renderHook } from '@testing-library/react-hooks'
+import { WrapperSearch } from './conftest.spec'
 import { useSearchContext } from './SearchContext'
-import { unitSystems, Quantity } from '../../units'
+import { Quantity } from '../units/Quantity'
 import { isEqualWith } from 'lodash'
 
-/**
- * Function that exposes the useSearchContext hook.
- */
-function setup() {
-  const returnVal = {}
-
-  function TestComponent() {
-    const {useParseQuery} = useSearchContext()
-    const parseQuery = useParseQuery()
-    Object.assign(returnVal, {parseQuery})
-    return null
-  }
-  renderSearchEntry(
-    <TestComponent />
-  )
-
-  return returnVal
-}
 describe('parseQuery', function() {
   test.each([
     ['unit not specified', 'results.material.topology.cell.a', '1', new Quantity(1, 'angstrom'), undefined],
@@ -47,16 +29,18 @@ describe('parseQuery', function() {
     ['filter hat accepts multiple values is wrapped in set', 'results.material.material_id', 'abcd', new Set(['abcd']), undefined],
     ['filter that does not accept multiple values is not wrapped in set', 'visibility', 'public', 'public', undefined]
   ])('%s', async (name, quantity, input, output, error) => {
-    const parseQuery = setup().parseQuery
+    const { result: resultUseSearchContext } = renderHook(() => useSearchContext(), { wrapper: WrapperSearch })
+    const { result: resultUseParseQuery } = renderHook(() => resultUseSearchContext.current.useParseQuery(), {})
+    const parseQuery = resultUseParseQuery.current
     if (!error) {
       function customizer(a, b) {
         if (a instanceof Quantity) {
           return a.equal(b)
         }
       }
-      expect(isEqualWith(parseQuery(quantity, input, unitSystems.Custom.units), output, customizer)).toBe(true)
+      expect(isEqualWith(parseQuery(quantity, input), output, customizer)).toBe(true)
     } else {
-      expect(() => parseQuery(quantity, input, unitSystems.Custom.units)).toThrow(error)
+      expect(() => parseQuery(quantity, input)).toThrow(error)
     }
     }
   )
diff --git a/gui/src/components/search/conftest.spec.js b/gui/src/components/search/conftest.spec.js
index cead5c0ea71b6a3e965d7a5423fa9952ebcf726f..7792d374268c78d581da81adc14d2b1cc975d793 100644
--- a/gui/src/components/search/conftest.spec.js
+++ b/gui/src/components/search/conftest.spec.js
@@ -28,7 +28,8 @@ import { SearchContext } from './SearchContext'
 import { defaultFilterData } from './FilterRegistry'
 import { format } from 'date-fns'
 import { DType } from '../../utils'
-import { Unit, unitSystems } from '../../units'
+import { Unit } from '../units/Unit'
+import { ui } from '../../config'
 import { menuMap } from './menus/FilterMainMenu'
 
 /*****************************************************************************/
@@ -36,7 +37,7 @@ import { menuMap } from './menus/FilterMainMenu'
 /**
  * Render within a search context.
  */
-const WrapperSearch = ({children}) => {
+export const WrapperSearch = ({children}) => {
   return <WrapperDefault>
     <SearchContext resource="entries">
       {children}
@@ -70,7 +71,7 @@ export async function expectFilterTitle(quantity, label, description, unit, disa
   const finalDescription = description || data?.description
   if (!disableUnit) {
     const finalUnit = unit || (
-      data?.unit && new Unit(data?.unit).toSystem(unitSystems.Custom.units).label()
+      data?.unit && new Unit(data?.unit).toSystem(ui.unit_systems.options.Custom.units).label()
     )
     if (finalUnit) finalLabel = `${finalLabel} (${finalUnit})`
   }
diff --git a/gui/src/components/search/input/InputRange.js b/gui/src/components/search/input/InputRange.js
index 822a8d6c1dc5d35150729296871c9d2d3df4f5bf..433fca03f3f1f87ca02f095841ecd1c3ee92f4a7 100644
--- a/gui/src/components/search/input/InputRange.js
+++ b/gui/src/components/search/input/InputRange.js
@@ -27,7 +27,9 @@ import InputHeader from './InputHeader'
 import InputTooltip from './InputTooltip'
 import { inputSectionContext } from './InputSection'
 import { InputTextField } from './InputText'
-import { useUnits, Quantity, Unit } from '../../../units'
+import { Quantity } from '../../units/Quantity'
+import { Unit } from '../../units/Unit'
+import { useUnitContext } from '../../units/UnitContext'
 import { DType, formatNumber } from '../../../utils'
 import { getInterval } from '../../plotting/common'
 import { dateFormat } from '../../../config'
@@ -118,7 +120,7 @@ export const Range = React.memo(({
   classes,
   'data-testid': testID
 }) => {
-  const units = useUnits()
+  const {units} = useUnitContext()
   const {filterData, useAgg, useFilterState, useIsStatisticsEnabled} = useSearchContext()
   const sectionContext = useContext(inputSectionContext)
   const repeats = sectionContext?.repeats
diff --git a/gui/src/components/search/input/InputText.js b/gui/src/components/search/input/InputText.js
index 854f7ec6cf25a65e5e818cfa86456f18748d0dd6..6c99cd0e2ac78cfd281a710606fe24db839ceb20 100644
--- a/gui/src/components/search/input/InputText.js
+++ b/gui/src/components/search/input/InputText.js
@@ -18,14 +18,13 @@
 import React, { useCallback, useState, useMemo, useRef } from 'react'
 import { makeStyles, useTheme } from '@material-ui/core/styles'
 import PropTypes from 'prop-types'
-import { useRecoilValue } from 'recoil'
 import clsx from 'clsx'
 import { CircularProgress, Tooltip, IconButton, TextField } from '@material-ui/core'
 import Autocomplete from '@material-ui/lab/Autocomplete'
+import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'
 import CloseIcon from '@material-ui/icons/Close'
 import { isNil } from 'lodash'
 import { useSearchContext } from '../SearchContext'
-import { guiState } from '../../GUIMenu'
 import { useSuggestions } from '../../../hooks'
 import { searchQuantities } from '../../../config'
 import Placeholder from '../../visualization/Placeholder'
@@ -40,13 +39,11 @@ const useInputTextFieldStyles = makeStyles(theme => ({
 }))
 export const InputTextField = React.memo((props) => {
   const initialLabel = useState(props.label)[0]
-  const inputVariant = useRecoilValue(guiState('inputVariant'))
-  const inputSize = useRecoilValue(guiState('inputSize'))
   const styles = useInputTextFieldStyles({classes: props.classes})
 
   return props.loading
     ? <Placeholder className={clsx(props.className, styles.root)} />
-    : <TextField size={inputSize} variant={inputVariant} {...props} hiddenLabel={!initialLabel}/>
+    : <TextField size="small" variant="filled" {...props} hiddenLabel={!initialLabel}/>
 })
 
 InputTextField.propTypes = {
@@ -57,7 +54,8 @@ InputTextField.propTypes = {
 }
 
 /*
- * Generic text field component that should be used for most user inputs.
+ * Customized version of Autocomplete with custom NOMAD styling and behaviour.
+ *
  * Defines default behaviour for user input such as clearing inputs when
  * pressing esc and submitting values when pressing enter. Can also display
  * customizable list of suggestions.
@@ -71,6 +69,16 @@ const useInputTextStyles = makeStyles(theme => ({
     flexDirection: 'column',
     boxSizing: 'border-box'
   },
+  popupIndicatorOpen: {
+    transform: 'rotate(180deg)'
+  },
+  adornmentList: {
+    display: 'flex',
+    alignItems: 'center'
+  },
+  adornment: {
+    padding: '3px'
+  },
   listbox: {
       boxSizing: 'border-box',
       '& ul': {
@@ -89,22 +97,28 @@ export const InputText = React.memo(({
   onAccept,
   onSelect,
   onBlur,
+  onFocus,
   onError,
   getOptionLabel,
   groupBy,
   renderOption,
   renderGroup,
+  suggestAllOnFocus,
+  showOpenSuggestions,
   ListboxComponent,
   filterOptions,
   className,
   classes,
   TextFieldProps,
   InputProps,
-  PaperComponent
+  PaperComponent,
+  disableClearable,
+  disableAcceptOnBlur
 }) => {
   const theme = useTheme()
   const styles = useInputTextStyles({classes: classes, theme: theme})
   const [open, setOpen] = useState(false)
+  const [suggestAll, setSuggestAll] = useState(false)
   const disabled = TextFieldProps?.disabled
   // The highlighted item is stored in a ref to keep the component more
   // responsive during browsing the suggestions
@@ -112,8 +126,8 @@ export const InputText = React.memo(({
 
   // Clears the input value and closes suggestions list
   const clearInputValue = useCallback(() => {
-    onError && onError(undefined)
-    onChange && onChange("")
+    onError?.(undefined)
+    onChange?.("")
     setOpen(false)
   }, [onChange, onError])
 
@@ -124,9 +138,9 @@ export const InputText = React.memo(({
 
   // Handle blur
   const handleBlur = useCallback(() => {
-    onBlur && onBlur()
-    onAccept && onAccept(value)
-  }, [onBlur, onAccept, value])
+    onBlur?.()
+    !disableAcceptOnBlur && onAccept?.(value)
+  }, [onBlur, onAccept, value, disableAcceptOnBlur])
 
   // Handles special key presses
   const handleKeyDown = useCallback((event) => {
@@ -145,9 +159,9 @@ export const InputText = React.memo(({
     // or if menu is not open submit the value.
     if (event.key === 'Enter') {
       if (open && highlightRef.current) {
-        onSelect && onSelect(getOptionLabel(highlightRef.current).trim())
+        onSelect?.(getOptionLabel(highlightRef.current).trim())
       } else {
-        onAccept && onAccept(value && value.trim())
+        onAccept?.(value && value.trim())
       }
       event.stopPropagation()
       event.preventDefault()
@@ -158,12 +172,13 @@ export const InputText = React.memo(({
   // Handle input events. Errors are cleaned in input change, regular typing
   // emits onChange, selection with mouse emits onSelect.
   const handleInputChange = useCallback((event, value, reason) => {
+    setSuggestAll(false)
     onError && onError(undefined)
     if (event) {
       if (reason === 'reset') {
-        onSelect && onSelect(value)
+        onSelect?.(value)
       } else {
-        onChange && onChange(value)
+        onChange?.(value)
       }
     }
   }, [onChange, onSelect, onError])
@@ -191,8 +206,12 @@ export const InputText = React.memo(({
       getOptionSelected={(option, value) => false}
       groupBy={groupBy}
       renderGroup={renderGroup}
-      filterOptions={filterOptions}
+      filterOptions={suggestAll
+          ? (opt) => opt
+          : filterOptions
+      }
       renderOption={renderOption}
+      selectOnFocus={true}
       renderInput={(params) => {
         // We need to strip out the styling of the input field that is imposed
         // by Autocomplete. Otherwise the styles enabled by the
@@ -203,25 +222,41 @@ export const InputText = React.memo(({
           size="small"
           helperText={error || undefined}
           error={!!error}
+          onFocus={() => { suggestAllOnFocus && setSuggestAll(true); onFocus?.() } }
           onKeyDown={handleKeyDown}
           InputLabelProps={{shrink}}
           InputProps={{
             ...params.InputProps,
-            endAdornment: (<>
-              {loading ? <CircularProgress color="inherit" size={20} /> : null}
-              {(value?.length || null) && <>
-                <Tooltip title="Clear">
-                  <IconButton
+            endAdornment: (<div className={styles.adornmentList}>
+              {loading ? <CircularProgress color="inherit" size={20} className={styles.adornment} /> : null}
+              {(value?.length && !disableClearable)
+                ? <Tooltip title="Clear">
+                    <IconButton
+                      size="small"
+                      disabled={disabled}
+                      onClick={clearInputValue}
+                      className={styles.iconButton}
+                      aria-label="clear"
+                    >
+                      <CloseIcon/>
+                    </IconButton>
+                  </Tooltip>
+                : null
+              }
+              {(showOpenSuggestions)
+                ? <IconButton
                     size="small"
-                    onClick={clearInputValue}
-                    className={styles.iconButton}
-                    aria-label="clear"
+                    disabled={disabled}
+                    onClick={() => setOpen(old => !old)}
+                    className={clsx(styles.popupIndicator, {
+                      [styles.popupIndicatorOpen]: open
+                    })}
                   >
-                    <CloseIcon/>
-                  </IconButton>
-                </Tooltip>
-              </>}
-            </>),
+                    <ArrowDropDownIcon />
+                </IconButton>
+                : null
+              }
+            </div>),
             ...InputProps
           }}
           {...TextFieldProps}
@@ -241,6 +276,7 @@ InputText.propTypes = {
   onSelect: PropTypes.func, // Triggered when an option is selected from suggestions
   onAccept: PropTypes.func, // Triggered when value should be accepted
   onBlur: PropTypes.func, // Triggered when text goes out of focus
+  onFocus: PropTypes.func, // Triggered when text is focused
   onError: PropTypes.func, // Triggered when any errors should be cleared
   getOptionLabel: PropTypes.func,
   groupBy: PropTypes.func,
@@ -251,12 +287,17 @@ InputText.propTypes = {
   TextFieldProps: PropTypes.object,
   InputProps: PropTypes.object,
   filterOptions: PropTypes.func,
+  disableClearable: PropTypes.bool,
+  disableAcceptOnBlur: PropTypes.bool,
+  suggestAllOnFocus: PropTypes.bool, // Whether to provide all suggestion values when input is focused
+  showOpenSuggestions: PropTypes.bool, // Whether to show button for opening suggestions
   className: PropTypes.string,
   classes: PropTypes.object
 }
 
 InputText.defaultProps = {
-  getOptionLabel: (option) => option.value
+  getOptionLabel: (option) => option.value,
+  showOpenSuggestions: false
 }
 
 /*
diff --git a/gui/src/components/search/widgets/WidgetScatterPlot.js b/gui/src/components/search/widgets/WidgetScatterPlot.js
index 001220c5a15a5caceab76a3016fc42cdc70eb0ad..0b8d175e10a70be927db7de5d1b69d8e33453943 100644
--- a/gui/src/components/search/widgets/WidgetScatterPlot.js
+++ b/gui/src/components/search/widgets/WidgetScatterPlot.js
@@ -40,7 +40,9 @@ import { CropFree, PanTool, Fullscreen, Replay } from '@material-ui/icons'
 import { autorangeDescription } from './WidgetHistogram'
 import { styled } from '@material-ui/core/styles'
 import { DType } from '../../../utils'
-import { Quantity, Unit, useUnits } from '../../../units'
+import { Quantity } from '../../units/Quantity'
+import { Unit } from '../../units/Unit'
+import { useUnitContext } from '../../units/UnitContext'
 
 // Predefined in order to not break memoization
 const dtypesNumeric = new Set([DType.Int, DType.Float])
@@ -91,7 +93,7 @@ export const WidgetScatterPlot = React.memo((
   onSelected
 }) => {
   const styles = useStyles()
-  const units = useUnits()
+  const {units} = useUnitContext()
   const canvas = useRef()
   const [float, setFloat] = useState(false)
   const [loading, setLoading] = useState(true)
diff --git a/gui/src/components/units/Quantity.js b/gui/src/components/units/Quantity.js
new file mode 100644
index 0000000000000000000000000000000000000000..803e884c7a31e9249cebe1c1d3c9507586cbbfd4
--- /dev/null
+++ b/gui/src/components/units/Quantity.js
@@ -0,0 +1,153 @@
+/*
+ * 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 {isNumber, isArray, isNil} from 'lodash'
+import {Unit} from './Unit'
+import {mapDeep} from '../../utils'
+
+/**
+ * Class for persisting persisting a numeric value together with unit
+ * information.
+ */
+export class Quantity {
+  /**
+   * @param {number | n-dimensional array of numbers} value Numeric value. See
+   * also the argument 'normalized'.
+   * @param {str | Unit} unit Unit for the quantity.
+   * @param {boolean} normalized Whether the given numeric value is already
+   * normalized to base units.
+   */
+  constructor(value, unit, normalized = false) {
+    this.unit = new Unit(unit)
+    if (!isNumber(value) && !isArray(value)) {
+      throw Error('Please provide the the value as a number, or as a multidimensional array of numbers.')
+    }
+
+    // This attribute stores the quantity value in 'normalized' form that is
+    // given in the base units (=SI). This value should only be determined once
+    // during the unit initialization and all calls to value() will then lazily
+    // determine the value in the currently set units. This avoids 'drift' in
+    // the value caused by several consecutive changes of the units.
+    this.normalized_value = normalized ? value : this.normalize(value)
+  }
+
+  /**
+   * Get value in current units.
+   * @returns The numeric value in the currently set units.
+   */
+  value() {
+    return this.denormalize(this.normalized_value)
+  }
+
+  /**
+   * Convert value from currently set units to base units.
+   * @param {n-dimensional array} value Value in currently set units.
+   * @returns Value in base units.
+   */
+  normalize(value) {
+    return mapDeep(value, (x) => this.unit.mathjsUnit._normalize(x))
+  }
+
+  /**
+   * Convert value from base units to currently set units.
+   * @param {n-dimensional array} value Value in base units.
+   * @returns Value in currently set units.
+   */
+  denormalize(value) {
+    return mapDeep(value, (x) => this.unit.mathjsUnit._denormalize(x))
+  }
+
+  label() {
+    return this.unit.label()
+  }
+
+  dimension(base) {
+    return this.unit.dimension(base)
+  }
+
+  to(unit) {
+    return new Quantity(this.normalized_value, this.unit.to(unit), true)
+  }
+
+  toSI() {
+    return new Quantity(this.normalized_value, this.unit.toSI(), true)
+  }
+
+  toSystem(system) {
+    return new Quantity(this.normalized_value, this.unit.toSystem(system), true)
+  }
+
+  /**
+   * Checks if the given Quantity is equal to this one.
+   * @param {Quantity} quantity Quantity to compare to
+   * @returns boolean Whether quantities are equal
+   */
+  equal(quantity) {
+    if (quantity instanceof Quantity) {
+      return this.normalized_value === quantity.normalized_value && this.unit.equalBase(quantity.unit)
+    } else {
+      throw Error('The given value is not an instance of Quantity.')
+    }
+  }
+}
+
+/**
+ * Convenience function for parsing value and unit information from a string.
+ *
+ * @param {string} input The input string to parse
+ * @param {boolean} requireValue Whether a value is required.
+ * @param {boolean} requireUnit Whether a unit is required.
+ * @param {string} dimension Dimension for the unit. Nil value means a
+ * dimensionless unit.
+ * @returns Object containing the following properties, if available:
+ *  - value: Numerical value as a number
+ *  - valueString: Numerical value as a string
+ *  - unit: Unit instance
+ *  - unitString: Unit as a string
+ *  - error: Error messsage
+ */
+export function parseQuantity(input, requireValue = true, requireUnit = true, dimension = undefined) {
+  input = input.trim()
+  const valueString = input.match(/^[+-]?((\d+\.\d+|\d+\.|\.\d?|\d+)(e|e\+|e-)\d+|(\d+\.\d+|\d+\.|\.\d?|\d+))?/)?.[0]
+  if (requireValue && isNil(valueString)) {
+    return {error: 'Enter a valid numerical value'}
+  }
+  const value = Number(valueString)
+  const unitString = input.substring(valueString.length).trim()
+  const dim = isNil(dimension) ? 'dimensionless' : dimension
+  if (unitString === '' && dim !== 'dimensionless' && requireUnit) {
+    return {value, valueString, unitString, error: 'Unit is required'}
+  }
+  if (unitString === '' && !requireUnit) {
+    return {value, valueString, unitString}
+  }
+  if (dim === 'dimensionless' && unitString !== '') {
+    return {value, valueString, unitString, error: 'Enter a numerical value without units'}
+  }
+  let unit
+  try {
+    unit = new Unit(dim === 'dimensionless' ? 'dimensionless' : input)
+  } catch {
+    return {valueString, value, unitString, error: `Unit "${unitString}" is not available`}
+  }
+  const inputDim = unit.dimension(false)
+  if (inputDim !== dimension) {
+    return {valueString, value, unitString, unit, error: `Unit "${unitString}" has incompatible dimension`}
+  }
+  return {value, valueString, unit, unitString}
+}
diff --git a/gui/src/components/units/Quantity.spec.js b/gui/src/components/units/Quantity.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..e0a1f225eed595c1df0780b2521b305f5c1e5251
--- /dev/null
+++ b/gui/src/components/units/Quantity.spec.js
@@ -0,0 +1,123 @@
+/*
+ * 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 { Quantity } from './Quantity'
+import { dimensionMap } from './UnitContext'
+
+test('conversion works both ways for each compatible unit', async () => {
+  // Create a list of all possible conversions
+  const conversions = []
+  for (const dimension of Object.values(dimensionMap)) {
+    const units = dimension.units
+    for (const unitA of units) {
+      for (const unitB of units) {
+        conversions.push([unitA, unitB])
+      }
+    }
+  }
+  for (const [unitA, unitB] of conversions) {
+    const a = new Quantity(1, unitA)
+    const b = a.to(unitB)
+    const c = b.to(unitA)
+    expect(a.value()).toBeCloseTo(c.value(), 10)
+  }
+})
+
+test.each([
+  ['same unit', 'kelvin', 'kelvin', 1, 1],
+  ['temperature celsius', 'kelvin', 'celsius', 1, -272.15],
+  ['temperature fahrenheit', 'kelvin', 'fahrenheit', 1, -457.87],
+  ['abbreviated name', 'J', 'eV', 1, 6241509074460763000],
+  ['full name', 'joule', 'electron_volt', 1, 6241509074460763000],
+  ['division', 'm/s', 'angstrom/femtosecond', 1, 0.00001],
+  ['multiplication', 'm*s', 'angstrom*femtosecond', 1, 9.999999999999999e+24],
+  ['power with hat', 'm^2', 'angstrom^2', 1, 99999999999999980000],
+  ['power with double asterisk (single)', 'm**2', 'angstrom**2', 1, 99999999999999980000],
+  ['power with double asterisk (multiple)', 'm**2 / s**2', 'angstrom**2 / ms**2', 1, 99999999999999.98],
+  ['explicit delta (single)', 'delta_celsius', 'delta_K', 1, 274.15],
+  ['explicit delta (multiple)', 'delta_celsius / delta_celsius', 'delta_K / delta_K', 1, 1],
+  ['explicit delta symbol (single)', 'Δcelsius', 'ΔK', 1, 274.15],
+  ['explicit delta symbol (multiple)', 'Δcelsius / Δcelsius', 'ΔK / ΔK', 1, 1],
+  ['combined', 'm*m/s^2', 'angstrom^2/femtosecond^2', 1, 9.999999999999999e-11],
+  ['negative exponent', 's^-2', 'femtosecond^-2', 1, 1e-30],
+  ['simple to complex with one unit', 'N', 'kg*m/s^2', 1, 1],
+  ['complex to simple with one unit', 'kg*m/s^2', 'N', 1, 1],
+  ['simple to complex with expression', 'N/m', 'kg/s^2', 1, 1],
+  ['complex to simple with expression', 'kg/s^2', 'N/m', 1, 1],
+  ['unit starting with a number', '1/minute', '1/second', 1, 0.016666666666666666]
+]
+)('test conversion with "to()": %s', async (name, unitA, unitB, valueA, valueB) => {
+  const a = new Quantity(valueA, unitA)
+  const b = a.to(unitB)
+  expect(b.value()).toBeCloseTo(valueB, 10)
+})
+
+test.each([
+  ['conversion with single unit', 'meter', {length: {definition: 'angstrom'}}, 1, 1e10],
+  ['conversion with power', 'meter^2', {length: {definition: 'angstrom'}}, 1, 99999999999999980000],
+  ['do not simplify', 'gram*angstrom/fs^2', {mass: {definition: 'kilogram'}, length: {definition: 'meter'}, time: {definition: 'second'}}, 1, 99999999999999980],
+  ['do not convert to base', 'eV', {energy: {definition: 'joule'}}, 1, 1.602176634e-19],
+  ['combination', 'a_u_force * angstrom', {force: {definition: 'newton'}, length: {definition: 'meter'}}, 1, 8.23872349823899e-18],
+  ['use base units if derived unit not defined in system', 'newton * meter', {mass: {definition: 'kilogram'}, time: {definition: 'second'}, length: {definition: 'meter'}}, 1, 1],
+  ['unit definition with prefix', 'kg^2', {mass: {definition: 'mg'}}, 1, 1e12],
+  ['expression as definition', 'N', {force: {definition: '(kg m) / s^2'}}, 1, 1]
+]
+)('test conversion with "toSystem()": %s', async (name, unit, system, valueA, valueB) => {
+  const a = new Quantity(valueA, unit)
+  const b = a.toSystem(system)
+  expect(b.value()).toBeCloseTo(valueB, 10)
+})
+
+test.each([
+  ['celsius to kelvin', 'celsius', 'kelvin', 5, 278.15],
+  ['fahrenheit to kelvin', 'fahrenheit', 'kelvin', 5, 258.15],
+  ['celsius to fahrenheit', 'celsius', 'fahrenheit', 5, 41],
+  ['celsius to kelvin: derived unit (implicit delta)', 'joule/celsius', 'joule/kelvin', 5, 5],
+  ['celsius to kelvin: derived unit (explicit delta)', 'joule/delta_celsius', 'joule/kelvin', 5, 5],
+  ['fahrenheit to kelvin: derived unit (offset not applied)', 'joule/fahrenheit', 'joule/kelvin', 5, 9 / 5 * 5],
+  ['celsius to fahrenheit: derived unit (offset not applied)', 'joule/celsius', 'joule/fahrenheit', 5, 5 / 9 * 5]
+]
+)('test temperature conversion": %s', async (name, unitA, unitB, valueA, valueB) => {
+  const a = new Quantity(valueA, unitA)
+  const b = a.to(unitB)
+  const c = b.to(unitA)
+  expect(b.value()).toBeCloseTo(valueB, 10)
+  expect(c.value()).toBeCloseTo(valueA, 10)
+})
+
+test.each([
+  [0],
+  [1],
+  [2],
+  [3]
+]
+)('test different value dimensions: %sD', async (dimension) => {
+  let value = 1
+  for (let i = 0; i < dimension; ++i) {
+    value = [value]
+  }
+  const a = new Quantity(value, 'angstrom')
+  const b = a.to('nanometer')
+  let valueA = a.value()
+  let valueB = b.value()
+  for (let i = 0; i < dimension; ++i) {
+    valueA = valueA[0]
+    valueB = valueB[0]
+  }
+  expect(valueA).toBeCloseTo(10 * valueB)
+})
diff --git a/gui/src/components/units/Unit.js b/gui/src/components/units/Unit.js
new file mode 100644
index 0000000000000000000000000000000000000000..d0a3e25307dd85036bcce5edcf0641a3a79e9cea
--- /dev/null
+++ b/gui/src/components/units/Unit.js
@@ -0,0 +1,364 @@
+/*
+ * 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 {isNil, has, isString} from 'lodash'
+import {Unit as UnitMathJS} from 'mathjs'
+import {unitToAbbreviationMap} from './UnitContext'
+
+/**
+ * Helper class for persisting unit information.
+ *
+ * Builds upon the math.js Unit class system, but adds additional functionality,
+ * including:
+ *  - Ability to convert to any unit system given as an argument
+ *  - Abbreviated labels for dense formatting
+ */
+export class Unit {
+  /**
+   * @param {str | Unit} unit Unit for the quantity.
+   */
+  constructor(unit) {
+    if (isString(unit)) {
+      unit = this.normalizeExpression(unit)
+      unit = new UnitMathJS(undefined, unit)
+    } else if (unit instanceof Unit) {
+      unit = unit.mathjsUnit.clone()
+    } else if (unit instanceof UnitMathJS) {
+      unit = unit.clone()
+    } else {
+      throw Error('Please provide the unit as a string or as an instance of Unit.')
+    }
+    this.mathjsUnit = unit
+    // this._labelabbreviate = undefined
+    // this._label = undefined
+  }
+
+  /**
+   * Normalizes the given expression into a format that can be parsed by MathJS.
+   *
+   * This function will replace the Pint power symbol of '**' with the symbol
+   * '^' used by MathJS. In addition, we convert any 'delta'-units (see:
+   * https://pint.readthedocs.io/en/stable/nonmult.html) into their regular
+   * counterparts: MathJS will automatically ignore the offset when using
+   * non-multiplicative units in expressions.
+   *
+   * @param {str} expression Expression
+   * @returns string Expression in normalized form
+   */
+  normalizeExpression(expression) {
+    let normalized = expression.replace(/\*\*/g, '^')
+    normalized = normalized.replace(/delta_/g, '')
+    normalized = normalized.replace(/Δ/g, '')
+    return normalized
+  }
+
+  /**
+   * Checks if the given unit has the same base dimensions as this one.
+   * @param {str | Unit} unit Unit to compare to
+   * @returns boolean Whether the units have the same base dimensions.
+   */
+  equalBase(unit) {
+    if (isString(unit)) {
+      unit = this.normalizeExpression(unit)
+      unit = new Unit(unit)
+    }
+    return this.mathjsUnit.equalBase(unit.mathjsUnit)
+  }
+
+  /**
+   * Used to create a human-readable description of the unit as a string.
+   *
+   * @param {bool} abbreviate Whether to abbreviate the label using the
+   * abbreviations for each unit and prefix. If false, the original unit names
+   * (as given or defined by the unit system) are used.
+   * @returns A string representing the unit.
+   */
+  label(abbreviate = true) {
+    // TODO: The label caching is disabled for now. Because Quantities are
+    // stored as recoil.js atoms, they become immutable which causes problems
+    // with internal state mutation.
+    // if (this._labelabbreviate === abbreviate && this._label) {
+    //   return this._label
+    // }
+    const units = this.mathjsUnit.units
+    let strNum = ''
+    let strDen = ''
+    let nNum = 0
+    let nDen = 0
+
+    function getName(unit) {
+      if (unit.base.key === 'dimensionless') return ''
+      return abbreviate
+        ? unitToAbbreviationMap?.[unit.name] || unit.name
+        : unit.name
+    }
+
+    function getPrefix(unit, original) {
+      if (!abbreviate) return original
+      const prefixMap = {
+        // SI
+        deca: 'da',
+        hecto: 'h',
+        kilo: 'k',
+        mega: 'M',
+        giga: 'G',
+        tera: 'T',
+        peta: 'P',
+        exa: 'E',
+        zetta: 'Z',
+        yotta: 'Y',
+        deci: 'd',
+        centi: 'c',
+        milli: 'm',
+        micro: 'u',
+        nano: 'n',
+        pico: 'p',
+        femto: 'f',
+        atto: 'a',
+        zepto: 'z',
+        yocto: 'y',
+        // IEC
+        kibi: 'Ki',
+        mebi: 'Mi',
+        gibi: 'Gi',
+        tebi: 'Ti',
+        pebi: 'Pi',
+        exi: 'Ei',
+        zebi: 'Zi',
+        yobi: 'Yi'
+      }
+      return prefixMap?.[original] || original
+    }
+
+    for (let i = 0; i < units.length; i++) {
+      if (units[i].power > 0) {
+        nNum++
+        const prefix = getPrefix(units[i].unit.name, units[i].prefix.name)
+        const name = getName(units[i].unit)
+        strNum += ` ${prefix}${name}`
+        if (Math.abs(units[i].power - 1.0) > 1e-15) {
+          strNum += '^' + units[i].power
+        }
+      } else if (units[i].power < 0) {
+        nDen++
+      }
+    }
+
+    if (nDen > 0) {
+      for (let i = 0; i < units.length; i++) {
+        if (units[i].power < 0) {
+          const prefix = getPrefix(units[i].unit.name, units[i].prefix.name)
+          const name = getName(units[i].unit)
+          if (nNum > 0) {
+            strDen += ` ${prefix}${name}`
+            if (Math.abs(units[i].power + 1.0) > 1e-15) {
+              strDen += '^' + (-units[i].power)
+            }
+          } else {
+            strDen += ` ${prefix}${name}`
+            strDen += '^' + (units[i].power)
+          }
+        }
+      }
+    }
+    // Remove leading whitespace
+    strNum = strNum.substr(1)
+    strDen = strDen.substr(1)
+
+    // Add parentheses for better copy/paste back into evaluate, for example, or
+    // for better pretty print formatting
+    if (nNum > 1 && nDen > 0) {
+      strNum = '(' + strNum + ')'
+    }
+    if (nDen > 1 && nNum > 0) {
+      strDen = '(' + strDen + ')'
+    }
+
+    let str = strNum
+    if (nNum > 0 && nDen > 0) {
+      str += ' / '
+    }
+    str += strDen
+
+    // this._labelabbreviate = abbreviate
+    // this._label = str
+    return str
+  }
+
+  /**
+   * Gets the dimension of this unit as a string. The order of the dimensions is
+   * fixed (determined at unit registration time).
+   *
+   * @param {boolean} base Whether to return dimension in base units. Otherwise
+   * the original unit dimensions are used.
+   * @returns The dimensionality as a string, e.g. 'time^2 energy mass^-2'
+   */
+  dimension(base = true) {
+    const dimensions = Object.keys(UnitMathJS.BASE_UNITS)
+    const dimensionMap = Object.fromEntries(dimensions.map(name => [name, 0]))
+
+    if (base) {
+      const BASE_DIMENSIONS = UnitMathJS.BASE_DIMENSIONS
+      for (let i = 0; i < BASE_DIMENSIONS.length; ++i) {
+        const power = this?.mathjsUnit.dimensions?.[i]
+        if (power) {
+          dimensionMap[BASE_DIMENSIONS[i]] += power
+        }
+      }
+    } else {
+      for (const unit of this?.mathjsUnit.units) {
+        const power = unit.power
+        if (power) {
+          dimensionMap[unit.unit.base.key] += power
+        }
+      }
+    }
+    return Object.entries(dimensionMap)
+      .filter(d => d[1] !== 0)
+      .map(d => `${d[0]}${((d[1] < 0 || d[1] > 1) && `^${d[1]}`) || ''}`).join(' ')
+  }
+
+  /**
+   * Function for converting to another unit.
+   *
+   * @param {str | Unit} unit The target unit
+   * @returns A new Unit expressed in the given units.
+   */
+  to(unit) {
+    if (isString(unit)) {
+      unit = this.normalizeExpression(unit)
+    } else if (unit instanceof Unit) {
+      unit = unit.label()
+    } else {
+      throw Error('Unknown unit type. Please provide the unit as as string or as instance of Unit.')
+    }
+
+    // We cannot directly feed the unit string into Math.js, because it will try
+    // to parse units like 1/<unit> as Math.js units which have values, and then
+    // will raise an exception when converting between valueless and valued
+    // unit. The workaround is to explicitly define a valueless unit.
+    unit = new UnitMathJS(undefined, unit)
+    return new Unit(this.mathjsUnit.to(unit))
+  }
+
+  /**
+   * Function for converting the value of this Unit to the SI unit system.
+   *
+   * @returns A new Unit instance in the SI unit system.
+   */
+  toSI() {
+    return this.toSystem({
+      "dimensionless": { "definition": "dimensionless" },
+      "length": { "definition": "m" },
+      "mass": { "definition": "kg" },
+      "time": { "definition": "s" },
+      "current": { "definition": "A" },
+      "temperature": { "definition": "K" },
+      "luminosity": { "definition": "cd" },
+      "luminous_flux": { "definition": "lm" },
+      "substance": { "definition": "mol" },
+      "angle": { "definition": "rad" },
+      "information": { "definition": "bit" },
+      "force": { "definition": "N" },
+      "energy": { "definition": "J" },
+      "power": { "definition": "W" },
+      "pressure": { "definition": "Pa" },
+      "charge": { "definition": "C" },
+      "resistance": { "definition": "Ω" },
+      "conductance": { "definition": "S" },
+      "inductance": { "definition": "H" },
+      "magnetic_flux": { "definition": "Wb" },
+      "magnetic_field": { "definition": "T" },
+      "frequency": { "definition": "Hz" },
+      "luminance": { "definition": "nit" },
+      "illuminance": { "definition": "lx" },
+      "electric_potential": { "definition": "V" },
+      "capacitance": { "definition": "F" },
+      "activity": { "definition": "kat" }
+    })
+  }
+
+  /**
+   * Function for converting the value of this unit to another unit system.
+   *
+   * Notice that converting a unit to another unit system is not as easy as
+   * conversions to a specific unit. When converting to a specific unit one can
+   * simply check that the dimensions match and go ahead with the conversion.
+   * With unit systems, there can be multiple alternative forms, and choosing a
+   * good one is more difficult. E.g. should 'a_u_force * angstrom' be converted
+   * into:
+   *
+   * a) N m
+   * b) J
+   * c) (kg m^2) / s^2
+   *
+   * By default this function will try to preserve the original unit dimensions
+   * and not convert everything down to base units. If a derived unit is not
+   * present, it will, however, attempt to convert it to the base units. Any
+   * further simplication is not performed.
+   *
+   * @param {object} system The target unit system.
+   * @returns A new Unit instance in the given system.
+   */
+  toSystem(system) {
+    // Go through the currently defined units, identify their dimension and look
+    // for the corresponding dimension in the given unit system. If one is
+    // present, convert to it. Otherwise convert to base dimensions.
+    const UNITS = UnitMathJS.UNITS
+    const PREFIXES = UnitMathJS.PREFIXES
+    const BASE_DIMENSIONS = UnitMathJS.BASE_DIMENSIONS
+    const BASE_UNITS = UnitMathJS.BASE_UNITS
+    const proposedUnitList = []
+    for (const unit of this.mathjsUnit.units) {
+      const dimension = unit.unit.base.key
+      const newUnitDefinition = system?.[dimension]?.definition
+      // If the unit for this dimension is defined, use it
+      if (!isNil(newUnitDefinition)) {
+        const newUnit = new Unit(newUnitDefinition)
+        for (const unitDef of newUnit.mathjsUnit.units) {
+          proposedUnitList.push({...unitDef, power: unitDef.power * unit.power})
+        }
+      // Otherwise convert to base units
+      } else {
+        let missingBaseDim = false
+        const baseUnit = BASE_UNITS[dimension]
+        const newDimensions = baseUnit.dimensions
+        for (let i = 0; i < BASE_DIMENSIONS.length; i++) {
+          const baseDim = BASE_DIMENSIONS[i]
+          if (Math.abs(newDimensions[i] || 0) > 1e-12) {
+            if (has(system, baseDim)) {
+              proposedUnitList.push({
+                unit: UNITS[system[baseDim].definition],
+                prefix: PREFIXES.NONE[''],
+                power: unit.power ? newDimensions[i] * unit.power : 0
+              })
+            } else {
+              missingBaseDim = true
+            }
+          }
+        }
+        if (missingBaseDim) {
+          throw Error(`The given unit system does not contain the required unit definitions for converting ${unit.name} with dimension ${dimension}.`)
+        }
+      }
+    }
+
+    const ret = this.mathjsUnit.clone()
+    ret.units = proposedUnitList
+    return new Unit(ret)
+  }
+}
diff --git a/gui/src/units.spec.js b/gui/src/components/units/Unit.spec.js
similarity index 59%
rename from gui/src/units.spec.js
rename to gui/src/components/units/Unit.spec.js
index 15255523f19d58a94bda03adcdb0a870189bf79f..851b28246b3e9489a5daee9631e5c9b67a5ebb91 100644
--- a/gui/src/units.spec.js
+++ b/gui/src/components/units/Unit.spec.js
@@ -17,24 +17,25 @@
  */
 
 import { Unit as UnitMathJS } from 'mathjs'
-import { Quantity, dimensionMap, unitMap } from './units'
+import { Unit } from './Unit'
+import { unitMap } from './UnitContext'
 
 test('each unit can be created using its full name, alias or short form (+ all available prefixes)', async () => {
   for (const [name, def] of Object.entries(unitMap)) {
     // Full name + prefixes
-    expect(new Quantity(1, name)).not.toBeNaN()
+    expect(new Unit(name)).not.toBeNaN()
     if (def.prefixes) {
       for (const prefix of Object.keys(UnitMathJS.PREFIXES[def.prefixes.toUpperCase()])) {
-        expect(new Quantity(1, `${prefix}${name}`)).not.toBeNaN()
+        expect(new Unit(`${prefix}${name}`)).not.toBeNaN()
       }
     }
     // Aliases + prefixes
     if (def.aliases) {
       for (const alias of def.aliases) {
-        expect(new Quantity(1, alias)).not.toBeNaN()
+        expect(new Unit(alias)).not.toBeNaN()
         if (def.prefixes) {
           for (const prefix of Object.keys(UnitMathJS.PREFIXES[def.prefixes.toUpperCase()])) {
-            expect(new Quantity(1, `${prefix}${alias}`)).not.toBeNaN()
+            expect(new Unit(`${prefix}${alias}`)).not.toBeNaN()
           }
         }
       }
@@ -42,25 +43,6 @@ test('each unit can be created using its full name, alias or short form (+ all a
   }
 })
 
-test('unit conversion works both ways for each compatible unit', async () => {
-  // Create a list of all possible conversions
-  const conversions = []
-  for (const dimension of Object.values(dimensionMap)) {
-    const units = dimension.units
-    for (const unitA of units) {
-      for (const unitB of units) {
-        conversions.push([unitA, unitB])
-      }
-    }
-  }
-  for (const [unitA, unitB] of conversions) {
-    const a = new Quantity(1, unitA)
-    const b = a.to(unitB)
-    const c = b.to(unitA)
-    expect(a.value()).toBeCloseTo(c.value(), 10)
-  }
-})
-
 test.each([
   ['dimensionless', 'dimensionless', ''],
   ['non-abbreviated', 'celsius', '°C'],
@@ -76,10 +58,24 @@ test.each([
   ['chain', 'meter*meter/second^2', '(m m) / s^2']
 ]
 )('label abbreviation: %s', async (name, unit, label) => {
-  const a = new Quantity(1, unit)
+  const a = new Unit(unit)
   expect(a.label()).toBe(label)
 })
 
+test.each([
+  ['dimensionless', 'dimensionless', 'dimensionless'],
+  ['single unit', 'meter', 'length'],
+  ['fixed order 1', 'meter * second', 'length time'],
+  ['fixed order 2', 'second * meter', 'length time'],
+  ['power', 'meter^3 * second^-1', 'length^3 time^-1'],
+  ['in derived', 'joule', 'energy', false],
+  ['in base', 'joule', 'mass length^2 time^-2']
+]
+)('test getting dimension": %s', async (name, unit, dimension, base = true) => {
+  const a = new Unit(unit)
+  expect(a.dimension(base)).toBe(dimension)
+})
+
 test.each([
   ['same unit', 'kelvin', 'kelvin', 'K'],
   ['temperature celsius', 'kelvin', 'celsius', '°C'],
@@ -104,80 +100,27 @@ test.each([
   ['unit starting with a number', '1/minute', '1/second', 's^-1']
 ]
 )('test conversion with "to()": %s', async (name, unitA, unitB, labelB) => {
-  const a = new Quantity(1, unitA)
+  const a = new Unit(unitA)
   const b = a.to(unitB)
-  expect(b.value()).not.toBeNaN()
   expect(b.label()).toBe(labelB)
 })
 
 test.each([
-  ['conversion with single unit', 'meter', {length: {name: 'angstrom'}}, 'Å'],
-  ['conversion with power', 'meter^2', {length: {name: 'angstrom'}}, 'Å^2'],
-  ['do not simplify', 'gram*angstrom/fs^2', {mass: {name: 'kilogram'}, length: {name: 'meter'}, time: {name: 'second'}}, '(kg m) / s^2'],
-  ['do not convert to base', 'eV', {energy: {name: 'joule'}}, 'J'],
-  ['combination', 'a_u_force * angstrom', {force: {name: 'newton'}, length: {name: 'meter'}}, 'N m'],
-  ['use base units if derived unit not defined in system', 'newton * meter', {mass: {name: 'kilogram'}, time: {name: 'second'}, length: {name: 'meter'}}, '(kg m m) / s^2']
+  ['conversion with single unit', 'meter', {length: {definition: 'angstrom'}}, 'Å'],
+  ['conversion with power', 'meter^2', {length: {definition: 'angstrom'}}, 'Å^2'],
+  ['do not simplify', 'gram*angstrom/fs^2', {mass: {definition: 'kilogram'}, length: {definition: 'meter'}, time: {definition: 'second'}}, '(kg m) / s^2'],
+  ['do not convert to base', 'eV', {energy: {definition: 'joule'}}, 'J'],
+  ['combination', 'a_u_force * angstrom', {force: {definition: 'newton'}, length: {definition: 'meter'}}, 'N m'],
+  ['use base units if derived unit not defined in system', 'newton * meter', {mass: {definition: 'kilogram'}, time: {definition: 'second'}, length: {definition: 'meter'}}, '(kg m m) / s^2'],
+  ['unit definition with prefix', 'kg^2', {mass: {definition: 'mg'}}, 'mg^2'],
+  ['expression as definition', 'N', {force: {definition: '(kg m) / s^2'}}, '(kg m) / s^2']
 ]
 )('test conversion with "toSystem()": %s', async (name, unit, system, label) => {
-  const a = new Quantity(1, unit)
+  const a = new Unit(unit)
   const b = a.toSystem(system)
-  expect(b.value()).not.toBeNaN()
   expect(b.label()).toBe(label)
 })
 
-test.each([
-  ['dimensionless', 'dimensionless', 'dimensionless'],
-  ['single unit', 'meter', 'length'],
-  ['fixed order 1', 'meter * second', 'length time'],
-  ['fixed order 2', 'second * meter', 'length time'],
-  ['power', 'meter^3 * second^-1', 'length^3 time^-1'],
-  ['in derived', 'joule', 'energy', false],
-  ['in base', 'joule', 'mass length^2 time^-2']
-]
-)('test getting dimension": %s', async (name, unit, dimension, base = true) => {
-  const a = new Quantity(1, unit)
-  expect(a.dimension(base)).toBe(dimension)
-})
-
-test.each([
-  ['celsius to kelvin', 'celsius', 'kelvin', 5, 278.15],
-  ['fahrenheit to kelvin', 'fahrenheit', 'kelvin', 5, 258.15],
-  ['celsius to fahrenheit', 'celsius', 'fahrenheit', 5, 41],
-  ['celsius to kelvin: derived unit (implicit delta)', 'joule/celsius', 'joule/kelvin', 5, 5],
-  ['celsius to kelvin: derived unit (explicit delta)', 'joule/delta_celsius', 'joule/kelvin', 5, 5],
-  ['fahrenheit to kelvin: derived unit (offset not applied)', 'joule/fahrenheit', 'joule/kelvin', 5, 9 / 5 * 5],
-  ['celsius to fahrenheit: derived unit (offset not applied)', 'joule/celsius', 'joule/fahrenheit', 5, 5 / 9 * 5]
-]
-)('test temperature conversion": %s', async (name, unitA, unitB, valueA, valueB) => {
-  const a = new Quantity(valueA, unitA)
-  const b = a.to(unitB)
-  const c = b.to(unitA)
-  expect(b.value()).toBeCloseTo(valueB, 10)
-  expect(c.value()).toBeCloseTo(valueA, 10)
-})
-
-test.each([
-  [0],
-  [1],
-  [2],
-  [3]
-]
-)('test different value dimensions: %sD', async (dimension) => {
-  let value = 1
-  for (let i = 0; i < dimension; ++i) {
-    value = [value]
-  }
-  const a = new Quantity(value, 'angstrom')
-  const b = a.to('nanometer')
-  let valueA = a.value()
-  let valueB = b.value()
-  for (let i = 0; i < dimension; ++i) {
-    valueA = valueA[0]
-    valueB = valueB[0]
-  }
-  expect(valueA).toBeCloseTo(10 * valueB)
-})
-
 test.each([
   ['incompatible dimensions', 'm', 'J'],
   ['wrong power of the correct dimension', 'm', 'm^2'],
@@ -187,6 +130,6 @@ test.each([
 ]
 )('invalid conversions with "to()": %s', async (name, unitA, unitB) => {
   expect(() => {
-    new Quantity(1, unitA).to(unitB)
+    new Unit(unitA).to(unitB)
   }).toThrow()
 })
diff --git a/gui/src/components/units/UnitContext.js b/gui/src/components/units/UnitContext.js
new file mode 100644
index 0000000000000000000000000000000000000000..5fff965cb2d0e27b6295eaccef38bd1130546f90
--- /dev/null
+++ b/gui/src/components/units/UnitContext.js
@@ -0,0 +1,157 @@
+/*
+ * 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, { useState, useMemo, useContext, useCallback } from 'react'
+import PropTypes from 'prop-types'
+import { isNil, isFunction, startCase, toLower, cloneDeep } from 'lodash'
+import { Unit as UnitMathJS, createUnit } from 'mathjs'
+import { unitList, unitPrefixes as prefixes } from '../../config'
+
+// Delete all units and prefixes that come by default with Math.js. This way
+// they cannot be intermixed with the NOMAD units. Notice that we have to clear
+// them in place: they are defined as const.
+Object.keys(UnitMathJS.UNITS).forEach(name => UnitMathJS.deleteUnit(name))
+UnitMathJS.BASE_DIMENSIONS.splice(0, UnitMathJS.BASE_DIMENSIONS.length)
+Object.getOwnPropertyNames(UnitMathJS.BASE_UNITS).forEach(function(prop) {
+  delete UnitMathJS.BASE_UNITS[prop]
+})
+Object.getOwnPropertyNames(UnitMathJS.PREFIXES).forEach(function(prop) {
+  delete UnitMathJS.PREFIXES[prop]
+})
+UnitMathJS.PREFIXES.NONE = {'': { name: '', value: 1, scientific: true }}
+UnitMathJS.PREFIXES.PINT = prefixes
+
+// Customize the unit parsing to allow certain special symbols
+const isAlphaOriginal = UnitMathJS.isValidAlpha
+const isSpecialChar = function(c) {
+  const specialChars = new Set(['_', 'Å', 'Å', 'å', '°', 'µ', 'ö', 'é', '∞'])
+  return specialChars.has(c)
+}
+const isGreekLetter = function(c) {
+  const charCode = c.charCodeAt(0)
+  return (charCode > 912 && charCode < 970)
+}
+UnitMathJS.isValidAlpha = function(c) {
+  return isAlphaOriginal(c) || isSpecialChar(c) || isGreekLetter(c)
+}
+
+// Create MathJS unit definitions from the data exported by 'nomad dev units'
+export const unitToAbbreviationMap = {}
+const unitDefinitions = {}
+for (let def of unitList) {
+  const name = def.name
+  def = {
+    ...def,
+    baseName: def.dimension,
+    prefixes: 'pint'
+  }
+  unitDefinitions[name] = def
+  if (def.abbreviation) {
+    unitToAbbreviationMap[name] = def.abbreviation
+    if (def.aliases) {
+      for (const alias of def.aliases) {
+        unitToAbbreviationMap[alias] = def.abbreviation
+      }
+    }
+  }
+}
+createUnit(unitDefinitions, {override: true})
+
+// Export unit options for each unit and dimension
+export const unitMap = Object.fromEntries(unitList.map(x => [x.name, x]))
+export const dimensionMap = {}
+for (const def of unitList) {
+  const name = def.name
+  const dimension = def.dimension
+  if (isNil(dimension)) {
+    continue
+  }
+  const oldInfo = dimensionMap[dimension] || {
+    label: startCase(toLower(dimension.replace('_', ' ')))
+  }
+  const oldList = oldInfo.units || []
+  oldList.push(name)
+  oldInfo.units = oldList
+  dimensionMap[dimension] = oldInfo
+}
+
+/**
+ * Convenience function for getting compatible units for a given dimension.
+ * Returns all compatible units that have been registered.
+ *
+ * @param {string} dimension The dimension.
+ * @returns Array of compatible units.
+ */
+export function getUnits(dimension) {
+  return dimensionMap?.[dimension]?.units || []
+}
+
+/**
+ * React context for interacting with unit configurations.
+ */
+export const unitContext = React.createContext()
+export const UnitProvider = React.memo(({initialUnitSystems, initialSelected, children}) => {
+  const resetUnitSystems = useState(cloneDeep(initialUnitSystems))[0]
+  const [unitSystems, setUnitSystems] = useState(cloneDeep(initialUnitSystems))
+  const [selected, setSelected] = useState(initialSelected)
+
+  const reset = useCallback(() => {
+    setUnitSystems(cloneDeep(resetUnitSystems))
+  }, [resetUnitSystems])
+
+  const values = useMemo(() => {
+    return {
+      units: unitSystems[selected].units,
+      setUnits: (value) => {
+        setUnitSystems(old => {
+          const newSystems = {...old}
+          newSystems[selected].units = isFunction(value)
+            ? value(newSystems[selected].units)
+            : value
+          return newSystems
+        })
+      },
+      unitSystems,
+      unitMap,
+      dimensionMap,
+      selected,
+      setSelected,
+      reset
+    }
+  }, [unitSystems, selected, reset])
+
+  return <unitContext.Provider value={values}>
+    {children}
+  </unitContext.Provider>
+})
+
+UnitProvider.propTypes = {
+  initialUnitSystems: PropTypes.object,
+  initialSelected: PropTypes.string,
+  children: PropTypes.node
+}
+
+/**
+ * Convenience hook for using the current unit context.
+ *
+ * @returns Object containing the currently set units for each dimension (e.g.
+ * {energy: 'joule'})
+ */
+export const useUnitContext = () => {
+  return useContext(unitContext)
+}
diff --git a/gui/src/components/units/UnitContext.spec.js b/gui/src/components/units/UnitContext.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..3654a17d47761c06f1b3b9ab5fae10aaddf9c729
--- /dev/null
+++ b/gui/src/components/units/UnitContext.spec.js
@@ -0,0 +1,57 @@
+/*
+ * 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 { renderHook, act } from '@testing-library/react-hooks'
+import { UnitProvider, useUnitContext } from './UnitContext'
+
+const wrapper = ({ children }) => <UnitProvider
+  initialUnitSystems={{
+    SI: {units: {'length': {'definition': 'meter'}}},
+    AU: {units: {'length': {'definition': 'meter'}}}
+  }}
+  initialSelected='SI'
+>
+  {children}
+</UnitProvider>
+
+test('the initial selection is returned correctly', () => {
+  const { result } = renderHook(() => useUnitContext(), { wrapper })
+  expect(result.current.selected).toBe('SI')
+})
+
+test('updating unit system selection works', () => {
+  const { result } = renderHook(() => useUnitContext(), { wrapper })
+  act(() => {
+    result.current.setSelected('AU')
+  })
+  expect(result.current.selected).toBe('AU')
+})
+
+test('the initial units are returned correctly', () => {
+  const { result } = renderHook(() => useUnitContext(), { wrapper })
+  expect(result.current.units.length.definition).toBe('meter')
+})
+
+test('updating units works', () => {
+  const { result } = renderHook(() => useUnitContext(), { wrapper })
+  act(() => {
+    result.current.setUnits(old => ({...old, length: {definition: 'mm'}}))
+  })
+  expect(result.current.units.length.definition).toBe('mm')
+})
diff --git a/gui/src/components/units/UnitDimensionSelect.js b/gui/src/components/units/UnitDimensionSelect.js
new file mode 100644
index 0000000000000000000000000000000000000000..aa7fdbba778f794e77015bd86f6fd267614b24cb
--- /dev/null
+++ b/gui/src/components/units/UnitDimensionSelect.js
@@ -0,0 +1,90 @@
+/*
+ * 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, { useState, useEffect, useCallback, useRef } from 'react'
+import PropTypes from 'prop-types'
+import {useUnitContext} from './UnitContext'
+import UnitInput from './UnitInput'
+
+/**
+ * Controls the unit for the specified dimension in the current unit context.
+ */
+const UnitDimensionSelect = React.memo(({label, dimension, onChange, disabled}) => {
+  const {units, setUnits} = useUnitContext()
+  const [error, setError] = useState()
+  const unit = units?.[dimension]
+  const disabledFinal = disabled || unit?.locked
+  const labelFinal = label || dimension
+  const oldValue = useRef(unit?.definition)
+  const [inputValue, setInputValue] = useState(unit?.definition)
+
+  // React to changes in units
+  useEffect(() => {
+    setError(undefined)
+    setInputValue(unit?.definition)
+  }, [unit, dimension])
+
+  const handleAccept = useCallback((unit, unitString) => {
+    setUnits(old => {
+      const newUnits = {
+        ...old,
+        [dimension]: {...old[dimension], definition: unitString}
+      }
+      return newUnits
+    })
+    onChange?.(unit)
+    oldValue.current = unitString
+  }, [dimension, onChange, setUnits])
+
+  const handleSelect = useCallback((unit, unitString) => {
+    handleAccept(unit, unit.label())
+  }, [handleAccept])
+
+  const handleBlur = useCallback((onAccept) => {
+    onAccept(inputValue)
+  }, [inputValue])
+
+  const handleChange = useCallback((value) => {
+    oldValue.current = value
+    setInputValue(value)
+  }, [])
+
+  return (unit && dimension !== 'dimensionless')
+    ? <UnitInput
+      value={inputValue}
+      label={labelFinal}
+      onChange={handleChange}
+      onAccept={handleAccept}
+      onSelect={handleSelect}
+      onBlur={handleBlur}
+      onError={setError}
+      dimension={dimension}
+      error={error}
+      disabled={disabledFinal}
+      disableGroup
+    />
+    : null
+})
+UnitDimensionSelect.propTypes = {
+  value: PropTypes.string,
+  label: PropTypes.string,
+  dimension: PropTypes.string,
+  onChange: PropTypes.func,
+  disabled: PropTypes.bool
+}
+
+export default UnitDimensionSelect
diff --git a/gui/src/components/units/UnitInput.js b/gui/src/components/units/UnitInput.js
new file mode 100644
index 0000000000000000000000000000000000000000..d8cc5a41da7e9216df39d33731959fd8006ee53c
--- /dev/null
+++ b/gui/src/components/units/UnitInput.js
@@ -0,0 +1,239 @@
+/*
+ * 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, useEffect, useMemo, useRef, useContext, createContext} from 'react'
+import PropTypes from 'prop-types'
+import {getSuggestions} from '../../utils'
+import {unitMap} from './UnitContext'
+import {parseQuantity} from './Quantity'
+import {List, ListItemText, ListSubheader, makeStyles} from '@material-ui/core'
+import {VariableSizeList} from 'react-window'
+import {InputText} from '../search/input/InputText'
+
+/**
+ * Wrapper around InputText that is specialized in showing unit options.
+ */
+export const useInputStyles = makeStyles(theme => ({
+  optionText: {
+    flexGrow: 1,
+    overflowX: 'scroll',
+    '&::-webkit-scrollbar': {
+      display: 'none'
+    },
+    '-ms-overflow-style': 'none',
+    scrollbarWidth: 'none'
+  },
+  noWrap: {
+    whiteSpace: 'nowrap'
+  },
+  option: {
+    width: '100%',
+    display: 'flex',
+    alignItems: 'stretch'
+  }
+}))
+export const UnitInput = React.memo(({value, error, onChange, onAccept, onSelect, onError, onBlur, dimension, options, disabled, label, disableGroup}) => {
+  const styles = useInputStyles()
+
+  // Predefine all option objects, all option paths and also pre-tokenize the
+  // options for faster matching.
+  const {keys, filter, finalOptions} = useMemo(() => {
+    const finalOptions = {}
+    Object.entries(unitMap)
+      .filter(([key, unit]) => unit.dimension === dimension)
+      .forEach(([key, unit]) => {
+        finalOptions[key] = {
+          key: key,
+          primary: `${unit.label} (${unit.abbreviation})`,
+          secondary: unit.aliases?.splice(1).join(', '),
+          dimension: unit.dimension,
+          unit: unit
+        }
+      })
+    const keys = Object.keys(finalOptions)
+    const {filter} = getSuggestions(keys, 0)
+    return {keys, filter, finalOptions}
+  }, [dimension])
+
+  const handleChange = useCallback((value) => {
+    onChange?.(value)
+  }, [onChange])
+
+  const handleAccept = useCallback((key) => {
+    const {unit, unitString, error} = parseQuantity(key, false, true, dimension)
+    if (error) {
+      onError(error)
+    } else {
+      onAccept?.(unit, unitString)
+    }
+  }, [onAccept, onError, dimension])
+
+  const handleError = useCallback((value) => {
+    onError?.(value)
+  }, [onError])
+
+  const handleSelect = useCallback((key) => {
+    const {unit, unitString, error} = parseQuantity(key, false, true, dimension)
+    if (error) {
+      onError(error)
+    } else {
+      onSelect?.(unit, unitString)
+    }
+  }, [onSelect, onError, dimension])
+
+  // Used to filter the shown options based on input
+  const filterOptions = useCallback((opt, { inputValue }) => {
+    let filtered = filter(inputValue).map(option => option.value)
+    if (!disableGroup) filtered = filtered.sort((a, b) => options[a].group > options[b].group ? 1 : -1)
+    return filtered
+  }, [disableGroup, filter, options])
+
+  return <InputText
+    value={value}
+    TextFieldProps={{label, disabled}}
+    error={error}
+    onChange={handleChange}
+    onSelect={handleSelect}
+    onAccept={handleAccept}
+    onError={handleError}
+    onBlur={() => { onBlur(handleAccept) }}
+    disableClearable
+    disableAcceptOnBlur
+    suggestAllOnFocus
+    showOpenSuggestions
+    suggestions={keys}
+    ListboxComponent={ListboxUnit}
+    groupBy={disableGroup ? undefined : (key) => finalOptions?.[key]?.dimension}
+    renderGroup={disableGroup ? undefined : renderGroup}
+    getOptionLabel={option => option}
+    filterOptions={filterOptions}
+    renderOption={(key) => {
+      const option = finalOptions[key]
+      return <div className={styles.option}>
+        <ListItemText
+          primary={option.primary || option.key}
+          secondary={option.secondary}
+          className={styles.optionText}
+          primaryTypographyProps={{className: styles.noWrap}}
+          secondaryTypographyProps={{className: styles.noWrap}}
+        />
+      </div>
+    }}
+  />
+})
+UnitInput.propTypes = {
+  value: PropTypes.string,
+  error: PropTypes.string,
+  label: PropTypes.string,
+  dimension: PropTypes.string,
+  options: PropTypes.object,
+  onChange: PropTypes.func,
+  onAccept: PropTypes.func,
+  onSelect: PropTypes.func,
+  onBlur: PropTypes.func,
+  onError: PropTypes.func,
+  disabled: PropTypes.bool,
+  disableGroup: PropTypes.bool
+}
+
+export default UnitInput
+
+/**
+ * Custom virtualized list component for displaying unit values.
+ */
+const ListboxUnit = React.forwardRef((props, ref) => {
+  const { children, ...other } = props
+  const itemSize = 64
+  const headerSize = 40
+  const itemData = React.Children.toArray(children)
+  const itemCount = itemData.length
+
+  // Calculate size of child element.
+  const getChildSize = (child) => {
+    return React.isValidElement(child) && child.type === ListSubheader
+      ? headerSize
+      : itemSize
+  }
+
+  // Calculates the height of the suggestion box
+  const getHeight = () => {
+    return itemCount > 8
+      ? 8 * itemSize
+      : itemData.map(getChildSize).reduce((a, b) => a + b, 0)
+  }
+
+  const gridRef = useResetCache(itemCount)
+
+  return <div ref={ref}>
+    <OuterElementContext.Provider value={other}>
+      <List disablePadding>
+        <VariableSizeList
+          itemData={itemData}
+          height={getHeight() + 2 * LISTBOX_PADDING}
+          width="100%"
+          ref={gridRef}
+          outerElementType={OuterElementType}
+          innerElementType="ul"
+          itemSize={(index) => getChildSize(itemData[index])}
+          overscanCount={5}
+          itemCount={itemCount}
+        >
+          {renderRow}
+        </VariableSizeList>
+      </List>
+    </OuterElementContext.Provider>
+  </div>
+})
+
+ListboxUnit.propTypes = {
+  children: PropTypes.node
+}
+
+const LISTBOX_PADDING = 8
+const OuterElementContext = createContext({})
+
+const OuterElementType = React.forwardRef((props, ref) => {
+  const outerProps = useContext(OuterElementContext)
+  return <div ref={ref} {...props} {...outerProps} />
+})
+
+const renderGroup = (params) => [
+  <ListSubheader key={params.key} component="div">
+    {params.group}
+  </ListSubheader>,
+  params.children
+]
+
+function useResetCache(data) {
+  const ref = useRef(null)
+  useEffect(() => {
+    if (ref.current != null) {
+      ref.current.resetAfterIndex(0, true)
+    }
+  }, [data])
+  return ref
+}
+
+function renderRow({ data, index, style }) {
+  return React.cloneElement(data[index], {
+    style: {
+      ...style,
+      top: style.top + LISTBOX_PADDING
+    }
+  })
+}
diff --git a/gui/src/components/units/UnitMenu.js b/gui/src/components/units/UnitMenu.js
new file mode 100644
index 0000000000000000000000000000000000000000..ac46985c7f86f1996b1cca9c0e1572146518aef1
--- /dev/null
+++ b/gui/src/components/units/UnitMenu.js
@@ -0,0 +1,184 @@
+/*
+ * 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, useState, useMemo } from 'react'
+import { Box, Button, Menu, FormLabel, makeStyles, Typography } from '@material-ui/core'
+import SettingsIcon from '@material-ui/icons/Settings'
+import ReplayIcon from '@material-ui/icons/Replay'
+import PropTypes from 'prop-types'
+import { HelpButton } from '../Help'
+import { InputText } from '../search/input/InputText'
+import UnitDimensionSelect from './UnitDimensionSelect'
+import UnitSystemSelect from './UnitSystemSelect'
+import { useUnitContext } from './UnitContext'
+import { Action, ActionHeader, Actions } from '../Actions'
+
+/**
+ * Menu for controlling all units in the current unit context.
+ */
+const useStyles = makeStyles(theme => ({
+  // MUI will automatically add a padding whe scroll bar is visible. This is
+  // disabled here because the contents already have a sufficient margin.
+  list: {
+    paddingRight: '0 !important',
+    width: '100% !important'
+  }
+}))
+const UnitMenu = React.memo(({
+  className,
+  onUnitChange,
+  onSystemChange
+}) => {
+  const {units, dimensionMap, reset} = useUnitContext()
+  const [anchorEl, setAnchorEl] = useState(null)
+  const open = Boolean(anchorEl)
+  const styles = useStyles()
+
+  const dimensionOptions = useMemo(() => {
+    return Object.keys(units)
+      .filter((name) => name !== 'dimensionless')
+      .sort()
+  }, [units])
+  const [dimension, setDimension] = useState(dimensionOptions[0])
+  const [dimensionInput, setDimensionInput] = useState(dimensionMap[dimensionOptions[0]].label)
+
+  const openMenu = useCallback((event) => {
+    setAnchorEl(event.currentTarget)
+  }, [])
+  const closeMenu = useCallback(() => {
+    setAnchorEl(null)
+  }, [])
+
+  const handleChange = useCallback((value) => {
+    setDimensionInput(value)
+  }, [])
+
+  const handleBlur = useCallback(() => {
+    const cleanValue = dimensionInput.trim().toLowerCase()
+    const dim = dimensionMap[cleanValue]
+    if (dim) {
+      setDimensionInput(dim.label)
+      setDimension(cleanValue)
+    } else {
+      setDimensionInput(dimensionMap[dimension].label)
+    }
+  }, [dimension, dimensionInput, dimensionMap])
+
+  const handleSelect = useCallback((value) => {
+    setDimension(value)
+    setDimensionInput(dimensionMap[value].label)
+  }, [dimensionMap])
+
+  return <>
+    <Button
+      aria-controls="customized-menu"
+      aria-haspopup="true"
+      variant="text"
+      color="primary"
+      onClick={openMenu}
+      className={className}
+      startIcon={<SettingsIcon/>}
+    >
+      Units
+    </Button>
+    <Menu
+      variant="menu"
+      anchorEl={anchorEl}
+      getContentAnchorEl={null}
+      anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
+      transformOrigin={{ vertical: 'top', horizontal: 'right' }}
+      keepMounted
+      open={open}
+      onClose={closeMenu}
+      classes={{list: styles.list}}
+    >
+      <Box px={2.5} py={1} width="20rem">
+        <Actions>
+          <ActionHeader>
+            <Typography variant='h6' fontSize='0.9rem'>
+              Unit Settings
+            </Typography>
+          </ActionHeader>
+          <Action
+            tooltip="About units"
+            ButtonComponent={HelpButton}
+            ButtonProps={{
+              title: "About units",
+              IconProps: {fontSize: 'small'},
+              content: `
+                With these settings you can change in which units numerical data
+                is **displayed** in the browser. This display unit can be
+                different from the unit that is used when the data is stored.
+                Note that it is possible to define a fixed display unit in the
+                metainfo which will overrule the settings made through this
+                menu.
+
+                Each NOMAD installation comes with a default set of unit
+                systems, which can be modified in the \`nomad.yaml\`
+                configuration file. You can here choose which of these unit
+                systems to use.
+
+                Each unit system contains information about the exact unit that
+                should be used for each dimension. Many of the commonly used
+                units are available for selection, and you may use the SI
+                prefixes on any of them. Note that in some unit systems certain
+                dimensions are locked, which means that you cannot change them.
+                `
+            }}
+          >
+          </Action>
+          <Action tooltip="Reset unit settings" onClick={reset}>
+            <ReplayIcon fontSize="small"/>
+          </Action>
+        </Actions>
+        <Box mt={1} />
+        <FormLabel component="legend">Select unit system</FormLabel>
+        <UnitSystemSelect onChange={onSystemChange}/>
+        <Box mt={1} />
+        <FormLabel component="legend">Select dimension and unit</FormLabel>
+        <Box mt={1} />
+        <InputText
+          value={dimensionInput}
+          suggestions={dimensionOptions}
+          disableClearable
+          suggestAllOnFocus
+          showOpenSuggestions
+          onChange={handleChange}
+          onSelect={handleSelect}
+          onBlur={handleBlur}
+          renderOption={(option) => dimensionMap[option].label}
+          getOptionLabel={(option) => option}
+          TextFieldProps={{label: 'Dimension'}}
+        />
+        <Box mt={1} />
+        <UnitDimensionSelect
+          onChange={onUnitChange}
+          dimension={dimension}
+          label="Unit"
+        />
+      </Box>
+    </Menu>
+  </>
+})
+
+UnitMenu.propTypes = {
+  onUnitChange: PropTypes.func,
+  onSystemChange: PropTypes.func,
+  className: PropTypes.string
+}
+
+export default UnitMenu
diff --git a/gui/src/components/UnitSelector.spec.js b/gui/src/components/units/UnitMenu.spec.js
similarity index 62%
rename from gui/src/components/UnitSelector.spec.js
rename to gui/src/components/units/UnitMenu.spec.js
index e80f66b7409f7e7c2edbee2da0c8ff03f67674d0..953e977896106a7eb1eac4e480ac0767e42c3ee9 100644
--- a/gui/src/components/UnitSelector.spec.js
+++ b/gui/src/components/units/UnitMenu.spec.js
@@ -17,15 +17,24 @@
  */
 
 import React from 'react'
-import { renderNoAPI, screen } from './conftest.spec'
+import { renderNoAPI, screen } from '../conftest.spec'
 import userEvent from '@testing-library/user-event'
-import UnitSelector from './UnitSelector'
+import UnitMenu from './UnitMenu'
 
-test('initial unit selection is read correctly from config', async () => {
+test('initial state is read correctly from config', async () => {
+  renderNoAPI(<UnitMenu />)
+
+  // Correct unit system is selected
   const selection = window.nomadEnv.ui.unit_systems.selected
-  renderNoAPI(<UnitSelector />)
   const button = screen.getByButtonText("Units")
   await userEvent.click(button)
-  const optionSI = screen.getByLabelText(selection)
-  expect(optionSI).toBeChecked()
+  const optionSelected = screen.getByLabelText(selection)
+  expect(optionSelected).toBeChecked()
+
+  // Dimension is shown
+  screen.getByDisplayValue('Activity')
+
+  // Unit is shown
+  const selectedUnit = window.nomadEnv.ui.unit_systems.options[selection].units.activity.definition
+  screen.getByDisplayValue(selectedUnit)
 })
diff --git a/gui/src/components/units/UnitSystemSelect.js b/gui/src/components/units/UnitSystemSelect.js
new file mode 100644
index 0000000000000000000000000000000000000000..b495880c91ac68e34cf1549903231a3b5167fe04
--- /dev/null
+++ b/gui/src/components/units/UnitSystemSelect.js
@@ -0,0 +1,45 @@
+/*
+ * 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 {FormControlLabel, RadioGroup, Radio} from '@material-ui/core'
+import PropTypes from 'prop-types'
+import {useUnitContext} from './UnitContext'
+
+/**
+ * Controls the unit system in the current unit context.
+ */
+const UnitSystemSelect = React.memo(({onChange}) => {
+  const {unitSystems, selected, setSelected} = useUnitContext()
+
+  const handleSystemChange = useCallback((event) => {
+    setSelected(event.target.value)
+    onChange && onChange(event)
+  }, [onChange, setSelected])
+
+  return <RadioGroup value={selected} onChange={handleSystemChange}>
+    {Object.entries(unitSystems).map(([key, system]) =>
+      <FormControlLabel key={key} value={key} control={<Radio />} label={system.label} />
+    )}
+  </RadioGroup>
+})
+UnitSystemSelect.propTypes = {
+  onChange: PropTypes.func,
+  disabled: PropTypes.bool
+}
+
+export default UnitSystemSelect
diff --git a/gui/src/components/uploads/UploadsPage.js b/gui/src/components/uploads/UploadsPage.js
index 03a733816e5968bb3f6f374797d9f28dd5c5be70..7c834eab508203fb0d6d06efb51100f5307a4a9a 100644
--- a/gui/src/components/uploads/UploadsPage.js
+++ b/gui/src/components/uploads/UploadsPage.js
@@ -23,7 +23,7 @@ import {
   Box, Divider, makeStyles
 } from '@material-ui/core'
 import ClipboardIcon from '@material-ui/icons/Assignment'
-import HelpDialog from '../Help'
+import { HelpButton } from '../Help'
 import { CopyToClipboard } from 'react-copy-to-clipboard'
 import { guiBase, servicesUploadLimit } from '../../config'
 import NewUploadButton from './NewUploadButton'
@@ -167,42 +167,45 @@ function UploadCommands({uploadCommands}) {
             <ClipboardIcon />
           </IconButton>
         </Tooltip>
-        {/* <button>Copy to clipboard with button</button> */}
       </CopyToClipboard>
-      <HelpDialog icon={<DetailsIcon/>} maxWidth="md" title="Alternative shell commands" content={`
-        As an experienced shell and *curl* user, you can modify the commands to
-        your liking.
-
-        The given command can be modified. To see progress on large files, use
-        \`\`\`
-          ${uploadCommands.upload_progress_command}
-        \`\`\`
-        To \`tar\` and upload multiple folders in one command, use
-        \`\`\`
-        ${uploadCommands.upload_tar_command}
-        \`\`\`
-
-        ### Form data vs. streaming
-        NOMAD accepts stream data (\`-T <local_file>\`) (like in the
-        examples above) or multi-part form data (\`-X PUT -f file=@<local_file>\`):
-        \`\`\`
-        ${uploadCommands.upload_command_form}
-        \`\`\`
-        We generally recommend to use streaming, because form data can produce very
-        large HTTP request on large files. Form data has the advantage of carrying
-        more information (e.g. the file name) to our servers (see below).
-
-        #### Upload names
-        With multi-part form data (\`-X PUT -f file=@<local_file>\`), your upload will
-        be named after the file by default. With stream data (\`-T <local_file>\`)
-        there will be no default name. To set a custom name, you can use the URL
-        parameter \`name\`:
-        \`\`\`
-        ${uploadCommands.upload_command_with_name}
-        \`\`\`
-        Make sure to user proper [URL encoding](https://www.w3schools.com/tags/ref_urlencode.asp)
-        and shell encoding, if your name contains spaces or other special characters.
-      `}/>
+      <Tooltip title="Alternative shell commands">
+        <span>
+          <HelpButton maxWidth="md" title="Alternative shell commands" content={`
+            As an experienced shell and *curl* user, you can modify the commands to
+            your liking.
+
+            The given command can be modified. To see progress on large files, use
+            \`\`\`
+              ${uploadCommands.upload_progress_command}
+            \`\`\`
+            To \`tar\` and upload multiple folders in one command, use
+            \`\`\`
+            ${uploadCommands.upload_tar_command}
+            \`\`\`
+
+            ### Form data vs. streaming
+            NOMAD accepts stream data (\`-T <local_file>\`) (like in the
+            examples above) or multi-part form data (\`-X PUT -f file=@<local_file>\`):
+            \`\`\`
+            ${uploadCommands.upload_command_form}
+            \`\`\`
+            We generally recommend to use streaming, because form data can produce very
+            large HTTP request on large files. Form data has the advantage of carrying
+            more information (e.g. the file name) to our servers (see below).
+
+            #### Upload names
+            With multi-part form data (\`-X PUT -f file=@<local_file>\`), your upload will
+            be named after the file by default. With stream data (\`-T <local_file>\`)
+            there will be no default name. To set a custom name, you can use the URL
+            parameter \`name\`:
+            \`\`\`
+            ${uploadCommands.upload_command_with_name}
+            \`\`\`
+            Make sure to user proper [URL encoding](https://www.w3schools.com/tags/ref_urlencode.asp)
+            and shell encoding, if your name contains spaces or other special characters.
+          `}/>
+        </span>
+      </Tooltip>
     </div>
   </div>
 }
diff --git a/gui/src/components/visualization/BandGap.js b/gui/src/components/visualization/BandGap.js
index 4521bd99230d38bf9b475b537d46d97fa4f22c41..a33fd4a4f18cdbd0b5bd64c4e4e5c8900bebf772 100644
--- a/gui/src/components/visualization/BandGap.js
+++ b/gui/src/components/visualization/BandGap.js
@@ -18,7 +18,7 @@
 import React, { } from 'react'
 import PropTypes from 'prop-types'
 import { SectionTable } from '../Quantity'
-import { useUnits } from '../../units'
+import { useUnitContext } from '../units/UnitContext'
 import { withErrorHandler } from '../ErrorHandler'
 import NoData from './NoData'
 import Placeholder from './Placeholder'
@@ -37,7 +37,7 @@ const columns = {
  * table.
  */
 const BandGap = React.memo(({data, section, 'data-testid': testID}) => {
-  const units = useUnits()
+  const {units} = useUnitContext()
   const extendedColumns = {}
   if (data && data[0].label) {
     extendedColumns.label = {label: '', align: 'left'}
diff --git a/gui/src/components/visualization/BandStructure.js b/gui/src/components/visualization/BandStructure.js
index 108d9d8d6aff75e11be01a02b6338ac5cd0956f5..201d4b9e7b7401e4bb7653c3b75b0d4e15787377 100644
--- a/gui/src/components/visualization/BandStructure.js
+++ b/gui/src/components/visualization/BandStructure.js
@@ -21,7 +21,8 @@ import { isFinite } from 'lodash'
 import { useTheme } from '@material-ui/core/styles'
 import Plot from '../plotting/Plot'
 import { add, distance, mergeObjects } from '../../utils'
-import { Quantity, Unit } from '../../units'
+import { Quantity } from '../units/Quantity'
+import { Unit } from '../units/Unit'
 import { withErrorHandler } from '../ErrorHandler'
 import { msgNormalizationWarning } from '../../config'
 import { getLineStyles } from '../plotting/common'
diff --git a/gui/src/components/visualization/DOS.js b/gui/src/components/visualization/DOS.js
index 4bc4fe8cd0b9878f26457eae422c4f835c73d4d5..968436037d46c063b94f2ba417b279f7587ff4db 100644
--- a/gui/src/components/visualization/DOS.js
+++ b/gui/src/components/visualization/DOS.js
@@ -21,7 +21,9 @@ import { useTheme } from '@material-ui/core/styles'
 import { MoreVert } from '@material-ui/icons'
 import Plot from '../plotting/Plot'
 import { add, mergeObjects, resolveInternalRef } from '../../utils'
-import { Quantity, Unit, useUnits } from '../../units'
+import { Quantity } from '../units/Quantity'
+import { Unit } from '../units/Unit'
+import { useUnitContext } from '../units/UnitContext'
 import { withErrorHandler } from '../ErrorHandler'
 import { Action } from '../Actions'
 import { msgNormalizationWarning } from '../../config'
@@ -39,7 +41,7 @@ const DOS = React.memo(({
   'data-testid': testID,
   ...other
 }) => {
-  const units = useUnits()
+  const {units} = useUnitContext()
 
   // Merge custom layout with default layout
   const initialLayout = useMemo(() => {
diff --git a/gui/src/components/visualization/ElectronicProperties.js b/gui/src/components/visualization/ElectronicProperties.js
index ff25b62c465912a4a8750dfb6441b0f9e590410a..2874d06de86cddf3af2303a1112f94fdf255d7fd 100644
--- a/gui/src/components/visualization/ElectronicProperties.js
+++ b/gui/src/components/visualization/ElectronicProperties.js
@@ -18,7 +18,8 @@
 import React, { useCallback, useMemo } from 'react'
 import { Subject } from 'rxjs'
 import PropTypes from 'prop-types'
-import { Quantity, useUnits } from '../../units'
+import { Quantity } from '../units/Quantity'
+import { useUnitContext } from '../units/UnitContext'
 import DOS from './DOS'
 import BandStructure from './BandStructure'
 import BrillouinZone from './BrillouinZone'
@@ -54,7 +55,7 @@ const ElectronicProperties = React.memo(({
   // We resolve the DMFT methodology from results.method
   const dmftprovenance = index?.results?.method?.simulation?.dmft || []
 
-  const units = useUnits()
+  const {units} = useUnitContext()
   const range = useMemo(() => new Quantity(electronicRange, 'electron_volt').toSystem(units).value(), [units])
   const bsLayout = useMemo(() => ({yaxis: {autorange: false, range: range}}), [range])
   const dosLayout = useMemo(() => ({yaxis: {autorange: false, range: range}}), [range])
diff --git a/gui/src/components/visualization/EnergyVolumeCurve.js b/gui/src/components/visualization/EnergyVolumeCurve.js
index 66b677229248cfd4e499a50a3152caf9af5076df..85a602d66c97ac2ffbf49cbdee26e6f4fc30ef0f 100644
--- a/gui/src/components/visualization/EnergyVolumeCurve.js
+++ b/gui/src/components/visualization/EnergyVolumeCurve.js
@@ -20,7 +20,8 @@ import PropTypes from 'prop-types'
 import { useTheme } from '@material-ui/core/styles'
 import Plot from '../plotting/Plot'
 import { withErrorHandler } from '../ErrorHandler'
-import { Quantity, Unit } from '../../units'
+import { Quantity } from '../units/Quantity'
+import { Unit } from '../units/Unit'
 import { getLineStyles } from '../plotting/common'
 
 /**
diff --git a/gui/src/components/visualization/GeometryOptimization.js b/gui/src/components/visualization/GeometryOptimization.js
index 9d36a596cbe20daf0cf40613671dd2291698e9da..5453c892dae0ec047713f07e7ea258cb093ac057 100644
--- a/gui/src/components/visualization/GeometryOptimization.js
+++ b/gui/src/components/visualization/GeometryOptimization.js
@@ -24,7 +24,9 @@ import Plot from '../plotting/Plot'
 import { QuantityTable, QuantityRow, QuantityCell } from '../Quantity'
 import { ErrorHandler, withErrorHandler } from '../ErrorHandler'
 import { diffTotal } from '../../utils'
-import { Quantity, Unit, useUnits } from '../../units'
+import { Quantity } from '../units/Quantity'
+import { Unit } from '../units/Unit'
+import { useUnitContext } from '../units/UnitContext'
 import { PropertyGrid, PropertyItem } from '../entry/properties/PropertyCard'
 
 const energyUnit = new Unit('joule')
@@ -39,7 +41,7 @@ const GeometryOptimization = React.memo(({
   'data-testid': testID
 }) => {
   const [finalData, setFinalData] = useState(!energies ? energies : undefined)
-  const units = useUnits()
+  const {units} = useUnitContext()
   const theme = useTheme()
 
   // Side effect that runs when the data that is displayed should change. By
diff --git a/gui/src/components/visualization/HeatCapacity.js b/gui/src/components/visualization/HeatCapacity.js
index e32699c2915d36fdb330eee9282b1e014b8afce9..9f5fc43d8d7ad30b83cbc88cd89493b0cbc570ea 100644
--- a/gui/src/components/visualization/HeatCapacity.js
+++ b/gui/src/components/visualization/HeatCapacity.js
@@ -20,7 +20,8 @@ import PropTypes from 'prop-types'
 import { useTheme } from '@material-ui/core/styles'
 import Plot from '../plotting/Plot'
 import { mergeObjects } from '../../utils'
-import { Quantity, Unit } from '../../units'
+import { Quantity } from '../units/Quantity'
+import { Unit } from '../units/Unit'
 import { withErrorHandler } from '../ErrorHandler'
 
 const HeatCapacity = React.memo(({
diff --git a/gui/src/components/visualization/HelmholtzFreeEnergy.js b/gui/src/components/visualization/HelmholtzFreeEnergy.js
index 1280affa69a94e74fd0c7189511efe5cdbf2a4a1..ef0c61fbedc09fd6405ede06a585f9d89b1c31f7 100644
--- a/gui/src/components/visualization/HelmholtzFreeEnergy.js
+++ b/gui/src/components/visualization/HelmholtzFreeEnergy.js
@@ -20,7 +20,8 @@ import PropTypes from 'prop-types'
 import { useTheme } from '@material-ui/core/styles'
 import Plot from '../plotting/Plot'
 import { mergeObjects } from '../../utils'
-import { Quantity, Unit } from '../../units'
+import { Quantity } from '../units/Quantity'
+import { Unit } from '../units/Unit'
 import { withErrorHandler } from '../ErrorHandler'
 
 const HelmholtzFreeEnergy = React.memo(({
diff --git a/gui/src/components/visualization/MeanSquaredDisplacement.js b/gui/src/components/visualization/MeanSquaredDisplacement.js
index c18c11b233b2a02bab93d8bb7b4355f166e21020..bff56f00f348b4d6cd5308d72fa11acdb90eed27 100644
--- a/gui/src/components/visualization/MeanSquaredDisplacement.js
+++ b/gui/src/components/visualization/MeanSquaredDisplacement.js
@@ -23,7 +23,9 @@ import { useTheme } from '@material-ui/core/styles'
 import Plot from '../plotting/Plot'
 import { PropertyGrid, PropertyItem } from '../entry/properties/PropertyCard'
 import { getLocation, formatNumber, DType } from '../../utils'
-import { Quantity, Unit, useUnits } from '../../units'
+import { Quantity } from '../units/Quantity'
+import { Unit } from '../units/Unit'
+import { useUnitContext } from '../units/UnitContext'
 import { ErrorHandler, withErrorHandler } from '../ErrorHandler'
 import { getLineStyles } from '../plotting/common'
 
@@ -46,7 +48,7 @@ const MeanSquaredDisplacement = React.memo(({
   const theme = useTheme()
   const [finalData, setFinalData] = useState(msd)
   const [finalLayout, setFinalLayout] = useState()
-  const units = useUnits()
+  const {units} = useUnitContext()
 
   // Check that the data is valid. Otherwise raise an exception.
   assert(isPlainObject(msd), 'Invalid msd data provided.')
diff --git a/gui/src/components/visualization/RadialDistributionFunction.js b/gui/src/components/visualization/RadialDistributionFunction.js
index d252bd9f1df9444881e11dc5449f749a548dce35..5df429dd38ddbe1a125953c1f650b5a0d6909ccb 100644
--- a/gui/src/components/visualization/RadialDistributionFunction.js
+++ b/gui/src/components/visualization/RadialDistributionFunction.js
@@ -23,7 +23,9 @@ import { useTheme } from '@material-ui/core/styles'
 import Plot from '../plotting/Plot'
 import { PropertyItem, PropertySubGrid } from '../entry/properties/PropertyCard'
 import { getLocation } from '../../utils'
-import { Quantity, Unit, useUnits } from '../../units'
+import { Quantity } from '../units/Quantity'
+import { Unit } from '../units/Unit'
+import { useUnitContext } from '../units/UnitContext'
 import { ErrorHandler, withErrorHandler } from '../ErrorHandler'
 import { getLineStyles } from '../plotting/common'
 
@@ -44,7 +46,7 @@ const RadialDistributionFunction = React.memo(({
   const theme = useTheme()
   const [finalData, setFinalData] = useState(rdf)
   const [finalLayout, setFinalLayout] = useState()
-  const units = useUnits()
+  const {units} = useUnitContext()
 
   // Check that the data is valid. Otherwise raise an exception.
   assert(isPlainObject(rdf), 'Invalid rdf data provided.')
diff --git a/gui/src/components/visualization/RadiusOfGyration.js b/gui/src/components/visualization/RadiusOfGyration.js
index 9e7bbe570ad851e25bdba8e792ddc48073e08e1b..094d7786bf4c9f0628322d1611696078649bd685 100644
--- a/gui/src/components/visualization/RadiusOfGyration.js
+++ b/gui/src/components/visualization/RadiusOfGyration.js
@@ -23,7 +23,9 @@ import { useTheme } from '@material-ui/core/styles'
 import Plot from '../plotting/Plot'
 import { PropertyItem, PropertySubGrid } from '../entry/properties/PropertyCard'
 import { getLocation } from '../../utils'
-import { Quantity, Unit, useUnits } from '../../units'
+import { Quantity } from '../units/Quantity'
+import { Unit } from '../units/Unit'
+import { useUnitContext } from '../units/UnitContext'
 import { ErrorHandler, withErrorHandler } from '../ErrorHandler'
 import { getLineStyles } from '../plotting/common'
 
@@ -45,7 +47,7 @@ const RadiusOfGyration = React.memo(({
   const theme = useTheme()
   const [finalData, setFinalData] = useState(rg)
   const [finalLayout, setFinalLayout] = useState()
-  const units = useUnits()
+  const {units} = useUnitContext()
 
   // Check that the data is valid. Otherwise raise an exception.
   assert(isPlainObject(rg), 'Invalid rg data provided.')
diff --git a/gui/src/components/visualization/Spectra.js b/gui/src/components/visualization/Spectra.js
index 3d5cc20333369e114238e24384df552957c649f2..68674dfb65819aa2c052650d9835e793b377ec28 100644
--- a/gui/src/components/visualization/Spectra.js
+++ b/gui/src/components/visualization/Spectra.js
@@ -23,7 +23,9 @@ import { MoreVert } from '@material-ui/icons'
 import Plot from '../plotting/Plot'
 import { mergeObjects } from '../../utils'
 import { getLineStyles } from '../plotting/common'
-import { Quantity, Unit, useUnits } from '../../units'
+import { Quantity } from '../units/Quantity'
+import { Unit } from '../units/Unit'
+import { useUnitContext } from '../units/UnitContext'
 import { withErrorHandler } from '../ErrorHandler'
 import { Action } from '../Actions'
 
@@ -42,7 +44,7 @@ const Spectra = React.memo(({
 }) => {
   const [finalData, setFinalData] = useState(!data ? data : undefined)
   const theme = useTheme()
-  const units = useUnits()
+  const {units} = useUnitContext()
   const [anchorEl, setAnchorEl] = React.useState(null)
   const [spectraNormalize, setSpectraNormalize] = useState(true)
 
diff --git a/gui/src/components/visualization/Structure.js b/gui/src/components/visualization/Structure.js
index 036435c40575ade612b96d5b553241bc826d08b1..3fda7e6b6e64dc24d68028dcccfabbef449071b8 100644
--- a/gui/src/components/visualization/Structure.js
+++ b/gui/src/components/visualization/Structure.js
@@ -44,7 +44,7 @@ import { Actions, Action } from '../Actions'
 import { withErrorHandler, withWebGLErrorHandler } from '../ErrorHandler'
 import { useHistory } from 'react-router-dom'
 import { isEmpty, flattenDeep } from 'lodash'
-import { Quantity } from '../../units'
+import { Quantity } from '../units/Quantity'
 import { delay } from '../../utils'
 import { useAsyncError } from '../../hooks'
 import clsx from 'clsx'
diff --git a/gui/src/components/visualization/StructureBase.js b/gui/src/components/visualization/StructureBase.js
index ad94f32f901aebb03aef5ec1d7b2f32d6c80eae5..3f240e49a42b72d45634c83f41082fc27141d42d 100644
--- a/gui/src/components/visualization/StructureBase.js
+++ b/gui/src/components/visualization/StructureBase.js
@@ -25,13 +25,8 @@ import {
   Button,
   Menu,
   MenuItem,
-  Tooltip,
   Typography,
-  FormControl,
-  FormLabel,
-  FormControlLabel,
-  Radio,
-  RadioGroup
+  FormControlLabel
 } from '@material-ui/core'
 import { Alert } from '@material-ui/lab'
 import {
@@ -43,20 +38,15 @@ import {
   ViewList,
   GetApp
 } from '@material-ui/icons'
-import { DownloadSystemMenu } from '../buttons/DownloadSystemButton'
+import { DownloadSystemMenu, WrapModeRadio } from '../buttons/DownloadSystemButton'
 import Floatable from './Floatable'
 import NoData from './NoData'
 import Placeholder from './Placeholder'
 import { Actions, Action } from '../Actions'
 import { withErrorHandler, withWebGLErrorHandler } from '../ErrorHandler'
 import { isNil } from 'lodash'
-import { Quantity } from '../../units'
+import { Quantity } from '../units/Quantity'
 
-export const WrapMode = {
-  Original: "original",
-  Wrap: "wrap",
-  Unwrap: "unwrap"
-}
 /**
  * Used to control a 3D system visualization that is implemented in the
  * 'children' prop. This allows for an easier change of visualization
@@ -315,29 +305,7 @@ const StructureBase = React.memo(({
                 label='Show simulation cell'
               />
             </MenuItem>
-            <FormControl key='wrap' component="fieldset" className={styles.menuItem}>
-              <FormLabel component="legend">Wrap mode</FormLabel>
-              <RadioGroup
-                value={wrapMode}
-                onChange={handleWrapModeChange}
-                >
-                {Object.entries(WrapMode).map(([key, value]) =>
-                  <FormControlLabel
-                    key={key}
-                    value={value}
-                    control={<Radio color="primary" disabled={disableWrapMode}/>}
-                    label={<Tooltip
-                      title={{
-                        [WrapMode.Original]: 'Original positions',
-                        [WrapMode.Wrap]: 'Positions wrapped inside the cell respecting periodic boundary conditions',
-                        [WrapMode.Unwrap]: 'Reconstructs positions so that small structures are not split by periodic cell boundary.'
-                      }[value]}>
-                        <span>{key}</span>
-                    </Tooltip>}
-                  />
-                )}
-              </RadioGroup>
-            </FormControl>
+            <WrapModeRadio value={wrapMode} onChange={handleWrapModeChange} disabled={disableWrapMode} className={styles.menuItem}/>
           </Menu>
         </div>
       </div>
diff --git a/gui/src/components/visualization/StructureNGL.js b/gui/src/components/visualization/StructureNGL.js
index fd17281d30a2a8c14e6aa0705aae1c29007a390e..2c70a7f19229d72dabdf318213bf164faab35e65 100644
--- a/gui/src/components/visualization/StructureNGL.js
+++ b/gui/src/components/visualization/StructureNGL.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-unused-vars */
 /*
  * Copyright The NOMAD Authors.
  *
@@ -29,7 +28,8 @@ import { makeStyles } from '@material-ui/core'
 import PropTypes from 'prop-types'
 import { isNil, isEqual, range } from 'lodash'
 import { Stage, Vector3 } from 'ngl'
-import StructureBase, { WrapMode } from './StructureBase'
+import StructureBase from './StructureBase'
+import { wrapModes } from '../buttons/DownloadSystemButton'
 import * as THREE from 'three'
 import { withErrorHandler } from '../ErrorHandler'
 import { useAsyncError } from '../../hooks'
@@ -67,7 +67,7 @@ const StructureNGL = React.memo(({
   const [showCell, setShowCell] = useState(true)
   const [showLatticeConstants, setShowLatticeConstants] = useState(true)
   const [disableShowLatticeConstants, setDisableShowLatticeContants] = useState(true)
-  const [wrapMode, setWrapMode] = useState(WrapMode.Wrap)
+  const [wrapMode, setWrapMode] = useState(wrapModes.wrap.key)
   const [disableWrapMode, setDisableWrapMode] = useState(false)
   const [disableShowCell, setDisableShowCell] = useState(false)
   const [disableShowBonds, setDisableShowBonds] = useState(false)
@@ -350,7 +350,7 @@ const StructureNGL = React.memo(({
           atoms: atomRepr,
           sele: sele,
           indices: indices,
-          wrapMode: (isMonomer || isMolecule) ? WrapMode.Unwrap : WrapMode.Wrap
+          wrapMode: (isMonomer || isMolecule) ? wrapModes.unwrap.key : wrapModes.wrap.key
         }
         for (const child of top.child_systems || []) {
           if (!child.atoms) addRepresentation(child)
@@ -1220,7 +1220,7 @@ function wrapRepresentation(component, representation) {
   }
 
   // Use wrapped positions
-  if (wrapMode === WrapMode.Wrap) {
+  if (wrapMode === wrapModes.wrap.key) {
     if (!isNil(representation.posWrap)) {
       posNew = representation.posWrap
     } else {
@@ -1232,7 +1232,7 @@ function wrapRepresentation(component, representation) {
       representation.posWrap = posNew
     }
   // Use unwrapped positions
-  } else if (wrapMode === WrapMode.Unwrap) {
+  } else if (wrapMode === wrapModes.unwrap.key) {
     if (!isNil(representation.posUnwrap)) {
       posNew = representation.posUnwrap
     } else {
@@ -1246,7 +1246,7 @@ function wrapRepresentation(component, representation) {
       representation.posUnwrap = posNew
     }
   // Use original positions
-  } else if (wrapMode === WrapMode.Original) {
+  } else if (wrapMode === wrapModes.original.key) {
     posNew = representation.posCart
   } else {
     throw Error('Invalid wrapmode provided.')
diff --git a/gui/src/components/visualization/Trajectory.js b/gui/src/components/visualization/Trajectory.js
index ac59f3f8141838028f37104cbf0b840428c478a6..803c27dbdf66998faff544b6b13c7136c4449465 100644
--- a/gui/src/components/visualization/Trajectory.js
+++ b/gui/src/components/visualization/Trajectory.js
@@ -26,7 +26,9 @@ import {
   PropertyProvenanceItem,
   PropertyProvenanceList
 } from '../entry/properties/PropertyCard'
-import { Quantity, Unit, useUnits } from '../../units'
+import { Quantity } from '../units/Quantity'
+import { Unit } from '../units/Unit'
+import { useUnitContext } from '../units/UnitContext'
 import { withErrorHandler } from '../ErrorHandler'
 import { getPlotLayoutVertical, getPlotTracesVertical } from '../plotting/common'
 
@@ -60,7 +62,7 @@ const Trajectory = React.memo(({
   if (energyPotential !== false) ++nPlots
   const styles = useStyles({classes: classes})
   const theme = useTheme()
-  const units = useUnits()
+  const {units} = useUnitContext()
   const [finalData, setFinalData] = useState(nPlots === 0 ? false : undefined)
   const [finalLayout, setFinalLayout] = useState()
 
diff --git a/gui/src/components/visualization/VibrationalProperties.js b/gui/src/components/visualization/VibrationalProperties.js
index ec81bc0326376cc05973b4973cad9bcb7a33f663..001094a2d38c04d3a718a029a966a956b9031b51 100644
--- a/gui/src/components/visualization/VibrationalProperties.js
+++ b/gui/src/components/visualization/VibrationalProperties.js
@@ -23,7 +23,7 @@ import BandStructure from './BandStructure'
 import HeatCapacity from './HeatCapacity'
 import { PropertyGrid, PropertyItem } from '../entry/properties/PropertyCard'
 import HelmholtzFreeEnergy from './HelmholtzFreeEnergy'
-import { Quantity } from '../../units'
+import { Quantity } from '../units/Quantity'
 
 const VibrationalProperties = React.memo(({
   bs,
diff --git a/gui/src/units.js b/gui/src/units.js
deleted file mode 100644
index 2ee2228e7d981abb8b01b3f7d2213670a5d58523..0000000000000000000000000000000000000000
--- a/gui/src/units.js
+++ /dev/null
@@ -1,671 +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 { isNil, startCase, toLower, has, cloneDeep, isString, isNumber, isArray } from 'lodash'
-import { mapDeep } from './utils'
-import { Unit as UnitMathJS, createUnit } from 'mathjs'
-import { atom, useRecoilValue } from 'recoil'
-import { ui, unitList, unitPrefixes as prefixes } from './config'
-
-// Delete all units and prefixes that come by default with Math.js. This way
-// they cannot be intermixed with the NOMAD units. Notice that we have to clear
-// them in place: they are defined as const.
-Object.keys(UnitMathJS.UNITS).forEach(name => UnitMathJS.deleteUnit(name))
-UnitMathJS.BASE_DIMENSIONS.splice(0, UnitMathJS.BASE_DIMENSIONS.length)
-Object.getOwnPropertyNames(UnitMathJS.BASE_UNITS).forEach(function(prop) {
-  delete UnitMathJS.BASE_UNITS[prop]
-})
-Object.getOwnPropertyNames(UnitMathJS.PREFIXES).forEach(function(prop) {
-  delete UnitMathJS.PREFIXES[prop]
-})
-UnitMathJS.PREFIXES.NONE = {'': { name: '', value: 1, scientific: true }}
-UnitMathJS.PREFIXES.PINT = prefixes
-
-// Customize the unit parsing to allow certain special symbols
-const isAlphaOriginal = UnitMathJS.isValidAlpha
-const isSpecialChar = function(c) {
-  const specialChars = new Set(['_', 'Å', 'Å', 'å', '°', 'µ', 'ö', 'é', '∞'])
-  return specialChars.has(c)
-}
-const isGreekLetter = function(c) {
-  const charCode = c.charCodeAt(0)
-  return (charCode > 912 && charCode < 970)
-}
-UnitMathJS.isValidAlpha = function(c) {
-  return isAlphaOriginal(c) || isSpecialChar(c) || isGreekLetter(c)
-}
-
-// Create MathJS unit definitions from the data exported by 'nomad dev units'
-const unitToAbbreviationMap = {}
-const unitDefinitions = {}
-for (let def of unitList) {
-  const name = def.name
-  def = {
-    ...def,
-    baseName: def.dimension,
-    prefixes: 'pint'
-  }
-  unitDefinitions[name] = def
-  // Register abbreviations
-  if (def.abbreviation) {
-    unitToAbbreviationMap[name] = def.abbreviation
-    if (def.aliases) {
-      for (const alias of def.aliases) {
-        unitToAbbreviationMap[alias] = def.abbreviation
-      }
-    }
-  }
-}
-createUnit(unitDefinitions, {override: true})
-
-export const unitMap = Object.fromEntries(unitList.map(x => [x.name, x]))
-
-// Define the unit systems
-const SIUnits = {
-  // Base units
-  dimensionless: {name: 'dimensionless', fixed: false},
-  length: {name: 'meter', fixed: false},
-  mass: {name: 'kilogram', fixed: false},
-  time: {name: 'second', fixed: false},
-  current: {name: 'ampere', fixed: false},
-  temperature: {name: 'kelvin', fixed: false},
-  luminosity: {name: 'candela', fixed: false},
-  luminous_flux: {name: 'lumen', fixed: false},
-  substance: {name: 'mole', fixed: false},
-  angle: {name: 'radian', fixed: false},
-  information: {name: 'bit', fixed: false},
-  // Derived units with specific name
-  force: {name: 'newton', fixed: false},
-  energy: {name: 'joule', fixed: false},
-  power: {name: 'watt', fixed: false},
-  pressure: {name: 'pascal', fixed: false},
-  charge: {name: 'coulomb', fixed: false},
-  resistance: {name: 'ohm', fixed: false},
-  conductance: {name: 'siemens', fixed: false},
-  inductance: {name: 'henry', fixed: false},
-  magnetic_flux: {name: 'weber', fixed: false},
-  magnetic_field: {name: 'tesla', fixed: false},
-  frequency: {name: 'hertz', fixed: false},
-  luminance: {name: 'nit', fixed: false},
-  illuminance: {name: 'lux', fixed: false},
-  electric_potential: {name: 'volt', fixed: false},
-  capacitance: {name: 'farad', fixed: false},
-  activity: {name: 'katal', fixed: false}
-  // TODO: Derived units without a specific name. Cannot be registered as such
-  // as they don't have labels or separate definitions.
-  // area: {name: 'm^2', fixed: false},
-  // volume: {name: 'm^2', fixed: false},
-  // wavenumber: {name: '1 / m', fixed: false},
-  // speed: {name: 'm/s', fixed: false},
-  // acceleration: {name: 'm / s^2', fixed: false},
-  // density: {name: 'kg / m^3', fixed: false},
-  // viscosity: {name: 'Pa / s', fixed: false},
-  // kinematic_viscosity: {name: 'm^2 / s', fixed: false},
-  // fluidity: {name: '1 / Pa / s', fixed: false},
-  // concentration: {name: 'mol / m^3', fixed: false},
-  // entropy: {name: 'J / K', fixed: false},
-  // molar_entropy: {name: 'J / K / mole', fixed: false},
-  // electric_field: {name: 'V / m', fixed: false},
-  // intensity: {name: 'W / m^2', fixed: false},
-  // electric_dipole: {name: 'C m', fixed: false},
-  // electric_quadrupole: {name: 'C m^2', fixed: false},
-  // magnetic_dipole: {name: 'A m^2', fixed: false}
-}
-const SIUnitsFixed = cloneDeep(SIUnits)
-Object.values(SIUnitsFixed).forEach(value => { value.fixed = true })
-
-export const unitSystems = {
-  Custom: {
-    label: 'Custom',
-    description: 'Custom unit setup',
-    units: {
-      ...SIUnits,
-      length: {name: 'angstrom', fixed: false},
-      time: {name: 'femtosecond', fixed: false},
-      energy: {name: 'electron_volt', fixed: false},
-      pressure: {name: 'gigapascal', fixed: false},
-      angle: {name: 'degree', fixed: false}
-    }
-  },
-  SI: {
-    label: 'SI',
-    description: 'International System of Units (SI)',
-    units: SIUnitsFixed
-  },
-  AU: {
-    label: 'AU',
-    description: 'Hartree atomic units',
-    units: {
-      ...SIUnits,
-      time: {name: 'atomic_unit_of_time', fixed: true},
-      length: {name: 'bohr', fixed: true},
-      mass: {name: 'electron_mass', fixed: true},
-      current: {name: 'atomic_unit_of_current', fixed: true},
-      temperature: {name: 'atomic_unit_of_temperature', fixed: true},
-      force: {name: 'atomic_unit_of_force', fixed: true},
-      energy: {name: 'hartree', fixed: true},
-      pressure: {name: 'atomic_unit_of_pressure', fixed: true},
-      angle: {name: 'radian', fixed: true}
-    }
-  }
-}
-
-// Create a map of all units per dimension
-export const dimensionMap = {}
-for (const def of unitList) {
-  const name = def.name
-  const dimension = def.dimension
-  if (isNil(dimension)) {
-    continue
-  }
-  const oldInfo = dimensionMap[dimension] || {
-    label: startCase(toLower(dimension.replace('_', ' ')))
-  }
-  const oldList = oldInfo.units || []
-  oldList.push(name)
-  oldInfo.units = oldList
-  dimensionMap[dimension] = oldInfo
-}
-
-// Check that all units in the unit systems have been registered
-for (const [systemName, system] of Object.entries(unitSystems)) {
-  for (const [dimension, unit] of Object.entries(system.units)) {
-    if (isNil(UnitMathJS.UNITS[unit.name])) {
-      throw Error(`Unknown unit for dimension '${dimension}' found in system '${systemName}'`)
-    }
-  }
-}
-
-// A state containing the currently configured unit system.
-export const unitsState = atom({
-  key: 'units',
-  default: unitSystems[ui?.unit_systems?.selected || 'Custom'] || unitSystems.Custom
-})
-
-/**
- * Convenience hook for using the currently set units.
- * @returns Object containing the currently set units for each dimension (e.g.
- * {energy: 'joule'})
- */
-export const useUnits = () => {
-  const unitSystem = useRecoilValue(unitsState)
-  return unitSystem.units
-}
-
-/**
- * Helper class for persisting unit information.
- *
- * Builds upon the math.js Unit class system, but adds additional functionality,
- * including:
- *  - Ability to convert to any unit system given as an argument
- *  - Abbreviated labels for dense formatting
- */
-export class Unit {
-  /**
-   * @param {str | Unit} unit Unit for the quantity.
-   */
-  constructor(unit) {
-    if (isString(unit)) {
-      unit = this.normalizeExpression(unit)
-      unit = new UnitMathJS(undefined, unit)
-    } else if (unit instanceof Unit) {
-      unit = unit.mathjsUnit.clone()
-    } else if (unit instanceof UnitMathJS) {
-      unit = unit.clone()
-    } else {
-      throw Error('Please provide the unit as a string or as an instance of Unit.')
-    }
-    this.mathjsUnit = unit
-    // this._labelabbreviate = undefined
-    // this._label = undefined
-  }
-
-  /**
-   * Normalizes the given expression into a format that can be parsed by MathJS.
-   *
-   * This function will replace the Pint power symbol of '**' with the symbol
-   * '^' used by MathJS. In addition, we convert any 'delta'-units (see:
-   * https://pint.readthedocs.io/en/stable/nonmult.html) into their regular
-   * counterparts: MathJS will automatically ignore the offset when using
-   * non-multiplicative units in expressions.
-   *
-   * @param {str} expression Expression
-   * @returns string Expression in normalized form
-   */
-  normalizeExpression(expression) {
-    let normalized = expression.replace(/\*\*/g, '^')
-    normalized = normalized.replace(/delta_/g, '')
-    normalized = normalized.replace(/Δ/g, '')
-    return normalized
-  }
-
-  /**
-   * Checks if the given unit has the same base dimensions as this one.
-   * @param {str | Unit} unit Unit to compare to
-   * @returns boolean Whether the units have the same base dimensions.
-   */
-  equalBase(unit) {
-    if (isString(unit)) {
-      unit = this.normalizeExpression(unit)
-      unit = new Unit(unit)
-    }
-    return this.mathjsUnit.equalBase(unit.mathjsUnit)
-  }
-
-  /**
-   * Used to create a human-readable description of the unit as a string.
-   *
-   * @param {bool} abbreviate Whether to abbreviate the label using the
-   * abbreviations for each unit and prefix. If false, the original unit names
-   * (as given or defined by the unit system) are used.
-   * @returns A string representing the unit.
-   */
-  label(abbreviate = true) {
-    // TODO: The label caching is disabled for now. Because Quantities are
-    // stored as recoil.js atoms, they become immutable which causes problems
-    // with internal state mutation.
-    // if (this._labelabbreviate === abbreviate && this._label) {
-    //   return this._label
-    // }
-    const units = this.mathjsUnit.units
-    let strNum = ''
-    let strDen = ''
-    let nNum = 0
-    let nDen = 0
-
-    function getName(unit) {
-      if (unit.base.key === 'dimensionless') {
-        return ''
-      }
-      if (!abbreviate) {
-        return unit.name
-      }
-      return unitToAbbreviationMap?.[unit.name] || unit.name
-    }
-
-    function getPrefix(unit, original) {
-      if (!abbreviate) {
-        return original
-      }
-      const prefixMap = {
-        // SI
-        deca: 'da',
-        hecto: 'h',
-        kilo: 'k',
-        mega: 'M',
-        giga: 'G',
-        tera: 'T',
-        peta: 'P',
-        exa: 'E',
-        zetta: 'Z',
-        yotta: 'Y',
-        deci: 'd',
-        centi: 'c',
-        milli: 'm',
-        micro: 'u',
-        nano: 'n',
-        pico: 'p',
-        femto: 'f',
-        atto: 'a',
-        zepto: 'z',
-        yocto: 'y',
-        // IEC
-        kibi: 'Ki',
-        mebi: 'Mi',
-        gibi: 'Gi',
-        tebi: 'Ti',
-        pebi: 'Pi',
-        exi: 'Ei',
-        zebi: 'Zi',
-        yobi: 'Yi'
-      }
-      return prefixMap?.[original] || original
-    }
-
-    for (let i = 0; i < units.length; i++) {
-      if (units[i].power > 0) {
-        nNum++
-        const prefix = getPrefix(units[i].unit.name, units[i].prefix.name)
-        const name = getName(units[i].unit)
-        strNum += ` ${prefix}${name}`
-        if (Math.abs(units[i].power - 1.0) > 1e-15) {
-          strNum += '^' + units[i].power
-        }
-      } else if (units[i].power < 0) {
-        nDen++
-      }
-    }
-
-    if (nDen > 0) {
-      for (let i = 0; i < units.length; i++) {
-        if (units[i].power < 0) {
-          const prefix = getPrefix(units[i].unit.name, units[i].prefix.name)
-          const name = getName(units[i].unit)
-          if (nNum > 0) {
-            strDen += ` ${prefix}${name}`
-            if (Math.abs(units[i].power + 1.0) > 1e-15) {
-              strDen += '^' + (-units[i].power)
-            }
-          } else {
-            strDen += ` ${prefix}${name}`
-            strDen += '^' + (units[i].power)
-          }
-        }
-      }
-    }
-    // Remove leading whitespace
-    strNum = strNum.substr(1)
-    strDen = strDen.substr(1)
-
-    // Add parentheses for better copy/paste back into evaluate, for example, or
-    // for better pretty print formatting
-    if (nNum > 1 && nDen > 0) {
-      strNum = '(' + strNum + ')'
-    }
-    if (nDen > 1 && nNum > 0) {
-      strDen = '(' + strDen + ')'
-    }
-
-    let str = strNum
-    if (nNum > 0 && nDen > 0) {
-      str += ' / '
-    }
-    str += strDen
-
-    // this._labelabbreviate = abbreviate
-    // this._label = str
-    return str
-  }
-
-  /**
-   * Gets the dimension of this unit as a string. The order of the dimensions is
-   * fixed (determined at unit registration time).
-   *
-   * @param {boolean} base Whether to return dimension in base units. Otherwise
-   * the original unit dimensions are used.
-   * @returns The dimensionality as a string, e.g. 'time^2 energy mass^-2'
-   */
-  dimension(base = true) {
-    const dimensions = Object.keys(UnitMathJS.BASE_UNITS)
-    const dimensionMap = Object.fromEntries(dimensions.map(name => [name, 0]))
-
-    if (base) {
-      const BASE_DIMENSIONS = UnitMathJS.BASE_DIMENSIONS
-      for (let i = 0; i < BASE_DIMENSIONS.length; ++i) {
-        const power = this?.mathjsUnit.dimensions?.[i]
-        if (power) {
-          dimensionMap[BASE_DIMENSIONS[i]] += power
-        }
-      }
-    } else {
-      for (const unit of this?.mathjsUnit.units) {
-        const power = unit.power
-        if (power) {
-          dimensionMap[unit.unit.base.key] += power
-        }
-      }
-    }
-    return Object.entries(dimensionMap)
-      .filter(d => d[1] !== 0)
-      .map(d => `${d[0]}${((d[1] < 0 || d[1] > 1) && `^${d[1]}`) || ''}`).join(' ')
-  }
-
-  /**
-   * Function for converting to another unit.
-   *
-   * @param {str | Unit} unit The target unit
-   * @returns A new Unit expressed in the given units.
-   */
-  to(unit) {
-    if (isString(unit)) {
-      unit = this.normalizeExpression(unit)
-    } else if (unit instanceof Unit) {
-      unit = unit.label()
-    } else {
-      throw Error('Unknown unit type. Please provide the unit as as string or as instance of Unit.')
-    }
-
-    // We cannot directly feed the unit string into Math.js, because it will try
-    // to parse units like 1/<unit> as Math.js units which have values, and then
-    // will raise an exception when converting between valueless and valued
-    // unit. The workaround is to explicitly define a valueless unit.
-    unit = new UnitMathJS(undefined, unit)
-    return new Unit(this.mathjsUnit.to(unit))
-  }
-
-  /**
-   * Function for converting the value of this Unit to the SI unit system.
-   *
-   * @returns A new Unit instance in the SI unit system.
-   */
-  toSI() {
-    return this.toSystem(unitSystems.SI.units)
-  }
-
-  /**
-   * Function for converting the value of this unit to another unit system.
-   *
-   * Notice that converting a unit to another unit system is not as easy as
-   * conversions to a specific unit. When converting to a specific unit one can
-   * simply check that the dimensions match and go ahead with the conversion.
-   * With unit systems, there can be multiple alternative forms, and choosing a
-   * good one is more difficult. E.g. should 'a_u_force * angstrom' be converted
-   * into:
-   *
-   * a) N m
-   * b) J
-   * c) (kg m^2) / s^2
-   *
-   * By default this function will try to preserve the original unit dimensions
-   * and not convert everything down to base units. If a derived unit is not
-   * present, it will, however, attempt to convert it to the base units. Any
-   * further simplication is not performed.
-   *
-   * @param {object} system The target unit system.
-   * @returns A new Unit instance in the given system.
-   */
-  toSystem(system) {
-    // Go through the currently defined units, identify their dimension and look
-    // for the corresponding dimension in the given unit system. If one is
-    // present, convert to it. Otherwise convert to base dimensions.
-    const UNITS = UnitMathJS.UNITS
-    const PREFIXES = UnitMathJS.PREFIXES
-    const BASE_DIMENSIONS = UnitMathJS.BASE_DIMENSIONS
-    const BASE_UNITS = UnitMathJS.BASE_UNITS
-    const proposedUnitList = []
-    for (const unit of this.mathjsUnit.units) {
-      const dimension = unit.unit.base.key
-      const newUnitName = system?.[dimension]?.name
-      const newUnit = UNITS[newUnitName]
-      // If the unit for this dimension is defined, use it
-      if (!isNil(newUnitName)) {
-        proposedUnitList.push({
-          unit: newUnit,
-          prefix: PREFIXES.NONE[''],
-          power: unit.power || 0
-        })
-      // Otherwise convert to base units
-      } else {
-        let missingBaseDim = false
-        const baseUnit = BASE_UNITS[dimension]
-        const newDimensions = baseUnit.dimensions
-        for (let i = 0; i < BASE_DIMENSIONS.length; i++) {
-          const baseDim = BASE_DIMENSIONS[i]
-          if (Math.abs(newDimensions[i] || 0) > 1e-12) {
-            if (has(system, baseDim)) {
-              proposedUnitList.push({
-                unit: UNITS[system[baseDim].name],
-                prefix: PREFIXES.NONE[''],
-                power: unit.power ? newDimensions[i] * unit.power : 0
-              })
-            } else {
-              missingBaseDim = true
-            }
-          }
-        }
-        if (missingBaseDim) {
-          throw Error(`The given unit system does not contain the required unit definitions for converting ${unit.name} with dimension ${dimension}.`)
-        }
-      }
-    }
-
-    const ret = this.mathjsUnit.clone()
-    ret.units = proposedUnitList
-    return new Unit(ret)
-  }
-}
-
-/**
- * Class for persisting persisting a numeric value together with unit
- * information.
- */
-export class Quantity {
-  /**
-   * @param {number | n-dimensional array of numbers} value Numeric value. See
-   * also the argument 'normalized'.
-   * @param {str | Unit} unit Unit for the quantity.
-   * @param {boolean} normalized Whether the given numeric value is already
-   * normalized to base units.
-   */
-  constructor(value, unit, normalized = false) {
-    this.unit = new Unit(unit)
-    if (!isNumber(value) && !isArray(value)) {
-      throw Error('Please provide the the value as a number, or as a multidimensional array of numbers.')
-    }
-
-    // This attribute stores the quantity value in 'normalized' form that is
-    // given in the base units (=SI). This value should only be determined once
-    // during the unit initialization and all calls to value() will then lazily
-    // determine the value in the currently set units. This avoids 'drift' in
-    // the value caused by several consecutive changes of the units.
-    this.normalized_value = normalized ? value : this.normalize(value)
-  }
-
-  /**
-   * Get value in current units.
-   * @returns The numeric value in the currently set units.
-   */
-  value() {
-    return this.denormalize(this.normalized_value)
-  }
-
-  /**
-   * Convert value from currently set units to base units.
-   * @param {n-dimensional array} value Value in currently set units.
-   * @returns Value in base units.
-   */
-  normalize(value) {
-    return mapDeep(value, (x) => this.unit.mathjsUnit._normalize(x))
-  }
-
-  /**
-   * Convert value from base units to currently set units.
-   * @param {n-dimensional array} value Value in base units.
-   * @returns Value in currently set units.
-   */
-  denormalize(value) {
-    return mapDeep(value, (x) => this.unit.mathjsUnit._denormalize(x))
-  }
-
-  label() {
-    return this.unit.label()
-  }
-
-  dimension(base) {
-    return this.unit.dimension(base)
-  }
-
-  to(unit) {
-    return new Quantity(this.normalized_value, this.unit.to(unit), true)
-  }
-
-  toSI() {
-    return new Quantity(this.normalized_value, this.unit.toSI(), true)
-  }
-
-  toSystem(system) {
-    return new Quantity(this.normalized_value, this.unit.toSystem(system), true)
-  }
-
-  /**
-   * Checks if the given Quantity is equal to this one.
-   * @param {Quantity} quantity Quantity to compare to
-   * @returns boolean Whether quantities are equal
-   */
-  equal(quantity) {
-    if (quantity instanceof Quantity) {
-      return this.normalized_value === quantity.normalized_value && this.unit.equalBase(quantity.unit)
-    } else {
-      throw Error('The given value is not an instance of Quantity.')
-    }
-  }
-}
-
-/**
- * Convenience function for getting compatible units for a given dimension.
- * Returns all compatible units that have been registered.
- *
- * @param {string} dimension The dimension.
- * @returns Array of compatible units.
- */
-export function getUnits(dimension) {
-  return dimensionMap?.[dimension]?.units || []
-}
-
-/**
- * Convenience function for parsing unit information from a string.
- *
- * @param {boolean} requireValue Whether a value is required.
- * @param {boolean} requireUnit Whether a unit is required.
- * @param {string} dimension Dimension for the unit. Nil value means a
- * dimensionless unit.
- * @returns Object containing the following properties, if available:
- *  - value: Numerical value as a number
- *  - valueString: Numerical value as a string
- *  - unit: Unit instance
- *  - unitString: Unit as a string
- *  - error: Error messsage
- */
-export function parseQuantity(input, requireValue = true, requireUnit = true, dimension = undefined) {
-  input = input.trim()
-  const valueString = input.match(/^[+-]?((\d+\.\d+|\d+\.|\.\d?|\d+)(e|e\+|e-)\d+|(\d+\.\d+|\d+\.|\.\d?|\d+))?/)?.[0]
-  if (requireValue && isNil(valueString)) {
-    return {error: 'Enter a valid numerical value'}
-  }
-  const value = Number(valueString)
-  const unitString = input.substring(valueString.length).trim()
-  const dim = isNil(dimension) ? 'dimensionless' : dimension
-  if (unitString === '' && dim !== 'dimensionless' && requireUnit) {
-    return {value, valueString, unitString, error: 'Enter a valid unit'}
-  }
-  if (unitString === '' && !requireUnit) {
-    return {value, valueString, unitString}
-  }
-  if (dim === 'dimensionless' && unitString !== '') {
-    return {value, valueString, unitString, error: 'Enter a numerical value without units'}
-  }
-  let unit
-  try {
-    unit = new Unit(dim === 'dimensionless' ? 'dimensionless' : input)
-  } catch {
-    return {valueString, value, unitString, error: `Unknown unit '${unitString}'`}
-  }
-  if (unit.dimension(false) !== dimension) {
-    return {valueString, value, unitString, unit, error: `Incompatible unit`}
-  }
-  return {value, valueString, unit, unitString}
-}
diff --git a/gui/src/utils.js b/gui/src/utils.js
index b00185f69d338931442c443e6a278d6036080b97..b082f11a8aa109004e89d64bdca7bd510a64996c 100644
--- a/gui/src/utils.js
+++ b/gui/src/utils.js
@@ -17,7 +17,7 @@
  */
 import minimatch from 'minimatch'
 import { cloneDeep, merge, isSet, isNil, isArray, isString, isNumber, isPlainObject, startCase, isEmpty } from 'lodash'
-import { Quantity } from './units'
+import { Quantity } from './components/units/Quantity'
 import { format } from 'date-fns'
 import { dateFormat, guiBase, apiBase, searchQuantities, parserMetadata, schemaSeparator, dtypeSeparator, yamlSchemaPrefix } from './config'
 const crypto = require('crypto')
@@ -605,10 +605,10 @@ export function getDeserializer(dtype, dimension) {
           throw Error(`Could not parse the number ${split[0]}`)
         }
         return split.length === 1
-          ? new Quantity(value, units?.[dimension]?.name || 'dimensionless')
+          ? new Quantity(value, units?.[dimension]?.definition || 'dimensionless')
           : new Quantity(value, split[1])
       } if (isNumber(value)) {
-        return new Quantity(value, units?.[dimension]?.name || 'dimensionless')
+        return new Quantity(value, units?.[dimension]?.definition || 'dimensionless')
       }
       return value
     }
@@ -1382,9 +1382,9 @@ export const alphabet = [
  * Function for creating suggestions functionality for a list of string values.
  * Mimics the suggestion logic used by the suggestions API endpoint.
  *
- * @param {str} category Category for the suggestions
  * @param {array} values Array of available values
  * @param {number} minLength Minimum input length before suggestions are considered.
+ * @param {str} category Category for the suggestions
  * @param {func} text Function that maps the value into the suggested text input
  *
  * @return {object} Object containing a list of options and a function for
diff --git a/gui/tests/artifacts.js b/gui/tests/artifacts.js
index c9734af9aa755a022fdc8611bdd3a14eb50ff6bd..8e32f71876f325cb141df468d3840f280db09846 100644
--- a/gui/tests/artifacts.js
+++ b/gui/tests/artifacts.js
@@ -78436,14 +78436,6 @@ window.nomadArtifacts = {
       "definition": "96485.33212331001 ampere * second",
       "offset": 0.0
     },
-    {
-      "name": "femtosecond",
-      "dimension": "time",
-      "label": "Femtosecond",
-      "abbreviation": "fs",
-      "definition": "1e-15 second",
-      "offset": 0.0
-    },
     {
       "name": "fermi",
       "dimension": "length",
@@ -78566,14 +78558,6 @@ window.nomadArtifacts = {
       "definition": "0.0001 kilogram / ampere / second ^ 2",
       "offset": 0.0
     },
-    {
-      "name": "gigapascal",
-      "dimension": "pressure",
-      "label": "Gigapascal",
-      "abbreviation": "GPa",
-      "definition": "1000000000.0 kilogram / meter / second ^ 2",
-      "offset": 0.0
-    },
     {
       "name": "grade",
       "dimension": "angle",
@@ -78989,14 +78973,6 @@ window.nomadArtifacts = {
       "definition": "4.84813681109536e-09 radian",
       "offset": 0.0
     },
-    {
-      "name": "millibar",
-      "dimension": "pressure",
-      "label": "Millibar",
-      "abbreviation": "mbar",
-      "definition": "100.0 kilogram / meter / second ^ 2",
-      "offset": 0.0
-    },
     {
       "name": "millimeter_Hg",
       "dimension": "pressure",
diff --git a/gui/tests/env.js b/gui/tests/env.js
index bef29869bc344da70ea4c6dcf4ced65a6d3bcba0..b8819f67867beb497dd99b339daddd33e4184008 100644
--- a/gui/tests/env.js
+++ b/gui/tests/env.js
@@ -19,6 +19,347 @@ window.nomadEnv = {
       "title": "NOMAD"
     },
     "unit_systems": {
+      "options": {
+        "Custom": {
+          "label": "Custom",
+          "units": {
+            "length": {
+              "definition": "\u00c5",
+              "locked": false
+            },
+            "time": {
+              "definition": "fs",
+              "locked": false
+            },
+            "energy": {
+              "definition": "eV",
+              "locked": false
+            },
+            "pressure": {
+              "definition": "GPa",
+              "locked": false
+            },
+            "angle": {
+              "definition": "\u00b0",
+              "locked": false
+            },
+            "dimensionless": {
+              "definition": "dimensionless",
+              "locked": false
+            },
+            "mass": {
+              "definition": "kg",
+              "locked": false
+            },
+            "current": {
+              "definition": "A",
+              "locked": false
+            },
+            "temperature": {
+              "definition": "K",
+              "locked": false
+            },
+            "luminosity": {
+              "definition": "cd",
+              "locked": false
+            },
+            "luminous_flux": {
+              "definition": "lm",
+              "locked": false
+            },
+            "substance": {
+              "definition": "mol",
+              "locked": false
+            },
+            "information": {
+              "definition": "bit",
+              "locked": false
+            },
+            "force": {
+              "definition": "N",
+              "locked": false
+            },
+            "power": {
+              "definition": "W",
+              "locked": false
+            },
+            "charge": {
+              "definition": "C",
+              "locked": false
+            },
+            "resistance": {
+              "definition": "\u03a9",
+              "locked": false
+            },
+            "conductance": {
+              "definition": "S",
+              "locked": false
+            },
+            "inductance": {
+              "definition": "H",
+              "locked": false
+            },
+            "magnetic_flux": {
+              "definition": "Wb",
+              "locked": false
+            },
+            "magnetic_field": {
+              "definition": "T",
+              "locked": false
+            },
+            "frequency": {
+              "definition": "Hz",
+              "locked": false
+            },
+            "luminance": {
+              "definition": "nit",
+              "locked": false
+            },
+            "illuminance": {
+              "definition": "lx",
+              "locked": false
+            },
+            "electric_potential": {
+              "definition": "V",
+              "locked": false
+            },
+            "capacitance": {
+              "definition": "F",
+              "locked": false
+            },
+            "activity": {
+              "definition": "kat",
+              "locked": false
+            }
+          }
+        },
+        "SI": {
+          "label": "International System of Units (SI)",
+          "units": {
+            "dimensionless": {
+              "definition": "dimensionless",
+              "locked": true
+            },
+            "length": {
+              "definition": "m",
+              "locked": true
+            },
+            "mass": {
+              "definition": "kg",
+              "locked": true
+            },
+            "time": {
+              "definition": "s",
+              "locked": true
+            },
+            "current": {
+              "definition": "A",
+              "locked": true
+            },
+            "temperature": {
+              "definition": "K",
+              "locked": true
+            },
+            "luminosity": {
+              "definition": "cd",
+              "locked": true
+            },
+            "luminous_flux": {
+              "definition": "lm",
+              "locked": true
+            },
+            "substance": {
+              "definition": "mol",
+              "locked": true
+            },
+            "angle": {
+              "definition": "rad",
+              "locked": true
+            },
+            "information": {
+              "definition": "bit",
+              "locked": true
+            },
+            "force": {
+              "definition": "N",
+              "locked": true
+            },
+            "energy": {
+              "definition": "J",
+              "locked": true
+            },
+            "power": {
+              "definition": "W",
+              "locked": true
+            },
+            "pressure": {
+              "definition": "Pa",
+              "locked": true
+            },
+            "charge": {
+              "definition": "C",
+              "locked": true
+            },
+            "resistance": {
+              "definition": "\u03a9",
+              "locked": true
+            },
+            "conductance": {
+              "definition": "S",
+              "locked": true
+            },
+            "inductance": {
+              "definition": "H",
+              "locked": true
+            },
+            "magnetic_flux": {
+              "definition": "Wb",
+              "locked": true
+            },
+            "magnetic_field": {
+              "definition": "T",
+              "locked": true
+            },
+            "frequency": {
+              "definition": "Hz",
+              "locked": true
+            },
+            "luminance": {
+              "definition": "nit",
+              "locked": true
+            },
+            "illuminance": {
+              "definition": "lx",
+              "locked": true
+            },
+            "electric_potential": {
+              "definition": "V",
+              "locked": true
+            },
+            "capacitance": {
+              "definition": "F",
+              "locked": true
+            },
+            "activity": {
+              "definition": "kat",
+              "locked": true
+            }
+          }
+        },
+        "AU": {
+          "label": "Hartree atomic units (AU)",
+          "units": {
+            "dimensionless": {
+              "definition": "dimensionless",
+              "locked": true
+            },
+            "length": {
+              "definition": "bohr",
+              "locked": true
+            },
+            "mass": {
+              "definition": "m_e",
+              "locked": true
+            },
+            "time": {
+              "definition": "atomic_unit_of_time",
+              "locked": true
+            },
+            "current": {
+              "definition": "atomic_unit_of_current",
+              "locked": true
+            },
+            "temperature": {
+              "definition": "atomic_unit_of_temperature",
+              "locked": true
+            },
+            "luminosity": {
+              "definition": "cd",
+              "locked": false
+            },
+            "luminous_flux": {
+              "definition": "lm",
+              "locked": false
+            },
+            "substance": {
+              "definition": "mol",
+              "locked": false
+            },
+            "angle": {
+              "definition": "rad",
+              "locked": false
+            },
+            "information": {
+              "definition": "bit",
+              "locked": false
+            },
+            "force": {
+              "definition": "atomic_unit_of_force",
+              "locked": true
+            },
+            "energy": {
+              "definition": "Ha",
+              "locked": true
+            },
+            "power": {
+              "definition": "W",
+              "locked": false
+            },
+            "pressure": {
+              "definition": "atomic_unit_of_pressure",
+              "locked": true
+            },
+            "charge": {
+              "definition": "C",
+              "locked": false
+            },
+            "resistance": {
+              "definition": "\u03a9",
+              "locked": false
+            },
+            "conductance": {
+              "definition": "S",
+              "locked": false
+            },
+            "inductance": {
+              "definition": "H",
+              "locked": false
+            },
+            "magnetic_flux": {
+              "definition": "Wb",
+              "locked": false
+            },
+            "magnetic_field": {
+              "definition": "T",
+              "locked": false
+            },
+            "frequency": {
+              "definition": "Hz",
+              "locked": false
+            },
+            "luminance": {
+              "definition": "nit",
+              "locked": false
+            },
+            "illuminance": {
+              "definition": "lx",
+              "locked": false
+            },
+            "electric_potential": {
+              "definition": "V",
+              "locked": false
+            },
+            "capacitance": {
+              "definition": "F",
+              "locked": false
+            },
+            "activity": {
+              "definition": "kat",
+              "locked": false
+            }
+          }
+        }
+      },
       "selected": "Custom"
     },
     "entry": {
diff --git a/gui/yarn.lock b/gui/yarn.lock
index 4e3b8b13818127d494749358f3cf600a1e9389ad..856f5ebd87c3d956d1f1e0cfc84b4474fbd0337a 100644
--- a/gui/yarn.lock
+++ b/gui/yarn.lock
@@ -2003,6 +2003,14 @@
     lodash "^4.17.15"
     redent "^3.0.0"
 
+"@testing-library/react-hooks@^8.0.1":
+  version "8.0.1"
+  resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12"
+  integrity sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    react-error-boundary "^3.1.0"
+
 "@testing-library/react@^12.1.5":
   version "12.1.5"
   resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.5.tgz#bb248f72f02a5ac9d949dea07279095fa577963b"
@@ -13556,6 +13564,13 @@ react-error-boundary@4.0.3:
   dependencies:
     "@babel/runtime" "^7.12.5"
 
+react-error-boundary@^3.1.0:
+  version "3.1.4"
+  resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0"
+  integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+
 react-error-overlay@6.0.9, react-error-overlay@^6.0.9:
   version "6.0.9"
   resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
diff --git a/nomad/app/v1/routers/systems.py b/nomad/app/v1/routers/systems.py
index 33728d94e98a15f437d2ba4bb56e819bed3b6de2..1aeeb7af5d0826245ebb78e1007a18d8768e5156 100644
--- a/nomad/app/v1/routers/systems.py
+++ b/nomad/app/v1/routers/systems.py
@@ -15,7 +15,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
-from typing import Union, Dict, List
+from typing import Dict, List, Union
 from io import StringIO, BytesIO
 from collections import OrderedDict
 from enum import Enum
@@ -30,7 +30,7 @@ from MDAnalysis.coordinates.PDB import PDBWriter
 
 from nomad.units import ureg
 from nomad.utils import strip, deep_get, query_list_to_dict
-from nomad.atomutils import Formula
+from nomad.atomutils import Formula, wrap_positions, unwrap_positions
 from nomad.normalizing.common import ase_atoms_from_nomad_atoms, mda_universe_from_nomad_atoms
 from nomad.datamodel.metainfo.simulation.system import Atoms as NOMADAtoms
 from .entries import answer_entry_archive_request
@@ -183,6 +183,25 @@ class TempFormatEnum(str, Enum):
 FormatEnum = TempFormatEnum("FormatEnum", {format: format for format in format_map.keys()})  # type: ignore
 
 
+wrap_mode_map: Dict[str, dict] = OrderedDict({
+    'original': {'description': 'The original positions as set in the data'},
+    'wrap': {'description': 'Positions are wrapped to be inside the cell respecting periodic boundary conditions'},
+    'unwrap': {'description': strip('''
+        Positions are reconstructed so that the structure is not split by
+        periodic cell boundaries. Note that this produces meaningful results
+        only if the system dimensions are smaller than the unit cell.
+    ''')},
+})
+
+
+class TempWrapModeEnum(str, Enum):
+    pass
+
+
+WrapModeEnum = TempWrapModeEnum("WrapModeEnum", {wrap_mode: wrap_mode for wrap_mode in wrap_mode_map.keys()})  # type: ignore
+wrap_mode_description = "\n".join([f'- `{key}`: {value["description"]}' for [key, value] in wrap_mode_map.items()])
+
+
 _file_response = status.HTTP_200_OK, {
     'content': {'application/octet-stream': {}},
     'description': strip('''
@@ -237,10 +256,21 @@ Here is a brief rundown of the different features each format supports:
 
 {format_features}'''
         ),
+        wrap_mode: WrapModeEnum = Query(  # type: ignore
+            default=WrapModeEnum.original,
+            description=f'''Determines how to handle atomic positions for the requested system. The available options are:
+
+{wrap_mode_description}
+
+            '''),
         user: User = Depends(create_user_dependency(signature_token_auth_allowed=True))):
     '''
     Build and retrieve a structure file containing an atomistic system stored
-    within an entry. Note that some formats are more restricted and cannot fully
+    within an entry.
+
+    All length dimensions in the structure files are in Ångstroms (=1e-10 meter).
+
+    Note that some formats are more restricted and cannot fully
     describe certains kinds of systems. For examples some entries within NOMAD
     do not contain a unit cell (e.g. molecules), whereas some formats require it
     to be present.
@@ -323,6 +353,20 @@ Here is a brief rundown of the different features each format supports:
         except Exception:
             pass
 
+        # Handle wrap mode
+        if wrap_mode == WrapModeEnum.wrap:
+            atoms.positions = wrap_positions(
+                atoms.positions.magnitude,
+                atoms.lattice_vectors.magnitude,
+                atoms.periodic
+            )
+        elif wrap_mode == WrapModeEnum.unwrap:
+            atoms.positions = unwrap_positions(
+                atoms.positions.magnitude,
+                atoms.lattice_vectors.magnitude,
+                atoms.periodic
+            )
+
         content = format_info['writer'](atoms, entry_id, formula)
     except Exception as e:
         raise HTTPException(
diff --git a/nomad/atomutils.py b/nomad/atomutils.py
index a584e5ec2b432fadf88dc2d777f2f3c02c8eadb1..933720a572016a260fbceae2363a47cc4b3cf58f 100644
--- a/nomad/atomutils.py
+++ b/nomad/atomutils.py
@@ -112,51 +112,158 @@ def is_valid_basis(basis: NDArray[Any]) -> bool:
     return True
 
 
+def translate_pretty(
+        fractional: NDArray[Any],
+        pbc: Union[bool, NDArray[Any]]) -> NDArray[Any]:
+    """Translates atoms such that fractional positions are minimized."""
+    pbc = pbc2pbc(pbc)
+
+    for i in range(3):
+        if not pbc[i]:
+            continue
+
+        indices = np.argsort(fractional[:, i])
+        sp = fractional[indices, i]
+
+        widths = (np.roll(sp, 1) - sp) % 1.0
+        fractional[:, i] -= sp[np.argmin(widths)]
+        fractional[:, i] %= 1.0
+    return fractional
+
+
+def get_center_of_positions(
+        positions: NDArray[Any],
+        cell: NDArray[Any] = None,
+        pbc: Union[bool, NDArray[Any]] = True,
+        weights=None,
+        relative=False) -> NDArray[Any]:
+    """Calculates the center of positions with the given weighting. Also takes
+    the periodicity of the system into account.
+
+    The algorithm is replicated from:
+    https://en.wikipedia.org/wiki/Center_of_mass#Systems_with_periodic_boundary_conditions
+
+    Args:
+        positions: Positions of the atoms. Whether these are cartesian or
+            relative is controlled by the 'relative' argument.
+        cell: Unit cell
+        pbc: Periodic boundary conditions
+        relative: If true, the input and output positions are given relative to
+            the unit cell. Otherwise the positions are cartesian.
+
+    Returns:
+        The position of the center of mass in the given system.
+    """
+    pbc = pbc2pbc(pbc)
+    relative_positions = positions if relative else to_scaled(positions, cell)
+
+    rel_com = np.zeros((1, 3))
+    for i_comp in range(3):
+        i_pbc = pbc[i_comp]
+        i_pos = relative_positions[:, i_comp]
+        if i_pbc:
+            theta = i_pos * 2 * np.pi
+            xi = np.cos(theta)
+            zeta = np.sin(theta)
+            if weights:
+                xi *= weights
+                zeta *= weights
+
+            xi_mean = np.mean(xi)
+            zeta_mean = np.mean(zeta)
+
+            mean_theta = np.arctan2(-zeta_mean, -xi_mean) + np.pi
+            com_rel = mean_theta / (2 * np.pi)
+            rel_com[0, i_comp] = com_rel
+        else:
+            if weights:
+                total_weight = np.sum(weights)
+                rel_com[0, i_comp] = np.sum(i_pos * weights) / total_weight
+            else:
+                rel_com[0, i_comp] = np.sum(i_pos)
+
+    return rel_com if relative else to_cartesian(rel_com, cell)
+
+
 def wrap_positions(
         positions: NDArray[Any],
         cell: NDArray[Any] = None,
         pbc: Union[bool, NDArray[Any]] = True,
         center: NDArray[Any] = [0.5, 0.5, 0.5],
-        eps: float = 1e-12) -> NDArray[Any]:
+        pretty_translation=False,
+        eps: float = 1e-12,
+        relative=False) -> NDArray[Any]:
     '''
-    Wraps the given position so that they are within the unit cell. If no
-    cell is given, scaled positions are assumed. For wrapping cartesian
-    positions you also need to provide the cell.
+    Wraps the given position so that they are within the unit cell.
 
     Args:
-        positions: Positions of the atoms. Accepts both scaled and
-            cartesian positions.
-        cell: Lattice vectors for wrapping cartesian positions.
+        positions: Positions of the atoms. Whether these are cartesian or
+            relative is controlled by the 'relative' argument.
+        cell: Lattice vectors.
         pbc: For each axis in the unit cell decides whether the positions will
             be wrapped along this axis.
         center: The position in fractional coordinates that the wrapped
             positions will be nearest possible to.
         eps: Small number to prevent slightly negative coordinates from being
             wrapped.
+        relative: If true, the input and output positions are given relative to
+            the unit cell. Otherwise the positions are cartesian.
     '''
-    if not hasattr(center, '__len__'):
-        center = (center,) * 3
-
     pbc = pbc2pbc(pbc)
     shift = np.asarray(center) - 0.5 - eps
 
     # Don't change coordinates when pbc is False
     shift[np.logical_not(pbc)] = 0.0
 
-    if cell is None:
-        fractional = positions
-    else:
-        fractional = to_scaled(positions, cell)
-    fractional -= shift
-
-    for i, periodic in enumerate(pbc):
-        if periodic:
-            fractional[:, i] %= 1.0
-        fractional[:, i] += shift[i]
-    if cell is not None:
-        return np.dot(fractional, cell)
+    relative_pos = positions if relative else to_scaled(positions, cell)
+    relative_pos -= shift
+
+    if pretty_translation:
+        relative_pos = translate_pretty(relative_pos, pbc)
+        shift = np.asarray(center) - 0.5
+        shift[np.logical_not(pbc)] = 0.0
+        relative_pos += shift
     else:
-        return fractional
+        for i, periodic in enumerate(pbc):
+            if periodic:
+                relative_pos[:, i] %= 1.0
+                relative_pos[:, i] += shift[i]
+
+    return relative_pos if relative else to_cartesian(relative_pos, cell)
+
+
+def unwrap_positions(
+        positions: NDArray[Any],
+        cell: NDArray[Any],
+        pbc: Union[bool, NDArray[Any]],
+        relative=False) -> NDArray[Any]:
+    '''
+    Unwraps the given positions so that continuous structures are not broken by
+    cell boundaries.
+
+    Args:
+        positions: Positions of the atoms. Whether these are cartesian or
+            relative is controlled by the 'relative' argument.
+        cell: Lattice vectors.
+        pbc: For each axis in the unit cell decides whether the positions will
+            be wrapped along this axis.
+        center: The position in fractional coordinates that the wrapped
+            positions will be nearest possible to.
+        eps: Small number to prevent slightly negative coordinates from being
+            wrapped.
+        relative: If true, the input and output positions are given relative to
+            the unit cell. Otherwise the positions are cartesian.
+    '''
+    pbc = pbc2pbc(pbc)
+    if not any(pbc):
+        return positions
+
+    relative_pos = positions if relative else to_scaled(positions, cell)
+    center_of_pos = get_center_of_positions(relative_pos, pbc=pbc, relative=True)
+    relative_shifted = relative_pos + ([[0.5, 0.5, 0.5]] - center_of_pos)
+    wrapped_relative_pos = wrap_positions(relative_shifted, pbc=pbc, relative=True)
+
+    return wrapped_relative_pos if relative else to_cartesian(wrapped_relative_pos, cell)
 
 
 def chemical_symbols(atomic_numbers: Iterable[int]) -> List[str]:
diff --git a/nomad/cli/dev.py b/nomad/cli/dev.py
index 3540230f6c61b12bc4745427b89a30022286622f..daa14ebf65d058576a089bd01e31b083ff54b5dc 100644
--- a/nomad/cli/dev.py
+++ b/nomad/cli/dev.py
@@ -482,27 +482,6 @@ def _generate_units_json(all_metainfo) -> Tuple[Any, Any]:
 
     # Some units need to be added manually.
     unit_list.extend([
-        # Gigapascal
-        {
-            'name': 'gigapascal',
-            'dimension': 'pressure',
-            'label': 'Gigapascal',
-            'abbreviation': 'GPa',
-        },
-        # Millibar
-        {
-            'name': 'millibar',
-            'dimension': 'pressure',
-            'label': 'Millibar',
-            'abbreviation': 'mbar',
-        },
-        # Femtosecond
-        {
-            'name': 'femtosecond',
-            'dimension': 'time',
-            'label': 'Femtosecond',
-            'abbreviation': 'fs',
-        },
         # Kilogram as SI base unit
         {
             'name': 'kilogram',
diff --git a/nomad/config/__init__.py b/nomad/config/__init__.py
index f69e06da3760fa76315268f41d0e16aea0ec695f..cec9942fb619587d3f7cccce91520e3ea8fbcec8 100644
--- a/nomad/config/__init__.py
+++ b/nomad/config/__init__.py
@@ -493,7 +493,7 @@ def _check_config():
     ui.north_base = f'{"https" if services.https else "http"}://{north.hub_host}:{north.hub_port}{services.api_base_path.rstrip("/")}/north'
 
 
-def _merge(a: Union[dict, BaseModel], b: Union[dict, BaseModel], path: List[str] = None) -> Union[dict, BaseModel]:
+def _merge(a: Union[dict, BaseModel], b: Union[dict, BaseModel]) -> Union[dict, BaseModel]:
     '''
     Recursively merges b into a. Will add new key-value pairs, and will
     overwrite existing key-value pairs. Notice that this mutates the original
@@ -513,13 +513,17 @@ def _merge(a: Union[dict, BaseModel], b: Union[dict, BaseModel], path: List[str]
     def get(target, key):
         return target[key] if isinstance(target, dict) else getattr(target, key)
 
-    if path is None: path = []
-    for key in b.__dict__ if isinstance(b, BaseModel) else b:
+    # None values are ignored
+    if b is None:
+        return a
+    for key in (b.__dict__ if isinstance(b, BaseModel) else b):
         value = get(b, key)
         if has(a, key):
             child = get(a, key)
-            if isinstance(value, (BaseModel, dict)) and isinstance(child, (BaseModel, dict)):
-                _merge(child, value, path + [str(key)])
+            # Objects are merged
+            if isinstance(value, (BaseModel, dict)) or isinstance(child, (BaseModel, dict)):
+                _merge(child, value)
+            # Other types are replaced
             else:
                 set(a, key, value)
         else:
diff --git a/nomad/config/models.py b/nomad/config/models.py
index 89fdb8cd62494f39d4f17be9c1af7fef64a41a28..16b3cad8e7c07e281db1d8cfcc5eac3c3467cd14 100644
--- a/nomad/config/models.py
+++ b/nomad/config/models.py
@@ -105,7 +105,7 @@ class Options(OptionsBase):
     '''Common configuration class used for enabling/disabling certain
     elements and defining the configuration of each element.
     '''
-    options: Dict[str, Any] = Field({}, description='Contains the available options.')
+    options: Optional[Dict[str, Any]] = Field({}, description='Contains the available options.')
 
     def filtered_keys(self) -> List[str]:
         '''Returns a list of keys that fullfill the include/exclude
@@ -700,15 +700,120 @@ class Archive(NomadSettings):
         ''')
 
 
-class UnitSystemEnum(str, Enum):
-    CUSTOM = 'Custom'
-    SI = 'SI'
-    AU = 'AU'
+class UnitSystemUnit(StrictSettings):
+    definition: str = Field(description='''
+        The unit definition. Can be a mathematical expression that combines
+        several units, e.g. `(kg * m) / s^2`. You should only use units that are
+        registered in the NOMAD unit registry (`nomad.units.ureg`).
+    ''')
+    locked: Optional[bool] = Field(False, description='Whether the unit is locked in the unit system it is defined in.')
+
+
+dimensions = [
+    # Base units
+    'dimensionless',
+    'length',
+    'mass',
+    'time',
+    'current',
+    'temperature',
+    'luminosity',
+    'luminous_flux',
+    'substance',
+    'angle',
+    'information',
+    # Derived units with specific name
+    'force',
+    'energy',
+    'power',
+    'pressure',
+    'charge',
+    'resistance',
+    'conductance',
+    'inductance',
+    'magnetic_flux',
+    'magnetic_field',
+    'frequency',
+    'luminance',
+    'illuminance',
+    'electric_potential',
+    'capacitance',
+    'activity'
+]
+dimension_list = '\n'.join([' - ' + str(dim) for dim in dimensions])
+
+
+class UnitSystem(StrictSettings):
+    label: str = Field(description='Short, descriptive label used for this unit system.')
+    units: Optional[Dict[str, UnitSystemUnit]] = Field(description=f'''
+        Contains a mapping from each dimension to a unit. If a unit is not
+        specified for a dimension, the SI equivalent will be used by default.
+        The following dimensions are available:
+        {dimension_list}
+    ''')
+
+    @root_validator(pre=True)
+    def __dimensions_and_si_defaults(cls, values):  # pylint: disable=no-self-argument
+        '''Adds SI defaults for dimensions that are missing a unit.'''
+        units = values.get('units', {})
+        from nomad.units import ureg
+        from pint import UndefinedUnitError
+
+        # Check that only supported dimensions and units are used
+        for key in units.keys():
+            if key not in dimensions:
+                raise AssertionError(f'Unsupported dimension "{key}" used in a unit system. The supported dimensions are: {dimensions}.')
+
+        # Fill missing units with SI defaults
+        SI = {
+            'dimensionless': 'dimensionless',
+            'length': 'm',
+            'mass': 'kg',
+            'time': 's',
+            'current': 'A',
+            'temperature': 'K',
+            'luminosity': 'cd',
+            'luminous_flux': 'lm',
+            'substance': 'mol',
+            'angle': 'rad',
+            'information': 'bit',
+            'force': 'N',
+            'energy': 'J',
+            'power': 'W',
+            'pressure': 'Pa',
+            'charge': 'C',
+            'resistance': 'Ω',
+            'conductance': 'S',
+            'inductance': 'H',
+            'magnetic_flux': 'Wb',
+            'magnetic_field': 'T',
+            'frequency': 'Hz',
+            'luminance': 'nit',
+            'illuminance': 'lx',
+            'electric_potential': 'V',
+            'capacitance': 'F',
+            'activity': 'kat'
+        }
+        for dimension in dimensions:
+            if dimension not in units:
+                units[dimension] = {'definition': SI[dimension]}
+
+        # Check that units are available in registry, and thus also in the GUI.
+        for value in units.values():
+            definition = value['definition']
+            try:
+                ureg.Unit(definition)
+            except UndefinedUnitError as e:
+                raise AssertionError(f'Unsupported unit "{definition}" used in a unit registry.')
+
+        values['units'] = units
+
+        return values
 
 
-class UnitSystems(StrictSettings):
-    '''Controls the used unit system.'''
-    selected: UnitSystemEnum = Field(description='Controls the default unit system.')
+class UnitSystems(OptionsSingle):
+    '''Controls the available unit systems.'''
+    options: Optional[Dict[str, UnitSystem]] = Field(description='Contains the available unit systems.')
 
 
 class Theme(StrictSettings):
@@ -731,7 +836,7 @@ class Card(StrictSettings):
 
 class Cards(Options):
     '''Contains the overview page card definitions and controls their visibility.'''
-    options: Dict[str, Card] = Field(description='Contains the available card options.')
+    options: Optional[Dict[str, Card]] = Field(description='Contains the available card options.')
 
 
 class Entry(StrictSettings):
@@ -779,7 +884,7 @@ class Columns(OptionsMulti):
     Contains column definitions, controls their availability and specifies the default
     selection.
     '''
-    options: Dict[str, Column] = Field(description='''
+    options: Optional[Dict[str, Column]] = Field(description='''
         All available column options. Note here that the key must correspond to a
         quantity path that exists in the metadata.
     ''')
@@ -830,7 +935,7 @@ class FilterMenuActionCheckbox(FilterMenuAction):
 
 class FilterMenuActions(Options):
     '''Contains filter menu action definitions and controls their availability.'''
-    options: Dict[str, FilterMenuActionCheckbox] = Field(
+    options: Optional[Dict[str, FilterMenuActionCheckbox]] = Field(
         description='Contains options for filter menu actions.'
     )
 
@@ -852,7 +957,7 @@ class FilterMenu(StrictSettings):
 
 class FilterMenus(Options):
     '''Contains filter menu definitions and controls their availability.'''
-    options: Dict[str, FilterMenu] = Field(description='Contains the available filter menu options.')
+    options: Optional[Dict[str, FilterMenu]] = Field(description='Contains the available filter menu options.')
 
 
 class Schemas(OptionsBase):
@@ -1003,7 +1108,7 @@ class App(StrictSettings):
 
 class Apps(Options):
     '''Contains App definitions and controls their availability.'''
-    options: Dict[str, App] = Field(description='Contains the available app options.')
+    options: Optional[Dict[str, App]] = Field(description='Contains the available app options.')
 
 
 class ExampleUploads(OptionsBase):
@@ -1021,7 +1126,85 @@ class UI(StrictSettings):
         description='Controls the site theme and identity.'
     )
     unit_systems: UnitSystems = Field(
-        UnitSystems(**{'selected': 'Custom'}),
+        UnitSystems(**{
+            'selected': 'Custom',
+            'options': {
+                'Custom': {
+                    'label': 'Custom',
+                    'units': {
+                        'length': {'definition': 'Å'},
+                        'time': {'definition': 'fs'},
+                        'energy': {'definition': 'eV'},
+                        'pressure': {'definition': 'GPa'},
+                        'angle': {'definition': '°'},
+                    }
+                },
+                'SI': {
+                    'label': 'International System of Units (SI)',
+                    'units': {
+                        'dimensionless': {'definition': 'dimensionless', 'locked': True},
+                        'length': {'definition': 'm', 'locked': True},
+                        'mass': {'definition': 'kg', 'locked': True},
+                        'time': {'definition': 's', 'locked': True},
+                        'current': {'definition': 'A', 'locked': True},
+                        'temperature': {'definition': 'K', 'locked': True},
+                        'luminosity': {'definition': 'cd', 'locked': True},
+                        'luminous_flux': {'definition': 'lm', 'locked': True},
+                        'substance': {'definition': 'mol', 'locked': True},
+                        'angle': {'definition': 'rad', 'locked': True},
+                        'information': {'definition': 'bit', 'locked': True},
+                        'force': {'definition': 'N', 'locked': True},
+                        'energy': {'definition': 'J', 'locked': True},
+                        'power': {'definition': 'W', 'locked': True},
+                        'pressure': {'definition': 'Pa', 'locked': True},
+                        'charge': {'definition': 'C', 'locked': True},
+                        'resistance': {'definition': 'Ω', 'locked': True},
+                        'conductance': {'definition': 'S', 'locked': True},
+                        'inductance': {'definition': 'H', 'locked': True},
+                        'magnetic_flux': {'definition': 'Wb', 'locked': True},
+                        'magnetic_field': {'definition': 'T', 'locked': True},
+                        'frequency': {'definition': 'Hz', 'locked': True},
+                        'luminance': {'definition': 'nit', 'locked': True},
+                        'illuminance': {'definition': 'lx', 'locked': True},
+                        'electric_potential': {'definition': 'V', 'locked': True},
+                        'capacitance': {'definition': 'F', 'locked': True},
+                        'activity': {'definition': 'kat', 'locked': True}
+                    }
+                },
+                'AU': {
+                    'label': 'Hartree atomic units (AU)',
+                    'units': {
+                        'dimensionless': {'definition': 'dimensionless', 'locked': True},
+                        'length': {'definition': 'bohr', 'locked': True},
+                        'mass': {'definition': 'm_e', 'locked': True},
+                        'time': {'definition': 'atomic_unit_of_time', 'locked': True},
+                        'current': {'definition': 'atomic_unit_of_current', 'locked': True},
+                        'temperature': {'definition': 'atomic_unit_of_temperature', 'locked': True},
+                        'luminosity': {'definition': 'cd', 'locked': False},
+                        'luminous_flux': {'definition': 'lm', 'locked': False},
+                        'substance': {'definition': 'mol', 'locked': False},
+                        'angle': {'definition': 'rad', 'locked': False},
+                        'information': {'definition': 'bit', 'locked': False},
+                        'force': {'definition': 'atomic_unit_of_force', 'locked': True},
+                        'energy': {'definition': 'Ha', 'locked': True},
+                        'power': {'definition': 'W', 'locked': False},
+                        'pressure': {'definition': 'atomic_unit_of_pressure', 'locked': True},
+                        'charge': {'definition': 'C', 'locked': False},
+                        'resistance': {'definition': 'Ω', 'locked': False},
+                        'conductance': {'definition': 'S', 'locked': False},
+                        'inductance': {'definition': 'H', 'locked': False},
+                        'magnetic_flux': {'definition': 'Wb', 'locked': False},
+                        'magnetic_field': {'definition': 'T', 'locked': False},
+                        'frequency': {'definition': 'Hz', 'locked': False},
+                        'luminance': {'definition': 'nit', 'locked': False},
+                        'illuminance': {'definition': 'lx', 'locked': False},
+                        'electric_potential': {'definition': 'V', 'locked': False},
+                        'capacitance': {'definition': 'F', 'locked': False},
+                        'activity': {'definition': 'kat', 'locked': False}
+                    }
+                },
+            }
+        }),
         description='Controls the available unit systems.'
     )
     entry: Entry = Field(
diff --git a/nomad/parsing/tabular.py b/nomad/parsing/tabular.py
index 42c02e8b240b2e7928ba755dfe2e33b4b4385e0a..127c5ae77822f3f0dbefc17d40bd3f91a662be9c 100644
--- a/nomad/parsing/tabular.py
+++ b/nomad/parsing/tabular.py
@@ -247,9 +247,11 @@ class TableData(ArchiveSection):
                         except AttributeError:
                             continue
                 section_to_write = section_to_entry
-            if not any(item.label == 'EntryData' for item in section_to_entry.m_def.all_base_sections):
+            if not any(
+                    (item.label == 'EntryData' or item.label == 'ArchiveSection')
+                    for item in section_to_entry.m_def.all_base_sections):
                 logger.warning(
-                    f"make sure to inherit from EntryData in your base sections in {section_to_entry.m_def.name}")
+                    f"make sure to inherit from EntryData in the base sections of {section_to_entry.m_def.name}")
             if not is_quantity_def:
                 pass
                 # raise TabularParserError(
@@ -277,8 +279,9 @@ class TableData(ArchiveSection):
                     setattr(self, single_entry_section.split('/')[0], None)
                 self.m_add_sub_section(
                     self.m_def.all_properties[single_entry_section.split('/')[0]], target_section, -1)
-        from nomad.datamodel import EntryArchive, EntryMetadata
 
+        from nomad.datamodel import EntryArchive, EntryMetadata
+        section_to_entry.fill_archive_from_datafile = False
         child_archive = EntryArchive(
             data=section_to_entry,
             m_context=archive.m_context,
@@ -313,7 +316,7 @@ class TableData(ArchiveSection):
             logger.warning(f"make sure to inherit from EntryData in your base sections in {section.name}")
 
         try:
-            mainfile_name = getattr(getattr(section.m_root(), 'metadata'), 'mainfile')
+            mainfile_name = getattr(child_sections[0], section.m_def.more.get('label_quantity', None))
         except (AttributeError, TypeError):
             logger.info('could not extract the mainfile from metadata. Setting a default name.')
             mainfile_name = section.m_def.name
@@ -346,12 +349,14 @@ class TableData(ArchiveSection):
                 current_child_entry_name = [get_nested_value(first_child, segments), '.yaml']
             except Exception:
                 current_child_entry_name = archive.metadata.mainfile.split('.archive')
+            first_child.m_context = archive.m_context
             self.m_update_from_dict(first_child.m_to_dict())
 
         for index, child_section in enumerate(child_sections):
             if ref_entry_name:
                 ref_entry_name: str = child_section.m_def.more.get('label_quantity', None)
-                segments = ref_entry_name.split('#/data/')[1].split('/')
+                segments = ref_entry_name.split('#/data/')[1].split('/') if ref_entry_name.find(
+                    '/') else ref_entry_name
                 filename = f"{get_nested_value(child_section, segments)}.entry_data.archive.{file_type}"
                 current_child_entry_name = [get_nested_value(child_section, segments), '.yaml']
             else:
@@ -367,6 +372,7 @@ class TableData(ArchiveSection):
                     annotation = data_quantity_def.m_get_annotations('tabular_parser')
                     if annotation:
                         child_section.m_update_from_dict({annotation.m_definition.name: data_file})
+                child_section.fill_archive_from_datafile = False
                 child_archive = EntryArchive(
                     data=child_section,
                     m_context=archive.m_context,
@@ -391,7 +397,7 @@ m_package.__init_metainfo__()
 
 def set_entry_name(quantity_def, child_section, index) -> str:
     if name := child_section.m_def.more.get('label_quantity', None):
-        entry_name = f"{child_section[name]}_{index}"
+        entry_name = f"{child_section[name.split('#/data/')[1]]}_{index}"
     elif isinstance(quantity_def.type, Reference):
         entry_name = f"{quantity_def.type._target_section_def.name}_{index}"
     else:
@@ -674,7 +680,7 @@ def _strip_whitespaces_from_df_columns(df):
         cleaned_col_name = col_name.strip().split('.')[0]
         count = 0
         for string in transformed_column_names.values():
-            if cleaned_col_name in string:
+            if cleaned_col_name == string.split('.')[0]:
                 count += 1
         if count:
             transformed_column_names.update({col_name: f'{cleaned_col_name}.{count}'})
@@ -755,8 +761,8 @@ class TabularDataParser(MatchingParser):
         return None
 
     def is_mainfile(
-        self, filename: str, mime: str, buffer: bytes, decoded_buffer: str,
-        compression: str = None
+            self, filename: str, mime: str, buffer: bytes, decoded_buffer: str,
+            compression: str = None
     ) -> Union[bool, Iterable[str]]:
         # We use the main file regex capabilities of the superclass to check if this is a
         # .csv file
diff --git a/nomad/search.py b/nomad/search.py
index 3d6aa36270f94e5015cd7ce6607f3f6bd454ca27..56b1115adb4e123cbe544ff2edeba878de6cf43d 100644
--- a/nomad/search.py
+++ b/nomad/search.py
@@ -1412,6 +1412,7 @@ def search(
     )
 
     doc_type = index.doc_type
+    skip_sort = False
 
     # The first half of this method creates the ES query. Then the query is run on ES.
     # The second half is about transforming the ES response to a MetadataResponse.
@@ -1429,6 +1430,12 @@ def search(
     if isinstance(query, EsQuery):
         es_query = cast(EsQuery, query)
     else:
+        # TODO this is a temporary performance hot-fix. Sort is expensive and we sort
+        # by default. In the future, the client should explicitly state if sort is necessary.
+        # Now, we simply do never sort, if there is a top-level AND match for a single id in the query.
+        # In this case, there wil always be just one result and sorting is not necessary.
+        # This catches a lot of problematic queries as a hot-fix.
+        skip_sort = isinstance(query, dict) and isinstance(query.get(doc_type.id_field, None), str)
         query = normalize_api_query(cast(Query, query), doc_type=doc_type)
         es_query = create_es_query(cast(Query, query))
 
@@ -1446,7 +1453,8 @@ def search(
         pagination.order_by = doc_type.id_field
 
     sort, order_quantity, page_after_value = _api_to_es_sort(pagination, doc_type=doc_type)
-    search = search.sort(sort)
+    if not skip_sort:
+        search = search.sort(sort)
     search = search.extra(size=pagination.page_size, track_total_hits=True)
 
     if pagination.page_offset:
diff --git a/tests/app/v1/routers/test_systems.py b/tests/app/v1/routers/test_systems.py
index a0f506b1e4e2ad64dfc57fa901c186ac8880eebb..fb6e9cf832debc8a133d232926e41f50ba3d63f7 100644
--- a/tests/app/v1/routers/test_systems.py
+++ b/tests/app/v1/routers/test_systems.py
@@ -31,7 +31,7 @@ from nomad.datamodel.results import Results, Material, System
 from nomad.datamodel.metainfo.simulation.run import Run
 from nomad.datamodel.metainfo.simulation.system import System as SystemRun, Atoms
 from nomad.utils.exampledata import ExampleData
-from nomad.app.v1.routers.systems import format_map, FormatFeature
+from nomad.app.v1.routers.systems import format_map, FormatFeature, WrapModeEnum
 
 from .common import assert_response, assert_browser_download_headers
 
@@ -90,6 +90,18 @@ atoms_missing_positions = Atoms(
     species=[7, 8],
 )
 
+atoms_wrap_mode = Atoms(
+    n_atoms=2,
+    labels=["C", "H"],
+    species=[6, 1],
+    positions=np.array([[-15, -15, -15], [17, 17, 17]]) * ureg.angstrom,
+    lattice_vectors=np.array([[5, 0, 0], [0, 5, 0], [0, 0, 5]]) * ureg.angstrom,
+    periodic=[True, True, True],
+)
+
+atoms_wrap_mode_no_pbc = atoms_wrap_mode.m_copy()
+atoms_wrap_mode_no_pbc.periodic = [False, False, False]
+
 
 @pytest.fixture(scope="module")
 def example_data_systems(elastic_module, mongo_module, test_user):
@@ -104,6 +116,8 @@ def example_data_systems(elastic_module, mongo_module, test_user):
         SystemRun(atoms=atoms_with_cell),
         SystemRun(atoms=atoms_missing_positions),
         SystemRun(atoms=atoms_without_cell),
+        SystemRun(atoms=atoms_wrap_mode),
+        SystemRun(atoms=atoms_wrap_mode_no_pbc),
     ])])
     archive.results = Results(
         material=Material(
@@ -133,8 +147,8 @@ def example_data_systems(elastic_module, mongo_module, test_user):
     assert search(query=dict(upload_id=upload_id)).pagination.total == 0
 
 
-def run_query(entry_id, path, format, client):
-    response = client.get(f'systems/{entry_id}/?path={path}&format={format}', headers={})
+def run_query(entry_id, path, format, client, wrap_mode=None):
+    response = client.get(f'systems/{entry_id}/?path={path}&format={format}{f"&wrap_mode={wrap_mode}" if wrap_mode else ""}', headers={})
     return response
 
 
@@ -332,3 +346,21 @@ def test_indices(path, filename, n_atoms, client, example_data_systems):
     )
     atoms = ase.io.read(BytesIO(response.content), format='cif')
     assert len(atoms) == n_atoms
+
+
+@pytest.mark.parametrize("path, wrap_mode, expected_positions", [
+    pytest.param('/run/0/system/3', None, [[-15, -15, -15], [17, 17, 17]], id='default'),
+    pytest.param('/run/0/system/3', WrapModeEnum.original, [[-15, -15, -15], [17, 17, 17]], id='original'),
+    pytest.param('/run/0/system/3', WrapModeEnum.wrap, [[0, 0, 0], [2, 2, 2]], id='wrap, pbc=[1, 1, 1]'),
+    pytest.param('/run/0/system/4', WrapModeEnum.wrap, [[-15, -15, -15], [17, 17, 17]], id='wrap, pbc=[0, 0, 0]'),
+    pytest.param('/run/0/system/3', WrapModeEnum.unwrap, [[1.5, 1.5, 1.5], [3.5, 3.5, 3.5]], id='unwrap, pbc=[1, 1, 1]'),
+    pytest.param('/run/0/system/4', WrapModeEnum.unwrap, [[-15, -15, -15], [17, 17, 17]], id='unwrap, pbc=[0, 0, 0]'),
+])
+def test_wrap_mode(path, wrap_mode, expected_positions, client, example_data_systems):
+    '''Test that the wrap_mode parameter is handled correctly.
+    '''
+    response = run_query('systems_entry_1', path, 'xyz', client, wrap_mode)
+    assert_response(response, 200)
+    atoms = ase.io.read(StringIO(response.text), format='xyz')
+    positions = atoms.get_positions()
+    assert np.allclose(positions, expected_positions)
diff --git a/tests/archive/test_archive.py b/tests/archive/test_archive.py
index ddef2779087179faa221d383235c41ed012f9909..b1f6fc153d7ebc0e376afbf6329b3be68a5bcc46 100644
--- a/tests/archive/test_archive.py
+++ b/tests/archive/test_archive.py
@@ -291,6 +291,8 @@ def test_keys(key):
     assert key.strip() in query_archive(f, {key: '*'})
 
 
+@pytest.mark.skipif(config.normalize.springer_db_path is None,
+                    reason='Springer DB path missing')
 def test_read_springer():
     springer = read_archive(config.normalize.springer_db_path)
     with pytest.raises(KeyError):
diff --git a/tests/normalizing/test_material.py b/tests/normalizing/test_material.py
index a229738ead847782c50401a4330c13fea0c58db6..4bc716e11cca1103c005139b661493ee1a30dafd 100644
--- a/tests/normalizing/test_material.py
+++ b/tests/normalizing/test_material.py
@@ -23,7 +23,7 @@ import ase.build
 from matid.symmetry.wyckoffset import WyckoffSet  # pylint: disable=import-error
 
 from nomad.units import ureg
-from nomad import atomutils
+from nomad import atomutils, config
 from nomad.utils import hash
 from nomad.normalizing.common import ase_atoms_from_nomad_atoms
 from nomad.datamodel.results import ElementalComposition
@@ -149,7 +149,8 @@ def test_material_surface(surface):
     assert material.material_name is None
     assert material.symmetry is None
 
-
+@pytest.mark.skipif(config.normalize.springer_db_path is None,
+                    reason='Springer DB path missing')
 def test_material_bulk(bulk):
     # Material
     material = bulk.results.material
diff --git a/tests/normalizing/test_system.py b/tests/normalizing/test_system.py
index 340a31df949b150dc998d4ac5f43689df6648125..bb0ae667fa86a8422d154b3a5331455096f4355e 100644
--- a/tests/normalizing/test_system.py
+++ b/tests/normalizing/test_system.py
@@ -382,6 +382,8 @@ def test_aflow_prototypes():
     assert prototype_label == "186-SZn-hP4"
 
 
+@pytest.mark.skipif(config.normalize.springer_db_path is None,
+                    reason='Springer DB path missing')
 def test_springer_normalizer():
     '''
     Ensure the Springer normalizer works well with the VASP example.