No Time Dad

A blog about web development written by a busy dad

Team Member Cards in a Responsive Grid

I don’t have a good reason to build a team member section aside from the fact that I thought it might be challenging, and it was. Like anything, it can be as complex as you want. I wanted to use css grid, and I wanted the grid to be responsive. Which means that I am going to be changing the number of columns, and it also means that items in the columns will also need to change based on the screen size.

I took some inspiration from the examples at tailwindui and decided to try making a dark version of the team member section. I ended up using a palette with shades of green, dark to light. Why green? Well, I have been making a lot of components lately that have either a white background or dark gray background and I wanted to try something new. I don’t know if I would use these colors on a production site, but it was fun to try out a new palette. Also, I can change the palette easily since the colors are defined as css custom properties.

A demo of this component can be found here. Try changing the viewport size to see how it changes.

The team member section starts with a wrapper div where I defined the color palette, set the text color, set the background color, and give the entire element padding. The nice thing about css custom properties that I recently realized is that you can define them in a parent div and use them in child divs. For some reason I thought they had to be defined in :root or body. I like having them in a parent div because it gives context on where they’re being used. Another trick I picked up recently is to define css properties in the parent that I’ll use throughout its children. For example, if I knew I wanted all of the text in the section to be white, I’d define color: white; in the parent element’s css selector. This keeps me from having to define the properties and values multiple times. I can always change a property in a child selector when I need another value.

Above the team member card grid I’ll have text that says something inviting like, “Our team” and “Meet the awesome team behind this product”. I would swap out “this product” for the name of the actual product I was working on but this text is a placeholder, like much of the content.

Underneath the inviting text is where the card grid will be. On small screens I wanted the grid to just be a single column. As the screen size increases it should switch to two columns, then eventually to three columns on large screens.

const TeamMemberSection = () => (
  <div className={styles.Wrapper}>
    <div className={styles.Top__Wrapper}>
      ...
    </div>
    <div className={styles.Grid__Wrapper}>
      ...
    </div>
  </div>
);
.Wrapper {
  /* Custom palette */
  --nyanza: #d8f3dcff;
  --brunswick-green: #1b4332ff;
  --dark-jungle-green: #081c15ff;
  background-color: var(--dark-jungle-green);
  padding: 1rem;
  /* Color that will be used throughout the element */
  color: var(--nyanza);
}

.Top__Wrapper {
  display: flex;
  flex-direction: column;
  padding-bottom: 2rem;
}

.Grid__Wrapper {
  display: grid;
  gap: 1rem;
  /* Single column on small screens */
  grid-template-columns: auto;
}

Inside the Top__Wrapper div there is large header text and smaller sub-header text. As far as the size of the large header text goes, an h1 tag would work but I didn’t want to make any assumptions about whether there was already an existing h1 on the page already. I only want one h1 on a page. This is important for screen readers to provide users with a fluid experience on the page. With that in mind, I ended up wrapping the large text in a div and wrapping the small text in another div so I could modify the sizes via css.

const TeamMemberSection = () => (
  <div className={styles.Wrapper}>
    <div className={styles.Top__Wrapper}>
      <div className={styles.Top__Text}>
        Our team
      </div>
      <div className={styles.Top__Sub_Text}>
        Meet the awesome team behind this product
      </div>
    </div>
    <div className={styles.Grid__Wrapper}>
      ...
    </div>
  </div>
);
...
.Top__Text {
  font-size: 38px;
  font-weight: 600;
  padding-bottom: 0.5rem;
}

.Top__Sub_Text {
  font-size: 18px;
  font-weight: 600;
  opacity: 0.8;
}
...

The cards were the most complicated part. On small screens, I started out having the avatar being displayed with the name, title, and social icons centered to the right of it. This mostly worked for me at first, but the alignment was thrown off as soon as I added a name that was longer than “John Smith”. So, I decided to hide the avatar until the screen size reached 375 pixels. And even then I am sure that super long names will cause problems. I thought about clamping the line, but potentially cutting off a name makes clamping a bad choice. There is probably a “right” solution out there for this, but I think what I would do if I had a long name on a card is to just change the breakpoint so the avatar shows on larger screens, or reduce the image size.

