Component art. Writing a Facebook Messenger contact card

12 min


It is quite possible to evaluate a component and say that it is easy to write in HTML and CSS. I agree, it’s easy when you work just to practice, but in a real project it’s different. The perfect responsive component you just created quickly fails when it encounters the actual content of a real project. Why? Because while you are thinking about component design, you may be missing edge cases. I’ll show you a seemingly simple component that has a lot of work behind it. For the sake of realism, this will be an example straight from Facebook Messenger.


Let’s start

I am taking a very simple Facebook Messenger component, look at the screenshot below:

This sidebar lists the cards I wrote to on Facebook. Here I am only interested in the card. How would you write it in HTML / CSS? Very easy, right? It’s tempting to say it’s just a picture and a layer next to it. Here’s what to think about:

The case above occurs if you are just learning or competing in the UI challenge, but when you want to write a reliable card that stretches out, the example above quickly stops working. Let’s look at a simple option:

<div class="card">
  <img class="card__image" src="assets/shadeed.jpg" alt="" />
  <div>
	<h3>Ahmad Shadeed</h3>
	<p>You: Thanks, sounds good! . 8hr</p>
	<img class="card__seen" src="assets/shadeed.jpg" alt="" />
  </div>
</div>

.card {
  position: relative;
  display: flex; /* [1] */
  align-items: center; /* [2] */
  background-color: #fff;
  padding: 8px;
  border-radius: 7px;
  box-shadow: 0 3px 15px 0 rgba(0, 0, 0, 0.05);
}

.card h3 {
  font-size: 15px;
}

.card p {
  font-size: 13px;
  color: #65676b;
}

.card__image {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  margin-right: 12px;
}

.card__seen {
  position: absolute; /* [3] */
  right: 16px;
  top: 50%;
  transform: translateY(-50%);
  width: 16px;
  height: 16px;
  border-radius: 50%;
}

I have highlighted a few lines, I want to explain them:

  1. We used flexbox because we have a horizontal design and flexbox works well for it.
  2. Child elements need to be vertically centered.
  3. The icon is absolutely positioned, and it is also vertically centered.

Breaking the component

There is nothing wrong with this, but the component doesn’t scale, so I’ll show you another option:

The blue icon on the right means that a new message has arrived, which I have not yet opened. The green color on the avatar indicates that the user is currently online.

Please note that we have two new icons. What’s the best way to add them to the card? If you refer to the CSS I wrote for the very first component, you will see that there is a class .card_seen for small user avatars on the right. In this variant .card_seen should be replaced with a blue icon. With the HTML and CSS already written, without changing the HTML, it is impossible to write this.

For clarity, I will clarify that the option I am showing is only the surface of possibilities. The component has many variations and use cases.

All variations

Below I show all the variations of the component. I tried very hard to describe them all (yes, I drew it all by hand).

But this is not enough: we must take into account the styles of the dark theme.

In this article, we’ll work together to find out how best to write robust components that don’t break after changes and in a wide variety of locations. Let’s get started!

Intervals

First of all, I carefully studied the spacing of each UI element while working with the illustrations below. You can see how many spacing and sizes this simple component has.

In designing and implementing a UI in HTML and CSS, one of the key things to consider is spacing. If you underestimate the spacing, you may need to change the UI itself later.

Component areas

To write a decent component, you first need to think carefully about your markup. We have two component areas with several variations: an avatar and a content area.

Avatar

To work on the HTML avatar, you first need to understand its states. Here are the options:

  • One avatar.
  • One avatar with an online status icon.
  • Multiple avatars for group chat.
  • Multiple avatars with online status icon.

Given the HTML below, we want to make sure that .card__avatar works with all avatar options above.

<div class="card">
  <div class="card__avatar"></div>
  <div class="card__content">
	<!-- Name, message, badge.. -->
  </div>
</div>

One avatar

Let’s zoom in on the HTML and focus on the first option, which is one avatar. The avatar must have a border (or inner shadow) to make it look like a circle, even if it’s completely white.

In CSS it is impossible to apply internal box-shadow to element img… We have two options:

  • Additional div with transparent border
  • You can write svg

The purpose of the inner border is to show the outline around the avatar in cases where we have:

  • Full white light theme avatar;
  • A completely black dark theme avatar.

Without an inner border, an all white avatar will blend in with the parent background. This will happen with dark theme too. This is what is drawn with and without an inner border.

div for the inner border

There is an additional element in this solution (here it is div) is absolutely positioned above the image with opacity 0.1

<div class="card__avatar">
  <img src="assets/shadeed.jpg" alt="" />
  <div class="border"></div>
</div>

.card__avatar {
  position: relative;
}

.card__avatar img {
  width: 56px;
  height: 56px;
  border-radius: 50%;
}

.border {
  position: absolute;
  width: 56px;
  height: 56px;
  border: 2px solid #000;
  border-radius: 50%;
  opacity: 0.1;
}

This solution works, but it has limitations, which I will discuss shortly.

We work with svg

In this solution, we use the element svg… Here’s the idea: use a round mask for the avatar and an element for the inner border circle… SVG is great for this.

