It’s 10:00 AM and a designer just created a new icon for Yelp. At 10:05 AM iOS has received a newly versioned CocoaPod containing 1x, 2x, and 3x PNGs, Android has received an AAR Library containing mdpi, hdpi, xhdpi, and xxhdpi WebPs, and the Web CDN has a new SVG and PNG spritesheet, code to manage the PNG fallback for our IE8 users, and a new Python package to access the icon in a template.

The workflow wasn’t always this streamlined and was only made possible through a cross-team collaboration with Android, iOS, Web, and Design at Yelp. With a product as large as ours, (with around 120 metaphors) making sure that new icons with varying colors and sizes are up-to-date and look correct in all resolutions across all platforms is no easy feat. Yelp is divided into several different product teams with a mix of developers from each platform, so updating icons requires collaboration with all teams. This story is about taking the human process of developing, sharing, and requesting new icons for different platforms and automating it. We went from passing images through emails, Dropbox, and even Jira to automatically delivering the icons to the developers right when they needed them.

The process started with our design team. Their effort redefined the icon design standard, creating a unified set that looks great across all platforms. A few key choices in the beginning set the project up for success: using the SVG format, designing for pixel density, and using a bounding box. Using SVGs as the base image format allows us to scale an icon without losing quality and effortlessly colorize the icons via code. Since the Yelp product is supported on a variety of devices, each image had to define icon size in device independent pixels or in our case defining 2x, 3x, etc. densities for images. Finally, we needed to ensure that icon spacing was consistent so that developers could easily align the icons in whatever context they needed to use them. By creating a box that bounds each icon we were able to balance the visual weights individually and have the confidence that icons would look great on all platforms.

Once the design specifications were finalized, we turned to creating a build/delivery system. The system had to support conversion and scaling of icons and publish packages that could be consumed by all platforms.

Multi-Platform Build System

In defining an SVG-first icon system, the first task was to convert icons to different image formats in a fluid, programmatic way.

Icon Conversion and Scaling

Each platform has different image requirements. The Web needs to support SVGs and PNGs, Android requires WebPs, and iOS requires PNGs. Additionally, both Android and iOS require icons at different pixel densities. To automate creating these assets, we used build systems and CI. At Yelp, our traditional build systems are supported using make (this does not include platform specific build system such as gradle) with Jenkins running on Linux and OSX as our CI service.

All image manipulation is done in Python and one node package (SVGO). For image conversion from SVG to PNG and scaling we used CairoSVG. PNG stitching and WebP conversion is handled with PIL. For SVGs, we optimize with SVGO and clean and manipulate with lxml.

Pipeline

When a designer commits a new SVG icon to our git repository, we kick off the jenkins build pipeline. The first stage of the pipeline is to run unit tests for the repository and some checks on the icons themselves. We ensure all SVGs are valid and and that the naming of the icon follows our pre-defined naming scheme. Finally, we kick off the builds for each platform:

Web

There is only one build stage for Web. We optimize SVGs for the SVG spritesheet while we simultaneously convert the SVGs to PNGs. After we optimize the SVGs, we remove the fill attributes and namespace tags using lxml. These generated images are then used to create both the SVG and PNG spritesheets. Any JS or CSS needed for the PNG spritesheet is generated at the time the spritesheet is built with mako. Finally, we upload all of our assets to Amazon S3 (our backing store for our CDN) and upload a new python package to our internal PYPI server that contains all of the asset metadata needed for grabbing the assets from the template.

Android and iOS

We have increasingly started to use Docker as a encapsulation method for tools that require system dependencies. Specifically, to convert an image to the WebP format using PIL, we need to have libwebp installed on the system. We could install this on all of our systems that we would run this build, but this dependency is likely only going to be specific to our build, so it made sense to use Docker.

FROM    our-base-image

RUN apt-get update \
    && DEBIAN_FRONTEND=noninteractive apt-get -y --no-install-recommends install \
        build-essential \
        libwebp-dev \
        libjpeg-dev \
        libffi6 \
        npm \
        python3-dev \
        python3-pip \
        zlib1g \
    # keep the image small; clean up APT when done
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

RUN pip3 install pillow
RUN mkdir -p code
ADD . code
WORKDIR code
ENTRYPOINT ["python3.4", "convert_png_to_webp.py"]

After generating the icons in specific formats and sizes we use templates to generate both the Cocoa Pod and the AAR file.

Web

Our Front-End web developer defined our SVG-first spriting system with a minimal PNG fallback for any browsers that don’t support SVG. We developed two major components: the template helper and the fallback.

Template Helper

Creating this new infrastructure for developers doesn’t do us any good if it isn’t easy to use. We use a template helper to abstract the details away, so that they can focus on what’s important, the product. At Yelp we use the cheetah templating language, but the general concept applies to anyone. We defined a simple helper function that gives complete control over the icon. For example $icon(‘14x14_arrow’) creates the following markup:

<span class="icon icon--14-arrow" style="width: 14px; height: 14px;" aria-hidden="true">
   <!-- :before pseudo-element for the fallback generated with CSS -->
   <svg class="icon_svg">
      <use xlink:href="#14x14_arrow" />
   </svg>
</span>

This allows the size to be inlined and the icon is hidden for screen readers when no title parameter is passed in.

Template Helper: colors

Yelp has a standard colors palette where color, hover (+ focus) and active states have an alias. When the color is omitted we use the default. We use currentColor, an additional color in our toolkit, to fill the SVG icon with the CSS color value of the .icon element itself (or its parent’s) so when you hover over a link both the link and the icon color will change accordingly. The template supports colors in the following ways:

$icon('14x14_foo', color='inactive') ## .icon .icon--inactive
$icon('14x14_foo', color='inactive', color_hover='error') ## .icon .icon--inactive .icon--hover-error
$icon('14x14_foo', color='inactive', color_active='error') ## .icon .icon--inactive .icon--active-error
$icon('14x14_foo', color='currentColor') ## .icon .icon--currentColor

One important thing to note is the color (classname or inline) is set on the span element instead of the SVG element. This is so that we can also style the fallback (the :before pseudo element). This works well because the actual SVG fill property is set to inherit and the element inherits the fill color from the parent. For example:

<span style="icon--neutralgrey"> <!-- .icon--neutralgrey { fill: #999; }-->
   :before <!--.icon--neutralgrey:before { left: -50%; }-->
  <svg> <!--fill: inherit;-->
   ...
  </svg>
</span>

Template Helper: state

As seen above, the icon color can be changed on hover (focus) and be marked as active via the color_hover and color_active params. When those options are defined a modifier class with the –state- prefix is added to the icon component i.e .icon--hover-neutralgrey or .icon--active-neutralgrey. These are in fact like promises that get resolved when the element is hovered, focused or the icon is marked as active by adding the .is-active class, i.e. .icon--hover-neutralgrey:hover, .icon--hover-neutralgrey:focus { fill: #999; }, .icon--active-neutralgrey.is-active { fill: #999; }. Sometimes though the icon is part of other components and its color has to change only when the the parent component or element state changes.

<a href="https://yelp.com">
   <Icon />
    Yelp
</a>

We made our icons system so that state classes can be manually set on any ancestor to delegate the control of the icon state.

<a href="https://yelp.com" class="icon--hover-neutralgrey icon--active-neutralgrey">
   <Icon />
    Yelp
</a>

Will change the Icon color when the link is hovered or has the is-active class.

[class*="icon--active-"].is-active .icon,
[class*="icon--hover-"]:hover .icon,
[class*="icon--hover-"]:focus .icon {
    fill: inherit;
}

.icon--active-neutralgrey.is-active { fill: #999; }

Finally since we are working with SVG and CSS the icon can inherit the parent’s color by using the currentColor color.

<a href="https://yelp.com">
   <Icon class=”icon--currentColor” />
    Yelp
</a>

The icon inherits the color from the link. If the link color changes (on hover for example) the icon color will change accordingly.

Fallback

We decided to support black and white PNG icons as a fallback. This fallback needed to be simple but functional because it provides support to a small portion of our users. Furthermore we wanted to be able to eventually move to an SVG only framework. Mark Hinchliffe’s awesome article SVG icons are easy but the fallbacks aren’t provided some great inspiration. We started there and built a simplified version of his solution that doesn’t care about:

  • many colors – remember we have only two: black and white
  • icon sizes and scaling – we just have a spritesheet with icon of “any” size.

Here the .icon element is our canvas and the :before pseudo-element is an element with the PNG spritesheet as background-image. The size of the pseudo-element reflects the PNG one.

The Y-axis is for the symbol (icon) and X-axis is for the color.

In our case we have two columns of icons, one for the black and another for the white so the left position of the pseudo-element will be either 0 or -($png_width / 2)

.icon--14-arrow:before { top: -64px; } // symbol
.icon:before { left: 0; } // default color
.icon--fallback-inverted { left: -($png_width / 2)px; } // inverted color

iOS and Android

iOS and Android were a little more straightforward than Web. Once we had the images converted to their respective formats at different pixel densities, we just needed to figure out how we were going to color them. Due to improvements on native devices, with Android releasing tinting capability as of api version 21 and iOS tinting as of iOS 7, we decided that we could tint icons at runtime on both mobile platforms. By tinting them at runtime, we reduce the number of icons in each bundle and avoid duplication of icons (a common error we faced). Furthermore, we leverage the use of resource shrinking on Android to further shrink the asset bundle size. Finally, we package them up into consumable items for both platforms. For iOS this meant that we published a CocoaPod and for Android this meant that we would generate an AAR resource.

And that is all there is to it! Want to see the Finished Product? Check out yelp.com/styleguide/icons.

Want to Join Yelp as a Full-Stack Engineer?

Do you love engineering processes? Do you understand the web ecosystem? Can you tackle full-stack problems on a larger scale with ease? If so, our core web team is looking for someone like you!

View Job

Back to blog