On larger screens, I wanted the image size to increase. I also wanted the person’s name, title, and social media icons to sit under their image. The way I chose to do all of the position is to put everything into a flexbox container and change the flex-direction based on the screen size. Sometimes I forget how magical flexbox can be.

Aside from the structure and responsiveness of the cards I added flashy things like opacity changes, border color changes, and box-shadows on hover. I usually try to keep these kind of things as subtle as possible, and always using colors that are in the color palette.

import avatar_man_1 from './avatar_man_1.png';

const TeamMemberSection = () => (
  <div className={styles.Wrapper}>
    {/* Omitted for brevity */}
    <div className={styles.Grid__Wrapper}>
      <Card name="John Smith" position="Founder" avatarImage={avatar_man_1} />
      ...
    </div>
  </div>
);

const Card = ({ name, position, avatarImage }) => (
  <div className={styles.Card}>
    <img className={styles.Avatar__Image} src={avatarImage} alt="avatar"></img>
    <div className={styles.Card__Title}>
      <div className={styles.Title__Name}>{name}</div>
      <div className={styles.Title__Position}>{position}</div>
      <div className={styles.Title__Social}>
        {/* Social icons */}
      </div>
    </div>
  </div>
);
...
.Card {
  /* Init flexbox */
  display: flex;
  align-items: center;
  padding: 1rem;
  /* Contrasting color from the parent background-color */
  background-color: var(--brunswick-green);
  border: 1px solid var(--nyanza);
  border-radius: 0.5rem;
}

.Card:hover {
  cursor: pointer;
  box-shadow: 0 1px 6px var(--nyanza);
}

.Card__Header {
  display: flex;
  align-items: center;
}

.Avatar__Image {
  /* Hide the avatar on small screens */
  display: none;
}

.Card__Title {
  display: flex;
  flex-direction: column;
  padding-left: 1rem;
}

.Title__Name {
  font-size: 24px;
  font-weight: 600;
}

.Title__Position {
  font-size: 18px;
  opacity: 0.8;
}

.Title__Social {
  padding-top: 0.5rem;
}

.Social__Icon {
  width: 1.25rem;
  height: 1.25rem;
  margin-right: 0.5rem;
  opacity: 0.7;
  fill: var(--nyanza);
}

.Social__Icon:hover {
  opacity: 1;
}
...

As far as responsiveness goes, I targeted three viewport sizes; 375 pixels, 768 pixels, and 1024 pixels. My reason for picking these viewport sizes is because they’re the ones that show up in the device toolbar on Google chrome. Maybe there are better ones I should target, but I assume that Google put them there for a good reason.

I have been trying to stick to mobile-first design, so min-width is what I used for each media query. I started with a single column and add a second column when the screen size is 768 pixels or above. When the screen size is 1024 pixels or above, I switched the grid to 3 columns.

When the screen size is large enough (375 pixels), I wanted to show the avatar image on the card. As the size of the screen continues to increase, I’ll eventually (768 pixels) increase the avatar image size and change the flex-direction so the contents are stacked in a column instead of a row. Below are the media queries used to handle all of these changes.

@media (min-width: 375px) {
  .Avatar__Image {
    /* Show the avatar image */
    display: block;
    width: 80px;
    height: 80px;
    border-radius: 100%;
  }
}

@media (min-width: 768px) {
  .Grid__Wrapper {
    /* Switch to 2 columns that fill the available space */
    grid-template-columns: repeat(2, 1fr);
  }

  .Card {
    /* Stack the contents instead of having them in a row */
    flex-direction: column;
  }

  .Card__Title {
    align-items: center;
    justify-content: center;
  }

  .Avatar__Image {
    width: 150px;
    height: 150px;
  }

  .Title__Social {
    padding-top: 1.5rem;
  }
}