<svg role="none" style="height: 56px; width: 56px">
  <mask id="circle">
	<circle cx="28" cy="28" fill="white" r="28"></circle>
  </mask>
  <g mask="url(#circle)">
	<image
	  x="0"
	  y="0"
	  height="100%"
	  preserveAspectRatio="xMidYMid slice"
	  width="100%"
	  xlink:href="/assets/shadeed.jpg"
	  style="height: 56px; width: 56px"
	></image>
	<circle class="border" cx="28" cy="28" r="28"></circle>
  </g>
</svg>

.border {
  stroke-width: 3;
  stroke: rgba(0, 0, 0, 0.1);
  fill: none;
}

Both solutions are good when building one avatar just as an example. Things will get interesting when we add the online status icon element.

The only avatar with an online status icon

In light theme mode, the green icon is surrounded by a white border. But in dark mode, the icon must be cut from the avatar itself. In other words, you need to apply a mask.

How to do it? It turns out that if you use the SVG solution for that single avatar, the problem is easily solved with an SVG mask.


<svg role="none" style="height: 56px; width: 56px">
  <mask id="circle">
	<!-- [1] -->
	<circle cx="28" cy="28" fill="white" r="28"></circle>
	<!-- [2] -->
	<circle cx="48" cy="48" fill="black" r="7"></circle>
  </mask>
  <!-- [3] -->
  <g mask="url(#circle)">
	<image
	  x="0"
	  y="0"
	  height="100%"
	  preserveAspectRatio="xMidYMid slice"
	  width="100%"
	  xlink:href="/assets/shadeed.jpg"
	  style="height: 56px; width: 56px"
	></image>
	<circle class="border" cx="28" cy="28" r="28"></circle>
  </g>
</svg>

Let me explain this code:

  1. The circle disguises the avatar.
  2. A small circle is cut out in the lower right corner of the avatar.
  3. The group that contains circle and image for a transparent inner border.

Here is a picture that explains how multiple circles work as a mask. This is some kind of magic, right?

This is what an HTML avatar looks like with an online status icon.

<div class="card__avatar">
  <svg role="none" style="height: 56px; width: 56px">
	<mask id="circle">
	  <circle cx="28" cy="28" fill="white" r="28"></circle>
	  <circle cx="48" cy="48" fill="black" r="7"></circle>
	</mask>
	<g mask="url(#circle)">
	  <image
		x="0"
		y="0"
		height="100%"
		preserveAspectRatio="xMidYMid slice"
		width="100%"
		xlink:href="/assets/shadeed.jpg"
		style="height: 56px; width: 56px"
	  ></image>
	  <circle class="border" cx="28" cy="28" r="28"></circle>
	</g>
  </svg>
  <div class="badge"></div>
</div>

.card__avatar {
  position: relative;
  display: flex;
  margin-right: 12px;
}

.badge {
  position: absolute;
  right: 3px;
  bottom: 3px;
  width: 10px;
  height: 10px;
  background: #5ad539;
  border-radius: 50%;
}

When a component like this needs to adapt to different themes, it is highly recommended to store mutable colors in CSS variables.

:root {
  --primary-text: #050505;
  --secondary-text: #65676b;
  --bg-color: #fff;
}

html.is-dark {
  --primary-text: #e4e6eb;
  --secondary-text: #b0b3b8;
  --bg-color: #242526;
}

.card {
  background-color: var(--bg-color);
}

.card__title {
  color: var(--primary-text);
}

.card__subtitle {
  color: var(--secondary-text);
}

Multiple avatars in a group chat

In the case of a chat with several people, there are two avatars in the avatar zone, each will be located in the upper right and lower left corners, respectively. To align one or more avatars, you need to set a fixed size for their parent.

.card__avatar {
  width: 56px;
  height: 56px;
}

This option requires you to change the markup like this:

<div class="card__avatar card__avatar--multiple">
  <svg
	class="avatar avatar-1"
	role="none"
	style="height: 36px; width: 36px"
  ></svg>
  <svg
	class="avatar avatar-2"
	role="none"
	style="height: 36px; width: 36px"
  ></svg>
  <div class="badge"></div>
</div>

.card__avatar--multiple {
  position: relative;
  width: 56px;
  height: 56px;
}

.card__avatar--multiple .avatar {
  position: absolute;
}

.card__avatar--multiple .avatar-1 {
  right: 0;
  top: 0;
}

.card__avatar--multiple .avatar-2 {
  left: 0;
  bottom: 0;
}

.card__avatar--multiple .badge {
  right: 6px;
  bottom: 6px;
}

Content

In this area, the user will see the name of the person they are communicating with, as well as the text of the message or action.

I can imagine markup split in two, one for text (name, message, or action), and one for the indicator on the right (new message, seen, muted, sent).

First part

Let’s take a close look at the markup for the content area.

<div class="card__content">
  <div class="card__content__start">
	<h3>Ahmad Shadeed</h3>
	<div class="row">
	  <p>You: Thanks, sounds good. What about doing a webinar, too?</p>
	  <span class="separator">.</span>
	  <time>8hr</time>
	</div>
  </div>
  <div class="card__content__end">
	<!-- The indicator (new message, seen, muted, sent) -->
  </div>
