Repository: uber-web/baseui
Pull request #132 (2018-08-17 08:54:22)

feat(menu): add profile list to Menu component; tweak styles
Added Profile List to Menu component. Some outstanding items to be done below, but otherwise good for review.

* [x] Initial Component Implementation
* [x] Highlight Style

abmai: Updated with the implementation changes discussed before
schnerd: >Updated with the implementation changes discussed before. Seems like going all-in on overrides is not bad at all.

Woohoo! Glad to hear it worked, I think this simplifies things.

Can you update the RFC too to remove MenuProfile, etc?

Also it seems like the ProfileOption is missing cursor: pointer and hover styles. Thoughts on adding this?
abmai: Still waiting on hover styles from @nguyensomniac - we might have to circle back to that so that this PR doesn't just sit here.

Maybe I can also go with a first pass + add cursor style so that we have something tho.
nguyensomniac: Just talked about this with @schnerd, will update everything by eod
abmai: @nguyensomniac can you help sign off on the current styles when you get a chance? It's missing the condensed style, but I'm planning to add that in a separate PR
nguyensomniac: ooo, will take a look friday
abmai: Oh oops, gotcha. I'll merge then and you can do a whole review once I have the condensed style too

Patch:
diff --git a/rfcs/menu-component.md b/rfcs/menu-component.md
index 5ea571105..dcf3ba887 100644
--- a/rfcs/menu-component.md
+++ b/rfcs/menu-component.md
@@ -4,126 +4,83 @@
 
 > Given an array of items, I want to render them as a list, navigate through it using keyboard bindings, and receive feedback when I select (via pointer select or Enter) an item.
 
-## Exported Components
-
-* `Menu`
-* `StatefulContainer`
-* `StyledMenu`
-* `StyledList`
-* `StyledListItem`
-
-## Default Internal Structure
-
-The default exported `Menu` will have the following internal structure
-
-```js
-<StatefulContainer>
-  <StyledList>
-    <StyledListItem />
-  </StyledList>
-</StatefulContainer>
-```
-
-## StatefulContainer API
-
-#### `items (required)`
-
-```js
-Array<Object>
-```
-
-List of items
-
-#### `getItemLabel (required)`
-
-```js
-(item: Object) => string;
-```
-
-Function used to get the string label for each item.
-
-#### `initialState (optional)`
-
-```js
-{
-  highlightedIndex: number;
-}
-```
-
-Used to set initial state for the component. All the component's state can be controlled via props as well.
-
-#### `stateReducer (optional)`
-
-```js
-(changeType: string, changes: Object, currentState: Object) => Object;
-```
-
-State reducer to intercept state changes and return new internal state
-
-#### `onItemSelect (optional)`
-
-```js
-(selectedItem: ?Object) => any;
-```
-
-## Menu API
-
-All of `StatefulContainer` API and the following
-
-#### `overrides (optional)`
-
-```js
-{
-  List: Object | React.ComponentType<*>,
-  ListItem: Object | React.ComponentType<*>,
-}
-```
-
-Component injection prop, can be used to override any or all of the internal components
-
-## Render Props API
+### Exported Components
+
+- `Menu`
+- `StatefulContainer`
+- `StatefulMenu`
+- `OptionList`
+- `OptionProfile`
+- `StyledList`
+- `StyledListItem`
+- `StyledListItemProfile`
+- `StyledProfileImgContainer`
+- `StyledProfileImg`
+- `StyledProfileLabelsContainer`
+- `StyledProfileTitle`
+- `StyledProfileSubtitle`
+- `StyledProfileBody`
+
+### StatefulContainer API
+
+- `items: Array<any>`
+  List of items
+- `initialState: {highlightedIndex: number}`
+  Used to set initial state for the component. All the component's state can be controlled via props as well.
+- `stateReducer: (changeType: string, changes: Object, currentState: Object) => Object`
+  State reducer to intercept state changes and return new internal state
+- `onItemSelect: (selectedItem: ?Object) => any`
+
+### Render Props API
 
 These are props passed down to the render prop, or children-as-a-function function, for advanced usages (and also used internally for the preconstructed `Menu`)
 
-#### `items`
-
-```js
-Array<Object>
-```
-
-List of items
-
-#### `getItemLabel`
-
-```js
-(item: Object) => string;
-```
-
-Function used to get the string label for each item.
-
-#### `highlightedIndex`
-
-```js
-number;
-```
-
-Index of highlighted item if applicable
-
-#### `rootRef`
-
-```js
-React$ElementRef<*>;
-```
-
-Ref for the root element
-
-#### `getRequiredItemProps`
-
-```js
-(item: Object) => ({key: string, id: string});
-```
-
-Function to get props for each rendered item. This will have some defaults needed for keyboard bindings to work properly. Every rendered item should call this. This is a function to ensure that it is used.
+- `items`
+  Same as `StatefulContainer`
+- `highlightedIndex: number`
+  Index of highlighted item if applicable
+- `rootRef: React$ElementRef<*>`
+  Ref for the root element
+- `getRequiredItemProps: (item: Object) => ({key: string, id: string})`
+  Function to get props for each rendered item. This will have some defaults needed for keyboard bindings to work properly. Every rendered item should call this. This is a function to ensure that it is used.
+
+### Menu API
+
+- `items: Array<any>`
+  List of items
+- `getRequiredItemProps: (item: Object) => ({key: string, id: string})`
+  Function to get props for each rendered item. This will have some defaults needed for keyboard bindings to work properly. Every rendered item should call this. This is a function to ensure that it is used.
+- `highlightedIndex: number`
+  Index of highlighted item if applicable
+- `rootRef: React$ElementRef<*>`
+  Ref for the root element
+- `overrides: {List, Option}`
+  Component injection prop, can be used to override any or all of the internal components
+  - `List` and `Option` have type `Object | React.ComponentType<*>`
+
+### Option List API
+
+- `item: any`
+  Item
+- `getItemLabel: (item: any) => string`
+  Function used to get the string label for each item
+- `overrides: {ListItem}`
+  Component injection
+  - All of these have type `Object | React.ComponentType<*>`
+
+### Option Profile API
+
+- `item: any`
+  Item
+- `getProfileItemLabels: (item: Object) => ({title?: string, subtitle?: string, body?: string})`
+  Returns an object of three strings for the profile item
+- `getProfileItemImg: (item: Object) => string | React.ComponenType<*>`
+  Returns either an image source url, or a full React component to render as the image
+- `getProfileItemImgText: (item: Object) => string`
+  Returns the alt text for the image
+- `overrides: {ListItemProfile, ProfileImgContainer, ProfileImg, ProfileLabelsContainer, ProfileTitle, ProfileSubtitle, ProfileBody}`
+  Component injection
+  - All of these have type `Object | React.ComponentType<*>`
 
 ## Keybindings
 
diff --git a/src/helpers/overrides.js b/src/helpers/overrides.js
index 082282665..d026c873e 100644
--- a/src/helpers/overrides.js
+++ b/src/helpers/overrides.js
@@ -58,6 +58,21 @@ export function toObjectOverride<T>(override: OverrideT<T>): OverrideT<T> {
   return override;
 }
 
