diff --git a/web/components/tabs-header.js b/web/components/tabs-header.js
new file mode 100644
--- /dev/null
+++ b/web/components/tabs-header.js
@@ -0,0 +1,28 @@
+// @flow
+
+import classnames from 'classnames';
+import * as React from 'react';
+
+import css from './tabs.css';
+
+type Props<T: string> = {
+  +children?: React.Node,
+  +isActive: boolean,
+  +setTab: T => mixed,
+  +id: T,
+};
+
+function TabsHeader<T: string>(props: Props<T>): React.Node {
+  const { children, isActive, setTab, id } = props;
+  const headerClasses = classnames(css.tabHeader, {
+    [css.backgroundTabHeader]: !isActive,
+  });
+  const onClickSetTab = React.useCallback(() => setTab(id), [setTab, id]);
+  return (
+    <div className={headerClasses} onClick={onClickSetTab}>
+      {children}
+    </div>
+  );
+}
+
+export default TabsHeader;
diff --git a/web/components/tabs.css b/web/components/tabs.css
new file mode 100644
--- /dev/null
+++ b/web/components/tabs.css
@@ -0,0 +1,29 @@
+div.tabsContainer {
+  color: var(--fg);
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+div.tabsHeaderContainer {
+  display: flex;
+}
+
+div.tabHeader {
+  flex: 1;
+  padding: 16px;
+  display: flex;
+  justify-content: center;
+  color: var(--tabs-header-active-color);
+  border-bottom: 2px solid var(--tabs-header-active-border);
+}
+
+div.backgroundTabHeader {
+  cursor: pointer;
+  color: var(--tabs-header-background-color);
+  border-bottom-color: var(--tabs-header-background-border);
+}
+
+div.backgroundTabHeader:hover {
+  color: var(--tabs-header-background-color-hover);
+  border-bottom-color: var(--tabs-header-background-border-hover);
+}
diff --git a/web/components/tabs.react.js b/web/components/tabs.react.js
new file mode 100644
--- /dev/null
+++ b/web/components/tabs.react.js
@@ -0,0 +1,58 @@
+// @flow
+
+import * as React from 'react';
+
+import TabsHeader from './tabs-header';
+import css from './tabs.css';
+
+type TabsContainerProps<T: string> = {
+  +children?: React.ChildrenArray<?React.Element<typeof TabsItem>>,
+  +activeTab: T,
+  +setTab: T => mixed,
+};
+
+function TabsContainer<T: string>(props: TabsContainerProps<T>): React.Node {
+  const { children, activeTab, setTab } = props;
+
+  const headers = React.Children.map(children, tab => {
+    const { id, header } = tab.props;
+
+    const isActive = id === activeTab;
+    return (
+      <TabsHeader id={id} isActive={isActive} setTab={setTab}>
+        {header}
+      </TabsHeader>
+    );
+  });
+
+  const currentTab = React.Children.toArray(children).find(
+    tab => tab.props.id === activeTab,
+  );
+
+  const currentContent = currentTab ? currentTab.props.children : null;
+
+  return (
+    <div className={css.tabsContainer}>
+      <div className={css.tabsHeaderContainer}>{headers}</div>
+      {currentContent}
+    </div>
+  );
+}
+
+type TabsItemProps<T: string> = {
+  +children: React.Node,
+  +id: T,
+  +header: React.Node,
+};
+
+function TabsItem<T: string>(props: TabsItemProps<T>): React.Node {
+  const { children } = props;
+  return children;
+}
+
+const Tabs = {
+  Container: TabsContainer,
+  Item: TabsItem,
+};
+
+export default Tabs;
diff --git a/web/theme.css b/web/theme.css
--- a/web/theme.css
+++ b/web/theme.css
@@ -124,4 +124,10 @@
   --search-input-color: var(--shades-white-100);
   --search-input-placeholder: var(--shades-black-60);
   --search-icon-color: var(--shades-black-60);
+  --tabs-header-active-color: var(--shades-white-100);
+  --tabs-header-active-border: var(--violet-light-100);
+  --tabs-header-background-color: var(--shades-black-60);
+  --tabs-header-background-border: var(--shades-black-80);
+  --tabs-header-background-color-hover: var(--shades-white-80);
+  --tabs-header-background-border-hover: var(--shades-black-70);
 }