</div>

.card__content {
  display: flex;
  flex: 1;
}

.card__content__start {
  display: flex;
  flex: 1;
}

.card__content__start .row {
  display: flex;
  align-items: center;
}

.card__content__end {
  display: flex;
  justify-content: center;
  align-items: center;
  margin-left: 12px;
}

.separator {
  margin-left: 4px;
  margin-right: 4px;
}

With the code above, the content area looks like below (this is a screenshot from Firefox).

Messages or name can be very long. It is important to take this into account right away. Let’s first look at the flow as you like approach.

In the picture above, the content of the second card is split into multiple lines. This is ugly for such a component. Here’s what to do to keep the lines from breaking:

  • Install min-width: 0 for flex children. What for? I’ll explain later.
  • Trim text via properties overflow, white-spaceand text-overflow… I’m already wrote in more detail processing of short and long content.

I added the code below to the name and paragraph:

.card__content__start h3,
.card__content__start p {
  overflow-x: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

But this code doesn’t automatically solve our problem when we use flexbox. Note what the above CSS does:

And here’s the reason: flex items don’t shrink beyond the minimum content size. To fix this problem, you need to install min-width: 0 in .card__content and card__content__start

Second part

Each type of message has a certain indicator, we must consider indicators of all types.

In this part, we will focus on .card__content__end and on the content within it.

<div class="card__content">
  <div class="card__content__start">
	<!-- The name and message -->
  </div>
  <div class="card__content__end">
	<!-- The indicator (new message, seen, muted, sent) -->
  </div>
</div>

Element .card__content__end there should be no styles such as color or font size, this element will only serve as the parent of a specific component.

A new message

I looked at how Facebook works with the new message indicator; it turned out to be a button labeled “Mark as read”.

<div role="button" aria-label="Mark as read" tabindex="0"></div>

Not sure why the Facebook team chose div, but not button… No attributes needed with inline button role, aria-label and tabindex… They are all built into the button.

The only avatar around the post

This avatar is no different from the user’s avatar. It uses the element svg with attribute aria-labelwhich shows the username.

<svg aria-label="Ahmad Shadeed" role="img">
  <!-- Mask and image -->
</svg>

Several avatars near the post

To be honest, this is my favorite option. I really love how the Facebook team did it.

Can you see the border between the two avatars? At first it seems like it’s the CSS border for the first avatar. If you thought so, I’m sorry, but you were wrong, as I was at the beginning.

Border made with SVG mask. Yes, you heard right!

The mask works like this:

Incredible. This is where I like to use SVG.

Right to left content

When the layout is LTR (left to right) and the message text is written in Arabic, the direction of the text must also be RTL (right to left).

Element .card__content__start Is a flex container, so the children will automatically flip based on the property value direction on a component or root element. This behavior can be added dynamically, depending on the language of the text.

<div class="card__content">
  <div class="card__content__start" style="direction: rtl"></div>
  <div class="card__content__end"></div>
</div>

Flip the component

If the user has chosen the direction from right to left (for example, Arabic) for the entire user interface, then the component must be flipped.

Items are laid out using flexbox, so you only need to flip the margins.

/* LTR */
.card__content__end {
  margin-left: 12px;
}

/* LTR */
.card__content__end {
  margin-right: 12px;
}

Availability


Keyboard operation

A product that works with billions of users must be available to everyone. As for the component from this article, I tested it in Chrome and Firefox and noticed problems like this:

  • Focus styles work fine in Chrome, but there is no visual cue in Firefox.
  • The focus on the action menu that appears on hover is achievable in Firefox and I cannot access it from the keyboard in Chrome.

And to give you more context, let me remind you that when you hover over, an action menu appears. But I actively use the keyboard and expect to be able to navigate the menu through it.

Unfortunately, in Chrome, I was unable to reach the action menu using the keyboard.

List of cards

Several ARIA roles are listed in the card list. This list contains lines and looks like a grid. Each line can have one or more cells.

<div role="grid">
  <div role="row">
	<div role="gridcell">
	  <a href="#">
		<!-- The component lives here -->
	  </a>
	</div>
  </div>
  <div role="row">
	<div role="gridcell">
	  <a href="#">
		<!-- The component lives here -->
	  </a>
	</div>
  </div>
</div>

Multiple avatars

For a group chat, there are several view indicator avatars. Here roles ARIA arrange cells in a row.

<div role="grid">
  <div role="row">
	<!-- 1st avatar -->
	<div role="cell"></div>
	<!-- 2nd avatar -->
	<div role="cell"></div>
  </div>
</div>

Look at demo from the Codepen website. All options are not here, I just tested them.

Conclusion

In this article, I would like to emphasize that the simplest component takes a lot of work. By the way, all the explanations above were only about HTML and CSS. What about JavaScript? This is another story.

I enjoyed the work while writing this article and will definitely work on something like this in the future. I’m also glad to inform you that I have written an ebook on CSS Debugging. If you are interested, click on the link debuggingcss.com and watch the book for free. Did you like my content? Then you can pay for my coffee… Thank you very much!



0 Comments

Leave a Reply