+/**
+ * Get a convenient override object that will always have {component, props}
+ */
+export function getOverrideObject<T>(
+  override: ?OverrideT<T>,
+  defaultComponent: React.ComponentType<T>,
+): {
+  component: React.ComponentType<T>,
+  props: {},
+} {
+  const component = getOverride(override) || defaultComponent;
+  const props = getOverrideProps(override);
+  return {component, props};
+}
+
 /**
  * Merges two override objects – this is useful if you want to
  * inject your own overrides into a child component, but also
diff --git a/src/menu/index.js b/src/menu/index.js
index 9f47cc31e..bc4e02682 100644
--- a/src/menu/index.js
+++ b/src/menu/index.js
@@ -7,6 +7,8 @@ LICENSE file in the root directory of this source tree.
 // @flow
 export {default as StatefulMenu} from './stateful-menu';
 export {default as StatefulContainer} from './stateful-container';
+export {default as OptionList} from './option-list';
+export {default as OptionProfile} from './option-profile';
 export {default as Menu} from './menu';
 // Constants
 export {KEY_STRINGS, STATE_CHANGE_TYPES} from './constants';
@@ -14,4 +16,13 @@ export {KEY_STRINGS, STATE_CHANGE_TYPES} from './constants';
 export {
   List as StyledList,
   ListItem as StyledListItem,
+  ListItemProfile as StyledListItemProfile,
+  ProfileImgContainer as StyledProfileImgContainer,
+  ProfileImg as StyledProfileImg,
+  ProfileLabelsContainer as StyledProfileLabelsContainer,
+  ProfileTitle as StyledProfileTitle,
+  ProfileSubtitle as StyledProfileSubtitle,
+  ProfileBody as StyledProfileBody,
 } from './styled-components';
+// Flow
+export * from './types';
diff --git a/src/menu/menu.js b/src/menu/menu.js
index 4e95f0b00..5c1956250 100644
--- a/src/menu/menu.js
+++ b/src/menu/menu.js
@@ -7,39 +7,39 @@ LICENSE file in the root directory of this source tree.
 // @flow
 import * as React from 'react';
 // Components
-import {
-  List as StyledList,
-  ListItem as StyledListItem,
-} from './styled-components';
-import {STYLETRON_PROP_MAPPER} from './constants';
+import {List as StyledList} from './styled-components';
+import OptionList from './option-list';
 import {mapStyletronProps} from './utils';
-import {getOverride} from '../helpers/overrides';
+import {getOverrideObject} from '../helpers/overrides';
 // Types
 import type {StatelessMenuPropsT} from './types';
 
 export default function Menu({
   items,
-  getItemLabel,
   getRequiredItemProps = (item, index) => ({key: String(index)}),
   rootRef = React.createRef(),
   overrides = {},
 }: StatelessMenuPropsT) {
-  const {List: ListOverride, ListItem: ListItemOverride} = overrides;
-  const List = getOverride(ListOverride) || StyledList;
-  const ListItem = getOverride(ListItemOverride) || StyledListItem;
+  const {component: List, props: listProps} = getOverrideObject(
+    overrides.List,
+    StyledList,
+  );
+  const {component: Option, props: optionProps} = getOverrideObject(
+    overrides.Option,
+    OptionList,
+  );
   return (
-    <List $ref={rootRef}>
+    <List $ref={rootRef} {...listProps}>
       {items.map((item, index) => {
-        // $FlowFixMe
-        const {key, ...itemProps} = mapStyletronProps(
-          getRequiredItemProps(item, index),
-          STYLETRON_PROP_MAPPER,
-        );
+        const requiredProps = getRequiredItemProps(item, index);
         // Need to be explicit with `key` otherwise eslint throws error?
         return (
-          <ListItem key={key} {...itemProps}>
-            {getItemLabel(item)}
-          </ListItem>
+          // eslint-disable-next-line react/jsx-key
+          <Option
+            item={item}
+            {...mapStyletronProps(requiredProps)}
+            {...optionProps}
+          />
         );
       })}
     </List>
diff --git a/src/menu/option-list.js b/src/menu/option-list.js
new file mode 100644
index 000000000..b946c467e
--- /dev/null
+++ b/src/menu/option-list.js
@@ -0,0 +1,24 @@
+// @flow
+import * as React from 'react';
+// Components
+import {ListItem as StyledListItem} from './styled-components';
+import {getOverrideObject} from '../helpers/overrides';
+// Types
+import type {OptionListPropsT} from './types';
+
+export default function OptionList({
+  item,
+  getItemLabel = (item: *) => (item ? item.label : ''),
+  overrides = {},
+  ...restProps
+}: OptionListPropsT) {
+  const {component: ListItem, props: listItemProps} = getOverrideObject(
+    overrides.ListItem,
+    StyledListItem,
+  );
+  return (
+    <ListItem {...restProps} {...listItemProps}>
+      {getItemLabel(item)}
+    </ListItem>
+  );
+}
diff --git a/src/menu/option-profile.js b/src/menu/option-profile.js
new file mode 100644
index 000000000..43a06be04
--- /dev/null
+++ b/src/menu/option-profile.js
@@ -0,0 +1,91 @@
+// @flow
+import * as React from 'react';
+// Components
+import {
+  ListItemProfile as StyledListItemProfile,
+  ProfileImgContainer as StyledProfileImgContainer,
+  ProfileImg as StyledProfileImg,
+  ProfileLabelsContainer as StyledProfileLabelsContainer,
+  ProfileTitle as StyledProfileTitle,
+  ProfileSubtitle as StyledProfileSubtitle,
+  ProfileBody as StyledProfileBody,
+} from './styled-components';
+import {getOverrideObject} from '../helpers/overrides';
+// Types
+import type {OptionProfilePropsT} from './types';
+
+export default function OptionProfile({
+  item,
+  getProfileItemLabels,
+  getProfileItemImg,
+  getProfileItemImgText,
+  overrides = {},
+  ...restProps
+}: OptionProfilePropsT) {
+  const {
+    component: ListItemProfile,
+    props: listItemProfileProps,
+  } = getOverrideObject(overrides.ListItemProfile, StyledListItemProfile);
+  const {
+    component: ProfileImgContainer,
+    props: profileImgContainerProps,
+  } = getOverrideObject(
+    overrides.ProfileImgContainer,
+    StyledProfileImgContainer,
+  );
+  const {component: ProfileImg, props: profileImgProps} = getOverrideObject(
+    overrides.ProfileImg,
+    StyledProfileImg,
+  );
+  const {
+    component: ProfileLabelsContainer,
+    props: profileLabelsContainerProps,
+  } = getOverrideObject(
+    overrides.ProfileLabelsContainer,
+    StyledProfileLabelsContainer,
+  );
+  const {component: ProfileTitle, props: profileTitleProps} = getOverrideObject(
+    overrides.ProfileTitle,
+    StyledProfileTitle,
+  );
+  const {
+    component: ProfileSubtitle,
+    props: profileSubtitleProps,
+  } = getOverrideObject(overrides.ProfileSubtitle, StyledProfileSubtitle);
+  const {component: ProfileBody, props: profileBodyProps} = getOverrideObject(
+    overrides.ProfileBody,
+    StyledProfileBody,
+  );
+
+  const ItemImg = getProfileItemImg(item);
+  const {title, subtitle, body} = getProfileItemLabels(item);
+
+  return (
+    <ListItemProfile {...restProps} {...listItemProfileProps}>
+      <ProfileImgContainer {...profileImgContainerProps}>
+        {ItemImg &&
+          (typeof ItemImg === 'string' ? (
+            // Render img src string wrapped with image component
+            <ProfileImg
+              src={ItemImg}
+              alt={getProfileItemImgText(item)}
+              {...profileImgProps}
+            />
+          ) : (
+            // Or just render the entire component user specified
+            // TODO: need better way of checking if valid React component
+            <ItemImg {...profileImgProps} />
+          ))}
+      </ProfileImgContainer>
+      <ProfileLabelsContainer {...profileLabelsContainerProps}>
+        {title && <ProfileTitle {...profileTitleProps}>{title}</ProfileTitle>}
+        {subtitle && (
+          <ProfileSubtitle {...profileSubtitleProps}>
+            {subtitle}
+          </ProfileSubtitle>
+        )}
+        {body && <ProfileBody {...profileBodyProps}>{body}</ProfileBody>}
+      </ProfileLabelsContainer>
+    </ListItemProfile>
+  );
+}
diff --git a/src/menu/stateful-container.js b/src/menu/stateful-container.js
index e1dd44f37..c88d9f16c 100644
--- a/src/menu/stateful-container.js
+++ b/src/menu/stateful-container.js
@@ -116,6 +116,10 @@ export default class MenuStatefulContainer extends React.Component<
   getRequiredItemProps: GetRequiredItemPropsFnT = (item, index) => {
     const {highlightedIndex} = this.state;
     const {onItemSelect} = this.props;
+    let onClickHandler;
+    if (onItemSelect) {
+      onClickHandler = onItemSelect.bind(null, item);
+    }
     // Create and store ref or re-use
     let itemRef = this.refList[index];
     if (!itemRef) {
@@ -126,8 +130,7 @@ export default class MenuStatefulContainer extends React.Component<
       key: `${index}`,
       ref: itemRef,
       isHighlighted: highlightedIndex === index,
-      // $FlowFixMe
-      onClick: onItemSelect.bind(null, item),
+      onClick: onClickHandler,
       role: 'option',
       'aria-activedescendant': highlightedIndex === index,
     };
@@ -135,13 +138,12 @@ export default class MenuStatefulContainer extends React.Component<
 
   render() {
     const {highlightedIndex} = this.state;
-    const {children, items, getItemLabel} = this.props;
+    const {children, items} = this.props;
     // $FlowFixMe
     return children(
       ({
         highlightedIndex,
         items,
-        getItemLabel,
         rootRef: this.rootRef,
         getRequiredItemProps: this.getRequiredItemProps,
       }: RenderPropsT),
diff --git a/src/menu/stateful-menu.js b/src/menu/stateful-menu.js
index ecce3a453..7d8e1baf6 100644
--- a/src/menu/stateful-menu.js
+++ b/src/menu/stateful-menu.js
@@ -13,7 +13,9 @@ import StatefulContainer from './stateful-container';
 
 import type {StatefulMenuPropsT} from './types';
 
-export default class StatefulMenu extends React.Component<StatefulMenuPropsT> {
+export default class StatefulMenu extends React.PureComponent<
+  StatefulMenuPropsT,
+> {
   static defaultProps = {
     overrides: {},
   };
diff --git a/src/menu/stories.js b/src/menu/stories.js
index 7455081c5..2778f3e64 100644
--- a/src/menu/stories.js
+++ b/src/menu/stories.js
@@ -9,16 +9,33 @@ LICENSE file in the root directory of this source tree.
 import React from 'react';
 import {storiesOf} from '@storybook/react';
 import {action} from '@storybook/addon-actions';
-import {withStyle} from 'styletron-react';
 
 import Menu from './menu';
 import StatefulMenu from './stateful-menu';
-import {List} from './styled-components';
 
-// Give a width to render this nicer
-const ListMaxWidth = withStyle(List, {width: '200px'});
+import OptionProfile from './option-profile';
 
-const ITEMS: Array<{label: string}> = [
+function CloudComponent() {
+  return (
+    <svg
+      width={24}
+      height={24}
+      viewBox="0 0 24 24"
+      fill="none"
+      style={{width: 40, height: 40}}
+    >
+      <rect width={24} height={24} fill="white" />
+      <path
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="M14.5 5C12.8755 5 11.4519 5.86084 10.6609 7.15112C10.2905 7.05249 9.90137 7 9.5 7C7.14185 7 5.20752 8.81372 5.01562 11.1221C3.28247 11.5605 2 13.1304 2 15C2 17.2092 3.79077 19 6 19H11V14.4143L9.70703 15.707C9.31665 16.0977 8.68335 16.0977 8.29297 15.707C7.90234 15.3167 7.90234 14.6833 8.29297 14.293L11.293 11.293C11.6833 10.9023 12.3167 10.9023 12.707 11.293L15.707 14.293C16.0977 14.6833 16.0977 15.3167 15.707 15.707C15.3167 16.0977 14.6833 16.0977 14.293 15.707L13 14.4143V19H17C19.7615 19 22 16.7615 22 14C22 11.9492 20.7656 10.187 18.9993 9.41577C18.9543 6.96924 16.9573 5 14.5 5Z"
+        fill="black"
+      />
+    </svg>
+  );
+}
+
+const ITEMS = [
   {label: 'Item One'},
   {label: 'Item Two'},
   {label: 'Item Three'},
@@ -33,26 +50,108 @@ const ITEMS: Array<{label: string}> = [
   {label: 'Item Twelve'},
 ];
 
+const profileItem = {
+  title: 'David Smith',
+  subtitle: 'Senior Engineering Manager',
+  body: 'Uber Everything',
+  imgUrl: 'https://via.placeholder.com/60x60',
+};
+const PROFILE_ITEMS = [
+  {...profileItem},
+  {...profileItem, subtitle: null},
+  {...profileItem, body: null},
+  {...profileItem, title: null},
+  {...profileItem, imgUrl: null},
+  {...profileItem, imgUrl: CloudComponent},
+];
+
 storiesOf('Menu', module)
-  .add('Stateless', () => (
+  .add('Stateless simple', () => (
     <Menu
       items={ITEMS}
-      getItemLabel={item => item.label}
       rootRef={React.createRef()}
       overrides={{
-        // $FlowFixMe
-        List: ListMaxWidth,
+        List: {
+          style: {
+            width: '200px',
+          },
+        },
+        Option: {
+          props: {
+            getItemLabel: item => item.label,
+          },
+        },
+      }}
+    />
+  ))
+  .add('Stateless profile', () => (
+    <Menu
+      items={PROFILE_ITEMS}
+      rootRef={React.createRef()}
+      overrides={{
+        List: {
+          style: {
+            width: '350px',
+          },
+        },
+        Option: {
+          component: OptionProfile,
+          props: {
+            getProfileItemLabels: ({title, subtitle, body}) => ({
+              title,
+              subtitle,
+              body,
+            }),
+            getProfileItemImg: item => item.imgUrl,
+            getProfileItemImgText: item => item.title,
+          },
+        },
       }}
     />
   ))
-  .add('Stateful with keybindings', () => (
+  .add('Stateful simple with keybindings', () => (
     <StatefulMenu
       items={ITEMS}
       onItemSelect={action('item select')}
-      getItemLabel={item => item.label}
       overrides={{
-        // $FlowFixMe
-        List: withStyle(ListMaxWidth, {height: '150px', overflow: 'auto'}),
+        List: {
+          style: {
+            height: '150px',
+            width: '350px',
+            overflow: 'auto',
+          },
+        },
+        Option: {
+          props: {
+            getItemLabel: item => item.label,
+          },
+        },
+      }}
+    />
+  ))
+  .add('Stateful profile with keybindings', () => (
+    <StatefulMenu
+      items={PROFILE_ITEMS}
+      overrides={{
+        List: {
+          style: {
+            width: '350px',
+            height: '150px',
+            overflow: 'auto',
+          },
+        },
+        Option: {
+          component: OptionProfile,
+          props: {
+            getProfileItemLabels: ({title, subtitle, body}) => ({
+              title,
+              subtitle,
+              body,
+            }),
+            getProfileItemImg: item => item.imgUrl,
+            getProfileItemImgText: item => item.title,
+          },
+        },
       }}
     />
   ));
diff --git a/src/menu/styled-components.js b/src/menu/styled-components.js
index 3db4f8cd9..9662b56fd 100644
--- a/src/menu/styled-components.js
+++ b/src/menu/styled-components.js
@@ -19,36 +19,100 @@ type StyledListItemPropsT = {
 
 export const getListStyles = ({$theme}: StyledPropsT) => ({
   position: 'relative',
-  display: 'flex',
-  flexDirection: 'column',
   margin: 0,
+  padding: 0,
   background: $theme.colors.white,
   borderRadius: $theme.borders.radius300,
   boxShadow: $theme.lighting.shadow600,
-  paddingTop: $theme.sizing.scale200,
-  paddingBottom: $theme.sizing.scale200,
-  paddingLeft: $theme.sizing.scale400,
-  paddingRight: $theme.sizing.scale400,
 });
+export const List = styled('ul', getListStyles);
 
 export const getListItemStyles = ({
   $theme,
   $isHighlighted,
 }: StyledListItemPropsT) => ({
-  ...$theme.typography.font400,
+  ...$theme.typography.font300,
   position: 'relative',
   display: 'block',
   color: $isHighlighted ? $theme.colors.primary : $theme.colors.black,
-  margin: 0,
   cursor: 'pointer',
-  transitionProperty: 'color',
+  transitionProperty: 'color, background-color',
   transitionDuration: $theme.animation.timing100,
   transitionTimingFunction: $theme.animation.easeOutCurve,
   ':hover': {
-    color: $theme.colors.primary400,
+    backgroundColor: $theme.colors.mono300,
+  },
+  paddingTop: $theme.sizing.scale300,
+  paddingBottom: $theme.sizing.scale300,
+  paddingRight: $theme.sizing.scale600,
+  paddingLeft: $theme.sizing.scale600,
+  ':first-child': {
+    paddingTop: $theme.sizing.scale600,
+  },
+  ':last-child': {
+    paddingBottom: $theme.sizing.scale600,
   },
 });
+export const ListItem = styled('li', getListItemStyles);
 
-export const List = styled('ul', getListStyles);
+export const ListItemProfile = styled('li', ({$theme}: StyledPropsT) => ({
+  position: 'relative',
+  display: 'flex',
+  alignItems: 'center',
+  cursor: 'pointer',
+  paddingTop: $theme.sizing.scale300,
+  paddingBottom: $theme.sizing.scale300,
+  paddingRight: $theme.sizing.scale800,
+  paddingLeft: $theme.sizing.scale800,
+  transitionProperty: 'color, background-color',
+  transitionDuration: $theme.animation.timing100,
+  transitionTimingFunction: $theme.animation.easeOutCurve,
+  ':hover': {
+    backgroundColor: $theme.colors.mono300,
+  },
+  ':first-child': {
+    paddingTop: $theme.sizing.scale800,
+  },
+  ':last-child': {
+    paddingBottom: $theme.sizing.scale800,
+  },
+}));
 
-export const ListItem = styled('li', getListItemStyles);
+export const ProfileImgContainer = styled('div', {
+  width: '60px',
+  height: '60px',
+  display: 'flex',
+  justifyContent: 'center',
+  alignItems: 'center',
+});
+
+export const ProfileImg = styled('img', {
+  width: '100%',
+  height: '100%',
+  borderRadius: '50%',
+});
+
+export const ProfileLabelsContainer = styled('div', ({$theme}) => ({
+  marginLeft: $theme.sizing.scale600,
+  alignSelf: 'flex-start',
+  display: 'flex',
+  flexDirection: 'column',
+}));
+
+export const ProfileTitle = styled('h6', ({$theme}) => ({
+  ...$theme.typography.font450,
+  color: $theme.colors.black,
+  margin: 0,
+}));
+
+export const ProfileSubtitle = styled('p', ({$theme}) => ({
+  ...$theme.typography.font300,
+  color: $theme.colors.black,
+  margin: 0,
+}));
+
+export const ProfileBody = styled('p', ({$theme}) => ({
+  ...$theme.typography.font200,
+  margin: 0,
+  color: $theme.colors.mono800,
+}));
diff --git a/src/menu/types.js b/src/menu/types.js
index 093dce991..cfc8d37d6 100644
--- a/src/menu/types.js
+++ b/src/menu/types.js
@@ -14,6 +14,18 @@ export type ItemsT = Array<*>;
 
 export type GetItemLabelFnT = (item: *) => React$Node;
 
+export type GetProfileItemLabelsFnT = (
+  item: *,
+) => {
+  title?: string,
+  subtitle?: string,
+  body?: string,
+};
+
+export type GetProfileItemImgFnT = (item: *) => string | React.ComponentType<*>;
+
+export type GetProfileItemImgTextFnT = (item: *) => string;
+
 export type SetRootRefFnT = (ref: React$ElementRef<*>) => void;
 
 export type RootRefT = React$ElementRef<*>;
@@ -28,6 +40,17 @@ export type OverridesT = {
   ListItem?: OverrideT<*>,
 };
 
+export type ProfileOverridesT = {
+  List?: OverrideT<*>,
+  ListItemProfile?: OverrideT<*>,
+  ProfileImgContainer?: OverrideT<*>,
+  ProfileImg?: OverrideT<*>,
+  ProfileLabelsContainer?: OverrideT<*>,
+  ProfileTitle?: OverrideT<*>,
+  ProfileSubtitle?: OverrideT<*>,
+  ProfileBody?: OverrideT<*>,
+};
+
 export type RenderItemPropsT = {
   key: string,
   ref?: React$ElementRef<*>,
@@ -56,7 +79,6 @@ export type StatefulContainerStateT = {
 
 export type RenderPropsT = StatefulContainerStateT & {
   items: ItemsT,
-  getItemLabel: GetItemLabelFnT,
   rootRef: RootRefT,
   getRequiredItemProps: GetRequiredItemPropsFnT,
 };
@@ -77,22 +99,63 @@ export type DefaultStatefulContainerPropsT = {
 
 export type StatefulContainerPropsT = {
   items: ItemsT,
-  getItemLabel: GetItemLabelFnT,
   initialState?: StatefulContainerStateT,
   stateReducer?: StateReducerFnT,
   onItemSelect?: OnItemSelectFnT,
   children?: RenderPropsT => React.Node,
 };
 
-export type StatefulMenuPropsT = StatefulContainerPropsT & {
-  overrides?: OverridesT,
+export type MenuPropsT = {
+  overrides?: {
+    List?: OverrideT<*>,
+    Option?: OverrideT<*>,
+  },
+};
+
+export type MenuProfilePropsT = {
+  getProfileItemLabels: GetProfileItemLabelsFnT,
+  getProfileItemImg: GetProfileItemImgFnT,
+  getProfileItemImgText: GetProfileItemImgTextFnT,
+  overrides?: ProfileOverridesT,
 };
 
-export type StatelessMenuPropsT = {
+export type SharedStatelessPropsT = {
   items: ItemsT,
-  getItemLabel: GetItemLabelFnT,
   rootRef: RootRefT,
   getRequiredItemProps?: GetRequiredItemPropsFnT,
   highlightedIndex?: number,
-  overrides?: OverridesT,
+};
+
+export type StatefulMenuPropsT = StatefulContainerPropsT & MenuPropsT;
+
+export type StatefulMenuProfilePropsT = StatefulContainerPropsT &
+  MenuProfilePropsT;
+
+export type StatelessMenuPropsT = SharedStatelessPropsT & MenuPropsT;
+
+export type StatelessMenuProfilePropsT = SharedStatelessPropsT &
+  MenuProfilePropsT;
+
+export type OptionListPropsT = {
+  item: *,
+  getItemLabel: GetItemLabelFnT,
+  overrides?: {
+    ListItem?: OverrideT<*>,
+  },
+};
+
+export type OptionProfilePropsT = {
+  item: *,
+  getProfileItemLabels: GetProfileItemLabelsFnT,
+  getProfileItemImg: GetProfileItemImgFnT,
+  getProfileItemImgText: GetProfileItemImgTextFnT,
+  overrides?: {
+    ListItemProfile?: OverrideT<*>,
+    ProfileImgContainer?: OverrideT<*>,
+    ProfileImg?: OverrideT<*>,
+    ProfileLabelsContainer?: OverrideT<*>,
+    ProfileTitle?: OverrideT<*>,
+    ProfileSubtitle?: OverrideT<*>,
+    ProfileBody?: OverrideT<*>,
+  },
 };
diff --git a/src/menu/utils.js b/src/menu/utils.js
index da0075ff7..33345e372 100644
--- a/src/menu/utils.js
+++ b/src/menu/utils.js
@@ -7,6 +7,7 @@ LICENSE file in the root directory of this source tree.
 // @flow
 /* eslint-disable import/prefer-default-export */
 import smoothscroll from 'smoothscroll-polyfill';