@media (min-width: 1024px) {
  .Grid__Wrapper {
    /* Switch to 3 columns that fill the available space */
    grid-template-columns: repeat(3, 1fr);
  }
}

Another fun project, but figuring the avatar image spacing was a headache. And it is still probably not right. I’m enjoying using css grid more, and it’s quickly becoming my display of choice for large layouts. I wasn’t sure about the colors I picked for this but I think they can break up a page nicely and might have a place after all. I do like trying colors and styles that are outside of the box, or “on the edge of the box” as Seth Godin would say.

Full code

Below is the full code this example. It is written using React because I find it easier to structure and explain my code using components. This component does not have any state or use any JavaScript (aside from React), so it can be translated straight to html if needed.

import React from 'react';
import * as styles from './index.module.css';
import avatar_man_1 from './avatar_man_1.png';
import avatar_woman_2 from './avatar_woman_2.png';

const Index = () => (
  <div className={styles.Wrapper}>
    <div className={styles.Top__Wrapper}>
      <div className={styles.Top__Text}>
        Our team
      </div>
      <div className={styles.Top__Sub_Text}>
        Meet the awesome team behind this product
      </div>
    </div>
    <div className={styles.Grid__Wrapper}>
      <Card name="John Smith" position="Founder" avatarImage={avatar_man_1} />
      <Card name="Sandra Holden" position="CTO" avatarImage={avatar_woman_2} />
      <Card name="Adam Jones" position="Design Director" avatarImage={avatar_man_1} />
      <Card name="Sara Lockley" position="Accounts Director" avatarImage={avatar_woman_2} />
      <Card name="Hank Oswald" position="Security" avatarImage={avatar_man_1} />
      <Card name="Ryan Cooler" position="Product Directory" avatarImage={avatar_man_1} />
    </div>
  </div>
);

const Card = ({ name, position, avatarImage }) => (
  <div className={styles.Card}>
    <img className={styles.Avatar__Image} src={avatarImage} alt="avatar"></img>
    <div className={styles.Card__Title}>
      <div className={styles.Title__Name}>{name}</div>
      <div className={styles.Title__Position}>{position}</div>
      <div className={styles.Title__Social}>
        <TwitterIcon />
        <FacebookIcon />
        <InstagramIcon />
      </div>
    </div>
  </div>
);

const TwitterIcon = () => (
  <svg className={styles.Social__Icon} role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Twitter icon</title><path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z" /></svg>
);

const FacebookIcon = () => (
  <svg className={styles.Social__Icon} role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Facebook icon</title><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" /></svg>
);