+import {STYLETRON_PROP_MAPPER} from './constants';
 
 if (__BROWSER__) {
   smoothscroll.polyfill();
@@ -17,7 +18,10 @@ if (__BROWSER__) {
  * all of the existing prop keys inside mapper with $ to present styletron
  * from passing through those props to the underlying React component.
  */
-export function mapStyletronProps(props: {}, mapper: {}): {} {
+export function mapStyletronProps(
+  props: {},
+  mapper: {} = STYLETRON_PROP_MAPPER,
+): {} {
   return Object.keys(props).reduce((newProps, propName) => {
     const newName = mapper[propName] ? `$${propName}` : propName;
     newProps[newName] = props[propName];
diff --git a/src/select/dropdown.js b/src/select/dropdown.js
index 767f276fc..cdd13ad9d 100644
--- a/src/select/dropdown.js
+++ b/src/select/dropdown.js
@@ -43,48 +43,59 @@ const SelectDropDown = function(props: DropDownPropsT) {
   } = props;
   return options.length ? (
     <StatefulMenu
-      getItemLabel={option => {
-        const $selected = selectedOptions.find(
-          selected => selected.id === option.id,
-        );
-        const events = option.disabled
-          ? {
-              onClickCapture: e => e.stopPropagation(),
-            }
-          : {
-              onClick: e =>
-                onChange(e, STATE_CHANGE_TYPE.select, option.id, option.label),
-            };
-        return (
-          <Option
-            disabled={option.disabled}
-            $selected={$selected}
-            {...events}
-            key={option.id}
-          >
-            {$selected && (
-              <SearchIcon
-                $type={ICON.selected}
-                src={
-                  'data:image/svg+xml;utf8,<svg width="11" height="10" viewBox="0 0 11 10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 6L4 9L10 1" stroke="#1B6DE0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
-                }
-              />
-            )}
-            {getOptionLabel(option)}
-          </Option>
-        );
-      }}
       overrides={{
         /* eslint-disable-next-line react/display-name*/
-        List: listProps => (
-          <DropDown
-            {...listProps}
-            $rows={rows}
-            $type={type}
-            $isOpen={isDropDownOpen}
-          />
-        ),
-        ListItem: DropDownItem,
+        List: {
+          component: DropDown,
+          props: {
+            $rows: rows,
+            $type: type,
+            $isOpen: isDropDownOpen,
+          },
+        },
+        Option: {
+          props: {
+            overrides: {
+              ListItem: DropDownItem,
+            },
+            getItemLabel: option => {
+              const $selected = selectedOptions.find(
+                selected => selected.id === option.id,
+              );
+              const events = option.disabled
+                ? {
+                    onClickCapture: e => e.stopPropagation(),
+                  }
+                : {
+                    onClick: e =>
+                      onChange(
+                        e,
+                        STATE_CHANGE_TYPE.select,
+                        option.id,
+                        option.label,
+                      ),
+                  };
+              return (
+                <Option
+                  disabled={option.disabled}
+                  $selected={$selected}
+                  {...events}
+                  key={option.id}
+                >
+                  {$selected && (
+                    <SearchIcon
+                      $type={ICON.selected}
+                      src={
+                        'data:image/svg+xml;utf8,<svg width="11" height="10" viewBox="0 0 11 10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 6L4 9L10 1" stroke="#1B6DE0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
+                      }
+                    />
+                  )}
+                  {getOptionLabel(option)}
+                </Option>
+              );
+            },
+          },
+        },
       }}
       items={options}
     />
diff --git a/src/select/styled-components.js b/src/select/styled-components.js
index 9ab323431..b4795aca0 100644
--- a/src/select/styled-components.js
+++ b/src/select/styled-components.js
@@ -137,7 +137,6 @@ export const DropDown = styled('ul', props => {
     display: !$isOpen ? 'none' : null,
     width: '96%',
     position: 'absolute',
-    padding: scale600,
     listStyle: 'none',
     borderRadius: scale300,
     boxShadow: $theme.lighting.shadow600,
@@ -159,18 +158,13 @@ export const Option = styled('div', props => {
   const {$selected, disabled, $theme} = props;
   const {
     colors: {mono700, primary400},
-    sizing: {scale300},
   } = $theme;
   const padding = $selected
     ? {
-        paddingTop: scale300,
-        paddingBottom: scale300,
         paddingRight: '0px',
         paddingLeft: '0px',
       }
     : {
-        paddingTop: scale300,
-        paddingBottom: scale300,
         paddingRight: '18px',
         paddingLeft: '18px',
       };


Test patch:
diff --git a/src/helpers/__tests__/overrides.test.js b/src/helpers/__tests__/overrides.test.js
index 3e1263ce6..5c0bfc400 100644
--- a/src/helpers/__tests__/overrides.test.js
+++ b/src/helpers/__tests__/overrides.test.js
@@ -11,6 +11,7 @@ import {
   getOverrideProps,
   toObjectOverride,
   mergeOverrides,
+  getOverrideObject,
 } from '../overrides';
 
 function getMockComponent<T>(): React.ComponentType<T> {
@@ -93,3 +94,41 @@ test('Helpers - Overrides - mergeOverrides', () => {
     },
   });
 });
+
+test('Helpers - Overrides - getOverrideObject', () => {
+  const DefaultComponent = getMockComponent();
+  const OverrideComponent = getMockComponent();
+
+  expect(getOverrideObject(null, DefaultComponent)).toEqual({
+    component: DefaultComponent,
+    props: {},
+  });
+
+  expect(getOverrideObject(OverrideComponent, DefaultComponent)).toEqual({
+    component: OverrideComponent,
+    props: {},
+  });
+
+  expect(
+    getOverrideObject(
+      {
+        component: OverrideComponent,
+        props: {
+          custom: 'prop',
+        },
+        style: {
+          cursor: 'pointer',
+        },
+      },
+      DefaultComponent,
+    ),
+  ).toEqual({
+    component: OverrideComponent,
+    props: {
+      custom: 'prop',
+      $style: {
+        cursor: 'pointer',
+      },
+    },
+  });
+});
diff --git a/src/menu/__tests__/__snapshots__/styled-components.test.js.snap b/src/menu/__tests__/__snapshots__/styled-components.test.js.snap
index 6aa95662f..1e3e72b0a 100644
--- a/src/menu/__tests__/__snapshots__/styled-components.test.js.snap
+++ b/src/menu/__tests__/__snapshots__/styled-components.test.js.snap
@@ -1,57 +1,155 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Menu Styled Components List - basic render: List has correct styles 1`] = `
+exports[`Menu Styled Components List - basic render: correct styles 1`] = `
 Object {
   "background": "$theme.colors.white",
   "borderRadius": "$theme.borders.radius300",
   "boxShadow": "$theme.lighting.shadow600",
-  "display": "flex",
-  "flexDirection": "column",
   "margin": 0,
-  "paddingBottom": "$theme.sizing.scale200",
-  "paddingLeft": "$theme.sizing.scale400",
-  "paddingRight": "$theme.sizing.scale400",
-  "paddingTop": "$theme.sizing.scale200",
+  "padding": 0,
   "position": "relative",
 }
 `;
 
-exports[`Menu Styled Components ListItem - basic render: ListItem has correct styles 1`] = `
+exports[`Menu Styled Components ListItem - basic render: correct styles 1`] = `
 Object {
+  ":first-child": Object {
+    "paddingTop": "$theme.sizing.scale600",
+  },
   ":hover": Object {
-    "color": "$theme.colors.primary400",
+    "backgroundColor": "$theme.colors.mono300",
+  },
+  ":last-child": Object {
+    "paddingBottom": "$theme.sizing.scale600",
   },
   "color": "$theme.colors.black",
   "cursor": "pointer",
   "display": "block",
-  "fontFamily": "$theme.typography.font400.fontFamily",
-  "fontSize": "$theme.typography.font400.fontSize",
-  "fontWeight": "$theme.typography.font400.fontWeight",
-  "lineHeight": "$theme.typography.font400.lineHeight",
-  "margin": 0,
+  "fontFamily": "$theme.typography.font300.fontFamily",
+  "fontSize": "$theme.typography.font300.fontSize",
+  "fontWeight": "$theme.typography.font300.fontWeight",
+  "lineHeight": "$theme.typography.font300.lineHeight",
+  "paddingBottom": "$theme.sizing.scale300",
+  "paddingLeft": "$theme.sizing.scale600",
+  "paddingRight": "$theme.sizing.scale600",
+  "paddingTop": "$theme.sizing.scale300",
   "position": "relative",
   "transitionDuration": "$theme.animation.timing100",
-  "transitionProperty": "color",
+  "transitionProperty": "color, background-color",
   "transitionTimingFunction": "$theme.animation.easeOutCurve",
 }
 `;
 
-exports[`Menu Styled Components ListItem - highlighted render: ListItem has correct styles when highlighted 1`] = `
+exports[`Menu Styled Components ListItem - highlighted render: correct styles 1`] = `
 Object {
+  ":first-child": Object {
+    "paddingTop": "$theme.sizing.scale600",
+  },
   ":hover": Object {
-    "color": "$theme.colors.primary400",
+    "backgroundColor": "$theme.colors.mono300",
+  },
+  ":last-child": Object {
+    "paddingBottom": "$theme.sizing.scale600",
   },
   "color": "$theme.colors.primary",
   "cursor": "pointer",
   "display": "block",
-  "fontFamily": "$theme.typography.font400.fontFamily",
-  "fontSize": "$theme.typography.font400.fontSize",
-  "fontWeight": "$theme.typography.font400.fontWeight",
-  "lineHeight": "$theme.typography.font400.lineHeight",
-  "margin": 0,
+  "fontFamily": "$theme.typography.font300.fontFamily",
+  "fontSize": "$theme.typography.font300.fontSize",
+  "fontWeight": "$theme.typography.font300.fontWeight",
+  "lineHeight": "$theme.typography.font300.lineHeight",
+  "paddingBottom": "$theme.sizing.scale300",
+  "paddingLeft": "$theme.sizing.scale600",
+  "paddingRight": "$theme.sizing.scale600",
+  "paddingTop": "$theme.sizing.scale300",
   "position": "relative",
   "transitionDuration": "$theme.animation.timing100",
-  "transitionProperty": "color",
+  "transitionProperty": "color, background-color",
   "transitionTimingFunction": "$theme.animation.easeOutCurve",
 }
 `;
+
+exports[`Menu Styled Components ListItemProfile - basic render: correct styles 1`] = `
+Object {
+  ":first-child": Object {
+    "paddingTop": "$theme.sizing.scale800",
+  },
+  ":hover": Object {
+    "backgroundColor": "$theme.colors.mono300",
+  },
+  ":last-child": Object {
+    "paddingBottom": "$theme.sizing.scale800",
+  },
+  "alignItems": "center",
+  "cursor": "pointer",
+  "display": "flex",
+  "paddingBottom": "$theme.sizing.scale300",
+  "paddingLeft": "$theme.sizing.scale800",
+  "paddingRight": "$theme.sizing.scale800",
+  "paddingTop": "$theme.sizing.scale300",
+  "position": "relative",
+  "transitionDuration": "$theme.animation.timing100",
+  "transitionProperty": "color, background-color",
+  "transitionTimingFunction": "$theme.animation.easeOutCurve",
+}
+`;
+
+exports[`Menu Styled Components ProfileBody - basic render: correct styles 1`] = `
+Object {
+  "color": "$theme.colors.mono800",
+  "fontFamily": "$theme.typography.font200.fontFamily",
+  "fontSize": "$theme.typography.font200.fontSize",
+  "fontWeight": "$theme.typography.font200.fontWeight",
+  "lineHeight": "$theme.typography.font200.lineHeight",
+  "margin": 0,
+}
+`;
+
+exports[`Menu Styled Components ProfileImg - basic render: correct styles 1`] = `
+Object {
+  "borderRadius": "50%",
+  "height": "100%",
+  "width": "100%",
+}
+`;
+
+exports[`Menu Styled Components ProfileImgContainer - basic render: correct styles 1`] = `
+Object {
+  "alignItems": "center",
+  "display": "flex",
+  "height": "60px",
+  "justifyContent": "center",
+  "width": "60px",
+}
+`;
+
+exports[`Menu Styled Components ProfileLabelsContainer - basic render: correct styles 1`] = `
+Object {
+  "alignSelf": "flex-start",
+  "display": "flex",
+  "flexDirection": "column",
+  "marginLeft": "$theme.sizing.scale600",
+}
+`;
+
+exports[`Menu Styled Components ProfileSubtitle - basic render: correct styles 1`] = `
+Object {
+  "color": "$theme.colors.black",
+  "fontFamily": "$theme.typography.font300.fontFamily",
+  "fontSize": "$theme.typography.font300.fontSize",
+  "fontWeight": "$theme.typography.font300.fontWeight",
+  "lineHeight": "$theme.typography.font300.lineHeight",
+  "margin": 0,
+}
+`;
+
+exports[`Menu Styled Components ProfileTitle - basic render: correct styles 1`] = `
+Object {
+  "color": "$theme.colors.black",
+  "fontFamily": "$theme.typography.font450.fontFamily",
+  "fontSize": "$theme.typography.font450.fontSize",
+  "fontWeight": "$theme.typography.font450.fontWeight",
+  "lineHeight": "$theme.typography.font450.lineHeight",
+  "margin": 0,
+}
+`;
diff --git a/src/menu/__tests__/menu.test.js b/src/menu/__tests__/menu.test.js
index c7116dcca..f8c004a5a 100644
--- a/src/menu/__tests__/menu.test.js
+++ b/src/menu/__tests__/menu.test.js
@@ -7,7 +7,8 @@ LICENSE file in the root directory of this source tree.
 // @flow
 import React from 'react';
 import {mount} from 'enzyme';
-import {List, ListItem} from '../styled-components';
+import {List} from '../styled-components';
+import OptionList from '../option-list';
 import Menu from '../menu';
 
 const mockItems = [{label: 'item1'}, {label: 'item2'}];
@@ -17,16 +18,11 @@ function getSharedProps() {
     items: mockItems,
     getItemLabel: item => item.label,
     rootRef: React.createRef(),
-    overrides: {
-      List,
-      ListItem,
-    },
   };
 }
 
 describe('Menu Stateless Component', () => {
   test('basic renders', () => {
-    // $FlowFixMe
     const component = mount(<Menu {...getSharedProps()} />);
 
     expect(component.find(List)).toExist();
@@ -34,15 +30,7 @@ describe('Menu Stateless Component', () => {
       $ref: React.createRef(),
     });
 
-    expect(component.find(ListItem)).toExist();
-    expect(component.find(ListItem).first()).toHaveProp({});
-
-    expect(
-      component
-        .find(ListItem)
-        .first()
-        .text(),
-    ).toEqual(mockItems[0].label);
+    expect(component.find(OptionList)).toExist();
 
     component.setProps({
       getRequiredItemProps: (item, index) => ({
@@ -50,24 +38,32 @@ describe('Menu Stateless Component', () => {
         isHighlighted: true,
       }),
     });
-    expect(component.find(ListItem).first()).toHaveProp({
+    expect(component.find(OptionList).first()).toHaveProp({
       $isHighlighted: true,
     });
   });
 
   test('renders with components overrides', () => {
-    const NewListItem = jest
-      .fn()
-      .mockImplementation(() => <div id="list-item" />);
+    const NewOption = () => <div />;
     const props = {
       ...getSharedProps(),
       overrides: {
-        ListItem: NewListItem,
+        Option: {
+          component: NewOption,
+          props: {
+            custom: 'prop',
+          },
+        },
       },
     };
-    // $FlowFixMe
     const component = mount(<Menu {...props} />);
-    expect(component.find(ListItem)).not.toExist();
-    expect(component.find(NewListItem)).toExist();
+    expect(component.find(OptionList)).not.toExist();
+    expect(component.find(NewOption)).toExist();
+    expect(
+      component
+        .find(NewOption)
+        .first()
+        .prop('custom'),
+    ).toEqual('prop');
   });
 });
diff --git a/src/menu/__tests__/option-list.test.js b/src/menu/__tests__/option-list.test.js
new file mode 100644
index 000000000..cdaed1ecd
--- /dev/null
+++ b/src/menu/__tests__/option-list.test.js
@@ -0,0 +1,48 @@
+// @flow
+import React from 'react';
+import {mount} from 'enzyme';
+import {ListItem} from '../styled-components';
+import OptionList from '../option-list';
+
+const mockItem = {label: 'item1'};
+
+function getSharedProps() {
+  return {
+    item: mockItem,
+    getItemLabel: item => item.label,
+  };
+}
+
+describe('Option List Stateless Component', () => {
+  test('basic renders', () => {
+    const component = mount(<OptionList {...getSharedProps()} />);
+
+    expect(component.find(ListItem)).toExist();
+
+    expect(
+      component
+        .find(ListItem)
+        .first()
+        .text(),
+    ).toEqual(mockItem.label);
+  });
+
+  test('renders with components overrides', () => {
+    const NewListItem = () => <div id="list-item" />;
+    const props = {
+      ...getSharedProps(),
+      overrides: {
+        ListItem: {
+          component: NewListItem,
+          props: {
+            custom: 'prop',
+          },
+        },
+      },
+    };
+    const component = mount(<OptionList {...props} />);
+    expect(component.find(ListItem)).not.toExist();
+    expect(component.find(NewListItem)).toExist();
+    expect(component.find(NewListItem).prop('custom')).toEqual('prop');
+  });
+});
diff --git a/src/menu/__tests__/option-profile.test.js b/src/menu/__tests__/option-profile.test.js
new file mode 100644
index 000000000..c3c6effdc
--- /dev/null
+++ b/src/menu/__tests__/option-profile.test.js
@@ -0,0 +1,100 @@
+// @flow
+import React from 'react';
+import {mount} from 'enzyme';
+import {
+  ListItemProfile,
+  ProfileImg,
+  ProfileTitle,
+  ProfileSubtitle,
+  ProfileBody,
+} from '../styled-components';
+import OptionProfile from '../option-profile';
+
+const mockItem = {
+  title: 'David Smith',
+  subtitle: 'Senior Engineering Manager',
+  body: 'Uber Everything',
+  imgUrl: 'imgUrl',
+};
+
+function getSharedProps() {
+  return {
+    item: mockItem,
+    getProfileItemLabels: ({title, subtitle, body}) => ({
+      title,
+      subtitle,
+      body,
+    }),
+    getProfileItemImg: item => item.imgUrl,
+    getProfileItemImgText: item => item.title,
+  };
+}
+
+describe('Option Profile Stateless Component', () => {
+  test('basic renders', () => {
+    const component = mount(<OptionProfile {...getSharedProps()} />);
+
+    expect(component.find(ListItemProfile)).toExist();
+
+    expect(
+      component
+        .find(ProfileTitle)
+        .first()
+        .text(),
+    ).toEqual(mockItem.title);
+
+    expect(
+      component
+        .find(ProfileSubtitle)
+        .first()
+        .text(),
+    ).toEqual(mockItem.subtitle);
+
+    expect(
+      component
+        .find(ProfileBody)
+        .first()
+        .text(),
+    ).toEqual(mockItem.body);
+
+    expect(
+      component
+        .find(ProfileImg)
+        .first()
+        .prop('src'),
+    ).toEqual(mockItem.imgUrl);
+  });
+
+  test('renders with custom image component', () => {
+    function MockComponent() {
+      return <div />;
+    }
+    const props = {
+      ...getSharedProps(),
+      getProfileItemImg: item => MockComponent,
+    };
+    const component = mount(<OptionProfile {...props} />);
+    expect(component.find(MockComponent)).toExist();
+  });
+
+  test('renders with components overrides', () => {
+    function MockComponent() {
+      return <div />;
+    }
+    const props = {
+      ...getSharedProps(),
+      overrides: {
+        ListItemProfile: {
+          component: MockComponent,
+          props: {
+            custom: 'prop',
+          },
+        },
+      },
+    };
+    const component = mount(<OptionProfile {...props} />);
+    expect(component.find(ListItemProfile)).not.toExist();
+    expect(component.find(MockComponent)).toExist();
+    expect(component.find(MockComponent).prop('custom')).toEqual('prop');
+  });
+});
diff --git a/src/menu/__tests__/stateful-container.test.js b/src/menu/__tests__/stateful-container.test.js
index 38214755c..2ac21db05 100644
--- a/src/menu/__tests__/stateful-container.test.js
+++ b/src/menu/__tests__/stateful-container.test.js
@@ -15,7 +15,6 @@ import {scrollItemIntoView} from '../utils';
 jest.mock('../utils');
 
 const mockItems = [{label: 'item1'}, {label: 'item2'}];
-const mockGetItemLabel = item => item.label;
 const mockChildrenFn = jest.fn().mockImplementation(() => <div />);
 const mockItemSelect = jest.fn();
 
@@ -25,7 +24,6 @@ const originalRemoveEventListener = document.removeEventListener;
 function getSharedProps() {
   return {
     items: mockItems,
-    getItemLabel: mockGetItemLabel,
     onItemSelect: mockItemSelect,
     children: mockChildrenFn,
     stateReducer: jest
@@ -56,7 +54,6 @@ describe('Menu StatefulContainer', () => {
   test('renders and passes required props to children function', () => {
     const props = {
       items: mockItems,
-      getItemLabel: mockGetItemLabel,
       onItemSelect: mockItemSelect,
       children: mockChildrenFn,
     };
@@ -66,7 +63,6 @@ describe('Menu StatefulContainer', () => {
     expect(mockChildrenFn.mock.calls[0][0]).toEqual({
       highlightedIndex: -1,
       items: mockItems,
-      getItemLabel: mockGetItemLabel,
       rootRef: React.createRef(),
       getRequiredItemProps: component.instance().getRequiredItemProps,
     });
diff --git a/src/menu/__tests__/styled-components.test.js b/src/menu/__tests__/styled-components.test.js
index b10c557c1..01724725d 100644
--- a/src/menu/__tests__/styled-components.test.js
+++ b/src/menu/__tests__/styled-components.test.js
@@ -5,33 +5,90 @@ This source code is licensed under the MIT license found in the
 LICENSE file in the root directory of this source tree.
 */
 // @flow
-import React from 'react';
+import * as React from 'react';
 import {shallow} from 'enzyme';
-import {List, ListItem} from '../styled-components';
+import {
+  List,
+  ListItem,
+  ListItemProfile,
+  ProfileBody,
+  ProfileImg,
+  ProfileTitle,
+  ProfileSubtitle,
+  ProfileImgContainer,
+  ProfileLabelsContainer,
+} from '../styled-components';
+
+function makeTest({
+  title,
+  component: Component,
+  props = {},
+  children = null,
+  snapshotName = 'correct styles',
+}: {
+  title: string,
+  component: React.ComponentType<*>,
+  props?: {},
+  children?: *,
+  snapshotName?: string,
+}) {
+  test(title, () => {
+    const shallowed = shallow(<Component {...props}>{children}</Component>);
+    expect(shallowed.instance().getStyles()).toMatchSnapshot(snapshotName);
+  });
+}
 
 describe('Menu Styled Components', () => {
-  test('ListItem - basic render', () => {
-    const component = shallow(<ListItem>This is a list item</ListItem>);
-    expect(component.instance().getStyles()).toMatchSnapshot(
-      'ListItem has correct styles',
-    );
-  });
-
-  test('ListItem - highlighted render', () => {
-    const component = shallow(
-      <ListItem $isHighlighted={true}>
-        This is a highlighted list item
-      </ListItem>,
-    );
-    expect(component.instance().getStyles()).toMatchSnapshot(
-      'ListItem has correct styles when highlighted',
-    );
-  });
-
-  test('List - basic render', () => {
-    const component = shallow(<List />);
-    expect(component.instance().getStyles()).toMatchSnapshot(
-      'List has correct styles',
-    );
+  makeTest({
+    title: 'ListItem - basic render',
+    component: ListItem,
+  });
+
+  makeTest({
+    title: 'ListItem - highlighted render',
+    component: ListItem,
+    props: {
+      $isHighlighted: true,
+    },
+  });
+
+  makeTest({
+    title: 'List - basic render',
+    component: List,
+  });
+
+  makeTest({
+    title: 'ListItemProfile - basic render',
+    component: ListItemProfile,
+  });
+
+  makeTest({
+    title: 'ProfileImgContainer - basic render',
+    component: ProfileImgContainer,
+  });
+
+  makeTest({
+    title: 'ProfileImg - basic render',
+    component: ProfileImg,
+  });
+
+  makeTest({
+    title: 'ProfileLabelsContainer - basic render',
+    component: ProfileLabelsContainer,
+  });
+
+  makeTest({
+    title: 'ProfileTitle - basic render',
+    component: ProfileTitle,
+  });
+
+  makeTest({
+    title: 'ProfileSubtitle - basic render',
+    component: ProfileSubtitle,
+  });
+
+  makeTest({
+    title: 'ProfileBody - basic render',
+    component: ProfileBody,
   });
 });
diff --git a/src/select/__tests__/__snapshots__/styled-components.test.js.snap b/src/select/__tests__/__snapshots__/styled-components.test.js.snap
index 819398105..b16663287 100644
--- a/src/select/__tests__/__snapshots__/styled-components.test.js.snap
+++ b/src/select/__tests__/__snapshots__/styled-components.test.js.snap
@@ -6,16 +6,11 @@ Object {
   "borderRadius": "$theme.sizing.scale300",
   "boxShadow": "$theme.lighting.shadow600",
   "display": "none",
-  "flexDirection": "column",
   "height": null,
   "listStyle": "none",
   "margin": 0,
   "overflowY": null,
-  "padding": "$theme.sizing.scale600",
-  "paddingBottom": "$theme.sizing.scale200",
-  "paddingLeft": "$theme.sizing.scale400",
-  "paddingRight": "$theme.sizing.scale400",
-  "paddingTop": "$theme.sizing.scale200",
+  "padding": 0,
   "position": "absolute",
   "top": null,
   "width": "96%",
@@ -28,16 +23,11 @@ Object {
   "borderRadius": "$theme.sizing.scale300",
   "boxShadow": "$theme.lighting.shadow600",
   "display": null,
-  "flexDirection": "column",
   "height": null,
   "listStyle": "none",
   "margin": 0,
   "overflowY": null,
-  "padding": "$theme.sizing.scale600",
-  "paddingBottom": "$theme.sizing.scale200",
-  "paddingLeft": "$theme.sizing.scale400",
-  "paddingRight": "$theme.sizing.scale400",
-  "paddingTop": "$theme.sizing.scale200",
+  "padding": 0,
   "position": "absolute",
   "top": null,
   "width": "96%",
@@ -50,16 +40,11 @@ Object {
   "borderRadius": "$theme.sizing.scale300",
   "boxShadow": "$theme.lighting.shadow600",
   "display": "none",
-  "flexDirection": "column",
   "height": "NaNpx",
   "listStyle": "none",
   "margin": 0,
   "overflowY": "scroll",
-  "padding": "$theme.sizing.scale600",
-  "paddingBottom": "$theme.sizing.scale200",
-  "paddingLeft": "$theme.sizing.scale400",
-  "paddingRight": "$theme.sizing.scale400",
-  "paddingTop": "$theme.sizing.scale200",
+  "padding": 0,
   "position": "absolute",
   "top": null,
   "width": "96%",
@@ -72,16 +57,11 @@ Object {
   "borderRadius": "$theme.sizing.scale300",
   "boxShadow": "$theme.lighting.shadow600",
   "display": "none",
-  "flexDirection": "column",
   "height": null,
   "listStyle": "none",
   "margin": 0,
   "overflowY": null,
-  "padding": "$theme.sizing.scale600",
-  "paddingBottom": "$theme.sizing.scale200",
-  "paddingLeft": "$theme.sizing.scale400",
-  "paddingRight": "$theme.sizing.scale400",
-  "paddingTop": "$theme.sizing.scale200",
+  "padding": 0,
   "position": "absolute",
   "top": "$theme.sizing.scale1000",
   "width": "96%",
@@ -90,20 +70,29 @@ Object {
 
 exports[`Select styled components StyledDropDownItem DropDownItem: StyledDropDownItem has correct styles 1`] = `
 Object {
+  ":first-child": Object {
+    "paddingTop": "$theme.sizing.scale600",
+  },
   ":hover": Object {
-    "color": "$theme.colors.primary400",
+    "backgroundColor": "$theme.colors.mono300",
+  },
+  ":last-child": Object {
+    "paddingBottom": "$theme.sizing.scale600",
   },
   "color": "$theme.colors.black",
   "cursor": "pointer",
   "display": "block",
-  "fontFamily": "$theme.typography.font400.fontFamily",
-  "fontSize": "$theme.typography.font400.fontSize",
-  "fontWeight": "$theme.typography.font400.fontWeight",
+  "fontFamily": "$theme.typography.font300.fontFamily",
+  "fontSize": "$theme.typography.font300.fontSize",
+  "fontWeight": "$theme.typography.font300.fontWeight",
   "lineHeight": "$theme.sizing.scale600",
-  "margin": 0,
+  "paddingBottom": "$theme.sizing.scale300",
+  "paddingLeft": "$theme.sizing.scale600",
+  "paddingRight": "$theme.sizing.scale600",
+  "paddingTop": "$theme.sizing.scale300",
   "position": "relative",
   "transitionDuration": "$theme.animation.timing100",
-  "transitionProperty": "color",
+  "transitionProperty": "color, background-color",
   "transitionTimingFunction": "$theme.animation.easeOutCurve",
 }
 `;
@@ -173,10 +162,8 @@ Object {
     "cursor": "pointer",
   },
   "color": null,
-  "paddingBottom": "$theme.sizing.scale300",
   "paddingLeft": "18px",
   "paddingRight": "18px",
-  "paddingTop": "$theme.sizing.scale300",
 }
 `;
 
@@ -186,10 +173,8 @@ Object {
     "cursor": "pointer",
   },
   "color": "$theme.colors.primary400",
-  "paddingBottom": "$theme.sizing.scale300",
   "paddingLeft": "0px",
   "paddingRight": "0px",
-  "paddingTop": "$theme.sizing.scale300",
 }
 `;
 
@@ -199,10 +184,8 @@ Object {
     "cursor": "not-allowed",
   },
   "color": "$theme.colors.mono700",
-  "paddingBottom": "$theme.sizing.scale300",
   "paddingLeft": "18px",
   "paddingRight": "18px",
-  "paddingTop": "$theme.sizing.scale300",
 }
 `;