const InstagramIcon = () => (
  <svg className={styles.Social__Icon} role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Instagram icon</title><path d="M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.789.306-1.459.717-2.126 1.384S.935 3.35.63 4.14C.333 4.905.131 5.775.072 7.053.012 8.333 0 8.74 0 12s.015 3.667.072 4.947c.06 1.277.261 2.148.558 2.913.306.788.717 1.459 1.384 2.126.667.666 1.336 1.079 2.126 1.384.766.296 1.636.499 2.913.558C8.333 23.988 8.74 24 12 24s3.667-.015 4.947-.072c1.277-.06 2.148-.262 2.913-.558.788-.306 1.459-.718 2.126-1.384.666-.667 1.079-1.335 1.384-2.126.296-.765.499-1.636.558-2.913.06-1.28.072-1.687.072-4.947s-.015-3.667-.072-4.947c-.06-1.277-.262-2.149-.558-2.913-.306-.789-.718-1.459-1.384-2.126C21.319 1.347 20.651.935 19.86.63c-.765-.297-1.636-.499-2.913-.558C15.667.012 15.26 0 12 0zm0 2.16c3.203 0 3.585.016 4.85.071 1.17.055 1.805.249 2.227.415.562.217.96.477 1.382.896.419.42.679.819.896 1.381.164.422.36 1.057.413 2.227.057 1.266.07 1.646.07 4.85s-.015 3.585-.074 4.85c-.061 1.17-.256 1.805-.421 2.227-.224.562-.479.96-.899 1.382-.419.419-.824.679-1.38.896-.42.164-1.065.36-2.235.413-1.274.057-1.649.07-4.859.07-3.211 0-3.586-.015-4.859-.074-1.171-.061-1.816-.256-2.236-.421-.569-.224-.96-.479-1.379-.899-.421-.419-.69-.824-.9-1.38-.165-.42-.359-1.065-.42-2.235-.045-1.26-.061-1.649-.061-4.844 0-3.196.016-3.586.061-4.861.061-1.17.255-1.814.42-2.234.21-.57.479-.96.9-1.381.419-.419.81-.689 1.379-.898.42-.166 1.051-.361 2.221-.421 1.275-.045 1.65-.06 4.859-.06l.045.03zm0 3.678c-3.405 0-6.162 2.76-6.162 6.162 0 3.405 2.76 6.162 6.162 6.162 3.405 0 6.162-2.76 6.162-6.162 0-3.405-2.76-6.162-6.162-6.162zM12 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm7.846-10.405c0 .795-.646 1.44-1.44 1.44-.795 0-1.44-.646-1.44-1.44 0-.794.646-1.439 1.44-1.439.793-.001 1.44.645 1.44 1.439z" /></svg>
);

export default Index;
.Wrapper {
  --nyanza: #d8f3dcff;
  --brunswick-green: #1b4332ff;
  --dark-jungle-green: #081c15ff;
  color: var(--nyanza);
  background-color: var(--dark-jungle-green);
  padding: 1rem;
}

.Top__Wrapper {
  display: flex;
  flex-direction: column;
  padding-bottom: 2rem;
}

.Top__Text {
  font-size: 38px;
  font-weight: 600;
  padding-bottom: 0.5rem;
}

.Top__Sub_Text {
  font-size: 18px;
  font-weight: 600;
  opacity: 0.8;
}

.Grid__Wrapper {
  display: grid;
  gap: 1rem;
  grid-template-columns: auto;
}

.Card {
  display: flex;
  align-items: center;
  padding: 1rem;
  background-color: var(--brunswick-green);
  border: 1px solid var(--nyanza);
  border-radius: 0.5rem;
}

.Card:hover {
  cursor: pointer;
  box-shadow: 0 1px 6px var(--nyanza);
}

.Card__Header {
  display: flex;
  align-items: center;
}

.Avatar__Image {
  display: none;
}

.Card__Title {
  display: flex;
  flex-direction: column;
  padding-left: 1rem;
}

.Title__Name {
  font-size: 24px;
  font-weight: 600;
}

.Title__Position {
  font-size: 18px;
  opacity: 0.8;
}

.Title__Social {
  padding-top: 0.5rem;
}

.Social__Icon {
  width: 1.25rem;
  height: 1.25rem;
  margin-right: 0.5rem;
  opacity: 0.7;
  fill: var(--nyanza);
}

.Social__Icon:hover {
  opacity: 1;
}

@media (min-width: 375px) {
  .Avatar__Image {
    display: block;
    width: 80px;
    height: 80px;
    border-radius: 100%;
  }
}

@media (min-width: 768px) {
  .Grid__Wrapper {
    grid-template-columns: repeat(2, 1fr);
  }

  .Card {
    flex-direction: column;
  }

  .Card__Title {
    align-items: center;
    justify-content: center;
  }

  .Avatar__Image {
    width: 150px;
    height: 150px;
  }

  .Title__Social {
    padding-top: 1.5rem;
  }
}

@media (min-width: 1024px) {
  .Grid__Wrapper {
    grid-template-columns: repeat(3, 1fr);
  }
}