For Omnichannel Retailers, Hedgehogs Beat Foxes

Focus and passion are the keys to long-term success for omnichannel and ecommerce retailers.

In his 2001 book “Good to Great,” author and researcher Jim Collins sought to explain “why some companies make the leap [from good to great] and others don’t.” His answer plays out over about 300 pages, and it includes the Hedgehog Concept.

“In the famous essay ‘The Hedgehog and the Fox,’ Isaiah Berlin divided the world into hedgehogs and foxes, based upon an ancient Greek parable: ‘The fox knows many things, but the hedgehog knows one big thing,” explained Collins in the opening of chapter six of his book.

“The fox is a cunning creature, able to devise a myriad of complex strategies for sneak attacks upon the hedgehog. Day in and day out, the fox circles around the hedgehog’s den, waiting for the perfect moment to pounce. Fast, sleek, beautiful, fleet of foot, and crafty — the fox looks like the sure winner. The hedgehog, on the other hand, is a dowdier creature, looking like a genetic mix-up between a porcupine and a small armadillo. He waddles along, going about his simple day, searching for lunch and taking care of his home,” wrote Collins.

“The fox waits in cunning silence at the juncture in the trail. The hedgehog, minding his own business, wanders right into the path of the fox,” Collins continued. “‘Aha, I’ve got you now!’ thinks the fox. He leaps out, bounding across the ground, lightning-fast. The little hedgehog, sensing danger, looks up and thinks, ‘Here we go again. Will he ever learn? Rolling up into a perfect little ball, the hedgehog becomes a sphere of sharp spikes, pointing outward in all directions. The fox, bounding toward his prey, sees the hedgehog defense and calls off the attack.”

A Retail Hedgehog?

Now imagine an austere conference room at a mid-sized retail company. The business’s leaders have gathered to discuss how Covid-19 has impacted the company, what the upcoming election might do to its business, and what they must do next.

There are several discussions. Ideas fly about. Some want to mimic the competition. Others believe new technology is the answer. Still others suggest hiring or firing workers. Each person argues for her position, giving good reasons for their approach until no one is sure what the company should do.

The problem in this scenario may be that there are too many foxes and not enough hedgehogs.

“Foxes,” Collins wrote, “pursue many ends at the same time and see the world in all its complexity…Hedgehogs, on the other hand, simplify a complex world into a single organizing idea, a basic principle or concept that unifies and guides everything. It doesn’t matter how complex the world, a hedgehog reduces all challenges and dilemmas to simple…hedgehog ideas.”

According to Collins, “Those who built the good-to-great companies were, to one degree or another, hedgehogs. They used their hedgehog nature to drive toward what became known as a Hedgehog Concept for their companies. Those who led the comparison companies [which failed to become great] tended to be foxes, never gaining the clarifying advantage of a Hedgehog Concept, being instead scattered, diffused, and inconsistent.”

As merchants struggle to deal with the many and various challenges 2020 has brought, it may make sense to try to simplify the complex.

3 Dimensions

In “Good to Great,” Collins does not tell readers what their particular Hedgehog Concept should be but, instead, suggests looking at the overlap of three circles or dimensions.

Venn diagram of the Hedgehog concept

Collins creates a Venn diagram for the Hedgehog Concept. The simple, clarifying hedgehog idea is at the intersection of the three circles or dimensions. Image: Jim Collins.

These dimensions are questions a company can ask of itself to develop a simple and clear business strategy.

  • What can you be the best at?
  • What makes you money? Or “What drives your economic engine?”
  • What are you deeply passionate about?

Context is important here. Collins is speaking directly to business leaders who want to build great companies and outperform the competition.

A company happy with its share of the market doesn’t necessarily need to focus on what it can be the best at. But an omnichannel or ecommerce retailer aspiring for greatness can only really focus on what it can do better than nearly any other business.

Competence is not enough in itself. If the business is not passionate, it will not sustain the energy and drive needed. Similarly, if the thing a business is passionate about and good at does not generate profit, it will not be great.

When a business can identify its Hedgehog Concept, it has one of the ingredients of a future, great company. It has a simple and understandable strategy that will guide it through hard decisions. This is true even in a year like 2020, even in the middle of a global pandemic, and even in the face of political uncertainty.

‘Gated’ Product Launch Leads to $250,000 in 1-day Sales

Jordan West launched Little & Lively, a direct-to-consumer children’s apparel company, with his wife, Carmen, six years ago in Canada. The business has grown to mid-seven-figures in revenue selling through its website, mostly. He’s a master at creating demand for his products before they are available for purchase.

“We had a recent launch where, in a single day, we did about $250,000 in sales without discounts or ads,” he told me. “We did what’s called a gated launch. We password protected the site for 24 hours. This drove the anticipation. Then we gave our VIP group and our SMS list two hours early access before the general public.”

I spoke with West recently about his business (which includes a women’s apparel brand and a marketing agency), the benefits of local manufacturing, and the psychological impact of reaching a life-long income goal at a relatively young age.

The entire audio version of our conversation is embedded below, followed by a transcript, which has been edited for clarity and length.

Eric Bandholz: Tell us about yourself.

Jordan West: I’m Canadian. My first business was a Taco Del Mar restaurant, which is a chain that has gone from roughly 200 stores to about 50 since I was involved in the franchise. I lost a lot of money. But I learned marketing, which was an interest of mine.

My wife and I then started a baby clothing company called Little & Lively. That was about six years ago. Fast forward to 2020, and we’ve grown to mid-seven-figures in revenue. I also run Mindful Marketing, an agency that helps people in the apparel vertical, mostly with performance marketing.

Bandholz: A mid-seven-figure business is impressive. Is it bootstrapped?

West: Yes. We’re completely bootstrapped. My wife and I are 50-50 owners. Bootstrapping becomes difficult as the company grows. But I have a wonderful CFO with a great five-year plan that’s modest and anticipates cash crunches, to plan.

Bandholz: I’ve always wanted to sell apparel. But the margins are tight, aren’t they?

West: I’ve heard that from a lot of people. It’s an outward view of apparel. We make everything in Canada. The only thing that we import, from China, is fabric because we don’t have mills in our area. All of our products are manufactured in our hometown here in British Columbia.

We’re about as close to vertically integrated as you can get with our manufacturer. I would love to purchase the company, but that’s not in the cards now. Our retail prices are roughly double our cost. Those are fine margins for us to acquire customers, especially as a bootstrap business.

In addition to our website, we’re in about 150 stores across Canada and about 30 to 40 in the United States, though that’s not an area of focus now. We’re trying to be the premier children’s clothing brand in Canada.

Bandholz: Are the online shopping habits of Canadians the same as in the U.S.?

West: Our agency has many clients in Canada and the States. The buyers are similar, except people in Canada don’t like to be pushed. An example is putting time limits on coupon codes or discount offers. Canadians do not respond well to that.

Bandholz: Your product offerings are extensive. Having a local manufacturer helps with lower order quantities, I would imagine.

West: Yes. Again, we’re vertically integrated with our manufacturer. We’re that company’s biggest customer. At the beginning of a season, we’ll make our best guess as to which sizes and styles will move the fastest. We have about a two-week turnaround for fairly large inventory orders. Our manufacturer knows that that’s the way that we operate.

We’ll buy enough fabric, but we don’t exactly know how to use that fabric until we do a product launch. We had a recent launch where, in a single day, we did about $250,000 in sales without discounts or ads. That really gave us a good idea of what people wanted.

So the more SKUs that we have, to more complicated it becomes. But it’s much easier with a local manufacturer.

Bandholz: I’ve never had a quarter-million-dollar day.

West: It was wild. We do our own fulfillment. It wasn’t just a single occurrence. The next day was $50,000, and the day after that was $40,000. We felt like we graduated to a different size business with that launch.

Generally, we’re able to do next day fulfillment. But took about a month to catch up in this case.

Bandholz: How did you generate that much hype?

West: A lot of psychology went into this launch. So we did what’s called a gated launch. We have our Little & Lively VIP customer group, and we also have an SMS group. Those two groups receive advance notifications for sales. We password protected the site for 24 hours to get ready for the launch because it involved so many products, and we wanted it to be perfect.

This drove the anticipation. Then we gave our VIP group and our SMS list two hours early access before the general public. During those two hours, we did about $120,000 in sales without discounts.

We use Facebook Ads to gain subscribers, but not for the launch itself. I’m a huge believer in the customer journey. You’re not going to have long-term customers if you just sell to them right off the bat. I love moving people farther down that funnel where they become advocates for us. That’s what we saw on this launch. We also experienced the idea of scarcity because we sold out of a lot of items.

Bandholz: You’ve mentioned your VIP group. This is a Facebook group you’ve created. How do your entice folks to participate?

West: We’ve got a very manual follow-up process. We’ll send the classic Klaviyo email sequence. But we also send a personalized video to every new customer that orders, letting them know about our VIP group. I’ve created an entire system with a virtual assistant who sends these personalized videos, saying the person’s name, saying what they ordered, and then asking them if they’d like to join the VIP group.

We use a wonderful app called Bonjoro for this. It’s an iPhone app with email integration. Our virtual assistant sends hundreds of these at a time. She will set it up. She can see all of the information about the person right there. And then as soon as she presses send, it’ll send the video right to them. They have to click, and then it goes onto a page. It’s similar to Loom, the video message app, but with a B2C focus.

Bandholz: How did you find the right virtual assistant for this? Does it impact customer service?

West: Bonjoro has its own chat feature. So if a shopper responds in that manner, we forward it to our customer service team. We’ve also trained our virtual assistant to handle quick responses. We tried using our in-house staff to do the videos. But they weren’t motivated. It would take them about 10 minutes per video. Our virtual assistant can do about 40 per hour. She has a really good process.

I auditioned many virtual assistants for this task. We gave them a script, and they sent in videos. I was super happy with the results. And the one that’s doing it now is Andie. She’s incredible. She’s in the Philippines.

Bandholz: And what is the typical compensation for a virtual assistant?

West: We pay about $10 (USD) an hour. We’re not trying to get cheap labor. We’re after good people that can do tasks that we can’t source in Canada.

Bandholz: What is your goal for the business? Five years, 10 years?

West: We can catapult off our two core brands — Little & Lively for kids and Kindred Clothing for women. I’m really working through this now. What’s my purpose? My whole goal since my early 20s was to make a lot of money. That one day, where we booked $250,000 in revenue, was actually one of those bad psychological days. I thought, “Oh man, this is repeatable. I can do this.” So I’m realizing my goal, and I’m asking, “What’s my life about?” That’s the crisis I’m in now, yet I want to grow these businesses.

We’ve got big revenue goals. But I’d like to see more of our staff assume additional responsibility, to have the potential for the kind of life that my wife and I have. That would make me happy. We’re looking at a few acquisitions now, as well. But I want to make the acquisition if I can put folks in place to empower them and improve their life.

Bandholz: You’ve touched on an important and vital topic for me. I’ve thought a lot about it. Here’s a question for you. If I could give you $50 million and the only stipulation is that you can no longer work — no creating or getting stuff done. You could only live a lifestyle of pure consumption. Would you do it?

West: No. I would not. I would be so bored. Having a purpose is what gives life some meaning.

Bandholz: How can people get that cool welcome video? How can they follow you and reach out?

West: They can order the welcome video at Little & Lively. I spend most of my social time on LinkedIn at jordan-west-marketer. My agency is Mindful Marketing.

A Monthly Update With New Guides And Community Resources

Whether you’re motivated to get started with creating artworks through code or instead find yourself spending too much time getting upset over ugly website policy disclosure pages, we always like to have your back. Here at Smashing, we are committed to nourishing curiosity on what works and what doesn’t, helping folks improve their skills and workflows, and last but not least, finessing their work-life balance.

The best way to get better together is by learning from each other, and what better way is there than to promote new ideas and the way forward for the web industry. A quick peek at our ever-growing guides will show you that we’re dedicated in bringing together a variety of topics that will help us all explore and learn new things.

You can always follow us on Twitter, Facebook, LinkedIn and subscribe to our RSS feed, but it’s nice to have an overview of the most important things in one place.

So, What’s New?

Well, things never get boring at Smashing, that’s for sure! From online meetings with team members and brainstorming sessions to online Smashing workshops and live events — each day is super exciting and brings new challenges. However, there is one thing that’s making the entire team bounce off the walls: our brand new book! “TypeScript In 50 Lessons” is a book that breaks down the quirks of TypeScript into short, manageable lessons.

If you’re spending a huge amount of time programming and want to be more productive, this is the book for you; but it’s also for developers who have already dipped their toes into TypeScript and now want to get their feet wet. From type systems to defining complex JavaScript scenarios, we’ve teamed up with Stefan Baumgartner to help you lose so much less sweat and tears in your projects!

Not convinced? You’re welcome to jump to the table of contents and even download a free sample (2.3 MB, PDF).

Recommended Reading on SmashingMag:

Online Events: See You There?

And so the countdown to the year 2021 begins! We have one more live event coming up on November 10th–11th: SmashingConf San Francisco. The schedule and tickets are online for you to take a look at. We’ll be sure to have a great celebration for our final event of 2020!

As for our ongoing online workshops, we still have a good number of spots available for you! We hope you’ll find at least one workshop that fits your projects and career path, and also allows us to help make life easier for you:

Oct. 28 – Oct. 29Designing for Emotion MasterclassAarron WalterDesign & UX
Nov. 6 – Nov. 20Front-End Accessibility MasterclassMarcy SuttonFront-end
Nov. 18 – Nov. 26Designing Websites That ConvertPaul BoagDesign & UX
Dec. 1 – Dec. 15Smart Interface Design Patterns, 2020 EditionVitaly FriedmanDesign & UX
Dec. 3 – Dec. 17Building A Design System With CSSAndy BellFront-end
Jan. 5 – Jan. 19Build, Ship and Extend GraphQL APIs from ScratchChristian NwambaFront-end
Jan. 19 – Jan. 27Form Design MasterclassAdam SilverDesign & UX

Smashing Podcast: Tune In And Get Inspired

Every second Tuesday, Drew McLellan talks to design and development experts about their work on the web. You can subscribe via your favorite app to get new episodes as soon as they’re ready.

Pssst. By the way, is there a topic that you’d love to hear and learn more about? Or perhaps you or someone you know would like to talk about a web- and design-related topic that is dear to your hearts? We’d love to hear from you! Feel free to reach out to us on Twitter and we’ll do our best to get back to you as soon as possible.

1. What Is Art Direction?2. What’s So Great About Freelancing?
3. What Are Design Tokens?4. What Are Inclusive Components?
5. What Are Variable Fonts?6. What Are Micro-Frontends?
7. What Is A Government Design System?8. What’s New In Microsoft Edge?
9. How Can I Work With UI Frameworks?10. What Is Ethical Design?
11. What Is Sourcebit?12. What Is Conversion Optimization?
13. What Is Online Privacy?14. How Can I Run Online Workshops?
15. How Can I Build An App In 10 Days?16. How Can I Optimize My Home Workspace?
17. What’s New In Drupal 9?18. How Can I Learn React?
19. What Is CUBE CSS?20. What Is Gatsby?
21. Are Modern Best Practices Bad For The Web?22. What Is Serverless?
23. What Is Next.js?24. What Is SVG Animation?
25. What Is RedwoodJS?26. What’s New In Vue 3.0?

The next podcast episode comes out next Tuesday (Oct. 20). We’ll be speaking with Stefan Baumgartner on all things Typescript! Stay tuned!

Smashing Newsletter: Best Picks

As we’ve now started sending out weekly editions of the Smashing Newsletter, we’ve been aiming for shorter and topic-specific issues. So far, we’ve sent out editions that focus on CSS, front-end accessibility, JavaScript, UX, and even one on little helpful tools and browser extensions. Of course, we like to add in a mix of other topics as well, just so that there’s something there for everyone! 😉

We love sharing all the cool things that we see folks doing across communities within the web industry, and we hope you’ll help spread the word! Here are just some of the projects that our subscribers found most interesting and valuable:

A Reliable Date Picker Library

There are dozens of date picker libraries out there, but it’s always great to have reliable workhorses that just work across browsers, don’t have heavy dependencies, are written reasonably well, and meet all major accessibility requirements.

Duet Date Picker is just like that. It’s an accessible, WCAG 2.1 compliant date picker that can be implemented and used across any JavaScript framework or no framework at all. It comes with built-in functionality that allows you to set a minimum and a maximum allowed date, and weighs around 10kb minified and Gzip’ed (this includes all styles and icons).

If you need an alternative, check out React Dates, a library released by Airbnb that’s optimized for internationalization, while also being accessible and mobile-friendly.

The Deck Of Brilliance

Do you sometimes find yourself facing a blank page with little or no inspiration whatsoever? Competing with creatives from every corner of the globe certainly doesn’t make it easy to generate brand new ideas, especially when you feel like they’ve all already been taken.

The Deck of Brilliance gives you 52 free tools that are bound to help you work up ideas in short periods of time. All you need to do is pick a tool one after the other, and be prepared to note down your ideas when they start rolling in. The more ideas you generate, the better the chances of nailing the big one!

Accessible Comics

When we use slightly more complex shapes and layouts on the web, sometimes it appears to be so much easier to just save it as a foreground or background image and serve different images to small and large screens. This holds true for complicated charts and graphs as well as good old comics with speaking bubbles, but what if we could re-imagine the experience altogether?

Comica11y is an experiment by Paul Spencer that aims to achieve an all-inclusive online comic reading experience. What if we could have different reading modes for the comic, e.g. with closed captions, proper focus management to navigate between panels, high-contrast mode, SVG color blindness filters, programatic bubbles, selectable and translatable text, LTR and RTL support, and even adjustable font sizes? A wonderful initiative that shows just how far we can take UI challenges and use the web to enhance the experience greatly.

A Free Resource Library For Product Designers

Nowadays, it doesn’t take us too long to research a topic we’re interested in learning more about. The answers are literally a click away. But do you remember the last time you had a peek at your browser bookmarks? Saving pages to read or view later is surely useful, but wouldn’t it be handy if we had our source of inspiration all ready and available in one place?

If you’re into product design, Design Notes is a library that will prove to be a true timesaver. It currently links to 334 resources that you can filter according to the topic you’re looking for: from resources related to user experience to design and prototyping tools. Anyone can contribute to the site, so feel free to share if you see anything missing!

The UX Of Banking

Every bank claims to offer the best overall banking experience, and why wouldn’t they, right? Well, in order to find out what the challenger banks did differently, UX specialist Peter Ramsey decided it was time to put a few of them to the test.

First, he opened 12 real bank accounts in the UK, and logged everything. What followed next were six detailed chapters of his user journey: opening an account, making his first payment, freezing his card, making international payments, open banking, and last but not least, customer support. A fantastic reference guide to help you craft better experiences indeed!

How To Make Data Tables Work Everywhere

One of the main difficulties when designing tables is that we need to find a way to display the entire table (or at least its structure completely) — be it on small screens or large screens. With navigation, we could find a way with tabs, accordions or even carousels, but tables are the beast of a different kind. So how can we tame them? In his article, “Design Better Data Tables”, Andrew Coyle highlights a couple of design patterns that we could apply (e.g. by allowing users to show/hide columns, or breaking rows into cards).

Molly Hellmuth’s “The Ultimate Guide To Designing Data Tables” provides a comprehensive set of best practices for designing tables, along with a free kit for tables. “How To Architect Complex Web Tables” introduces how to create a maintainable system for complex tables with resizing, filtering, truncation, and various states for each cell. Need to make a table work for mobile?

Also, “Designing a Complex Table for Mobile” shows how to transform a complex enterprise table into a manageable set of cards, filters and views on mobile by exploring it row-per-row or column-per-column. A great set of articles to dive in when dealing with those tricky tables!

Git Cheatsheets

Aw, Git! It’s always quite a challenge to remember all the right commands at just the right time, so having a few cheatsheets around can be quite useful. Git log features common ways to use the Git log to track what changed or search for commits. Git Branch cheatsheet shows how to list branches, create new ones, rename and switch branches, as well as delete them.

Need something slightly more advanced? Atlassian’s Git cheatsheet goes into more detail around Git basics, undoing changes, rewriting Git history, Git branches and remote repositories, and GitHub Cheat Sheet features shows how to inspect and compare, track path changes, share and update, ignore patterns, and add temporary commits.

Growing List Of Accessibility Resources

Chances are high that just when you need to look-up a solution or a technique, it will be quite difficult to find it quickly. Hannah Milan’s A11yresources provides a growing list of accessibility tools and resources, ranging from articles and browser extensions to newsletters and podcasts and videos.

A quite comprehensive list, well organized and structured — with pretty much everything you might need to resolve that accessibility issue quickly. For the bookmarks!

The State Of Design Systems And Prototyping 2020

It’s always interesting to explore what worked or failed for other professionals, so we can take some of the lessons and apply them to our context. The 2020 Design Systems Survey by Sparkbox highlights the results of the annual survey among organizations that have heavily invested in making design systems work well. The survey dives into common design system challenges and strategies to overcome them. The survey also dives into in-house design systems, design system maturity and the contributions of a design system to a positive culture within those organizations.

Conducted by the team behind Framer, The State of Prototyping 2020, provides a 31-pages PDF report exploring current challenges and benefits of prototyping. Only a third of respondents is able to provide a prototype within 12h. The common challenges are steep learning curves when starting to use a tool for the first time, and often the prototyping tool doesn’t fit exact requirements. A registration via email is required.

Not good enough? You can also explore the State of Email 2020, featuring current trends in email design and development, provided by Litmus.

Get Stuff Done With The Command Line

For some, the command line is a coder’s best friend, to others it might feel more like a daunting enemy. Fact is, there are a lot of awesome things you can do with terminal. Marcel Bischoff curates a list that is jam-packed with useful command line apps.

The list covers everything from automation and backup to encryption, productivity, version control, and much more that is bound to make a developer’s life easier. Even games are featured to sweeten up a long coding session. Your favorite command line app isn’t part of the list yet? You are highly encouraged to contribute to it, of course. One for the bookmarks.

Test Your Forms With Dummy Data

Testing forms can be a time-consuming process, but, luckily, it’s a task that can easily be automated. The browser extension Fake Filler was built to do just that.

Available for Chrome and Firefox, Fake Filler helps you test your forms quickly and efficiently by filling all form inputs with dummy data and randomly selecting radio buttons, dropdown menus, and checkboxes. The tool supports the maxlength property and ignores CAPTCHA, hidden, disabled, and readonly fields. A great little addition to any developer’s digital toolkit.

Make Team Calls Without Background Noises

The dog barking, kids screaming, or a construction site right outside your window — there are a lot of noises that interrupt a call or a virtual team meeting in these times where working from home has become the new normal. If you’re surrounded by a constant hustle and bustle and are afraid that it might distract your call partners, why not let AI ease the situation?

The AI-powered tool Krisp does exactly that: By clicking a button, it removes the background noise from you to other call participants when you speak, and the other way around. Whether you want to have noise-free virtual meetings or plan to record a podcast even though your apartment is located on a busy street, Krisp works in over 800 conferencing, voice messaging, streaming, and recording apps. Audio processing happens locally which means that no voice or audio will leave the device. Worth trying out.

More Smashing Stuff

In the past few years, we were very lucky to have worked together with some talented, caring people from the web community to publish their wealth of experience as printed books that stand the test of time. Paul and Alla are some of these people. Have you checked out their books already?


A practical guide on how to encourage clicks without shady tricks.

Add to cart $39

Design Systems

A practical guide to creating design languages for digital products.

Add to cart $39

Front-End & UX Workshops

Interactive, live online sessions, broken into 2.5h segments and a friendly Q&A.

Jump to topics →

Supercharge Testing React Applications With Wallaby.js

One thing you will discover very quickly when you start writing tests for an application is that you want to run your tests constantly when you are coding. Having to switch between your code editor and terminal window (or in the case of VS Code, the integrated terminal) adds an overhead and reduces your productivity as you build your application. In an ideal world, you would have instant feedback on your tests right in your editor as you are writing your code. Enter Wallaby.js.

What Is Wallaby.js?

Wallaby.js is an intelligent test runner for JavaScript that continuously runs your tests. It reports code coverage and other results directly to your code editor immediately as you change your code (even without saving the file). The tool is available as an editor extension for VS Code, IntelliJ Editors (such as WebStorm and IntelliJ IDEA), Atom, Sublime Text, and Visual Studio.

Why Wallaby.js?

As stated earlier, Wallaby.js aims to improve your productivity in your day to day JavaScript development. Depending on your development workflow, Wallaby can save you hours of time each
week by reducing context switching. Wallaby also provides code coverage reporting, error reporting, and other time-saving features such as time-travel debugging and test stories.

Getting Started With Wallaby.js In VS Code

Let’s see how we can get the benefits of Wallaby.js using VS Code.

Note: If you are not using VS Code you can check out here for instructions on how to set up for other editors.

Install The Wallaby.js VS Code Extension

To get started we will install the Wallaby.js VS Code extension.

After the extension is installed, the Wallaby.js core runtime will be automatically downloaded and installed.

Wallaby License

Wallaby provides an Open Source license for open source projects seeking to use Wallaby.js. Visit here to obtain an open-source license. You may use the open-source license with the demo repo for this article.

You can also get a fully functional 15-day trial license by visiting here.

If you want to use Wallaby.js on a non-open-source project beyond the 15-day trial license period, you may obtain a license key from the wallaby website.

Add License Key To VS Code

After obtaining a license key, head over to VS Code and in the command palette search for “Wallaby.js: Manage License Key”, click on the command and you will be presented with an input box to enter your license key, then hit enter and you will receive a notification that Wallaby.js has been successfully activated.

Wallaby.js And React

Now that we have Wallaby.js set up in our VS Code editor, let’s supercharge testing a React application with Wallaby.js.

For our React app, we will add a simple upvote/downvote feature and we will write some tests for our new feature to see how Wallaby.js plays out in the mix.

Creating The React App

Note: You can clone the demo repo if you like, or you can follow along below.

We will create our React app using the create-react-app CLI tool.

npx create-react-app wallaby-js-demo

Then open the newly scaffolded React project in VS Code.

Open src/App.js and start Wallaby.js by running: “Wallaby.js: Start” in VS Code command palette (alternatively you can use the shortcut combo — Ctrl + Shift + R R if you are on a Windows or Linux machine, or Cmd + Shift + R R if you are on a Mac).

When Wallaby.js starts you should see its test coverage indicators to the left of your editor similar to the screenshot below:

Wallaby.js provides 5 different colored indicators in the left margin of your code editor:

  1. Gray: means that the line of code is not executed by any of your tests.
  2. Yellow: means that some of the code on a given line was executed but other parts were not.
  3. Green: means that all of the code on a line was executed by your tests.
  4. Pink: means that the line of code is on the execution path of a failing test.
  5. Red: means that the line of code is the source of an error or failed expectation, or in the stack of an error.

If you look at the status bar you will see Wallaby.js metrics for this file and it’s showing we have a 100% test coverage for src/App.js and a single passing test with no failing test. How does Wallaby.js know this? When we started Wallaby.js, it detected src/App.js has a test file src/App.test.js, it then runs those tests in the background for us and conveniently gives us the feedbacks using its color indicators and also giving us a summary metric on our tests in the status bar.

When you also open src/App.test.js you will see similar feedback from Wallaby.js

Currently, all tests are passing at the moment so we get all green indicators. Let’s see how Wallaby.js handles failing tests. In src/App.test.js let’s make the test fail by changing the expectation of the test like so:

// src/App.test.js

The screenshot below shows how your editor would now look with src/App.test.js open:

You will see the indicators change to red and pink for the failing tests. Also notice we didn’t have to save the file for Wallaby.js to detect we made a change.

You will also notice the line in your editor in src/App.test.js that outputs the error of the test. This is done thanks to Wallaby.js advanced logging. Using Wallaby.js advanced logging, you can also report and explore runtime values beside your code using console.log, a special comment format //? and the VS Code command, Wallaby.js: Show Value.

Now let’s see the Wallaby.js workflow for fixing failing tests. Click on the Wallaby.js test indicator in the status bar to open the Wallaby.js output window. (“✗ 1 ✓ 0”)

In the Wallaby.js output window, right next to the failing test, you should see a “Debug Test” link. Pressing Ctrl and clicking on that link will fire up the Wallaby.js time travel debugger. When we do that, the Wallaby.js Tools window will open to the side of your editor, and you should see the Wallaby.js debugger section as well as the Value explorer and Test file coverage sections.

If you want to see the runtime value of a variable or expression, select the value in your editor and Wallaby.js will display it for you.

Also, notice the “Open Test Story” link in the output window. Wallby.js test story allows you to see all your tests and the code they are testing in a single view in your editor.

Let’s see this in action. Press Ctrl and click on the link — you should be able to see the Wallaby.js test story open up in your editor. Wallaby’s Test Story Viewer provides a unique and efficient way of inspecting what code your test is executing in a single logical view.

Another thing we will explore before fixing our failing test is the Wallaby.js app. Notice the link in the Wallaby.js output window: “Launch Coverage & Test Explorer”. Clicking on the link will launch the Wallaby.js app which will give you a compact birds-eye view of all tests in your project.

Next, click on the link and start up the Wallaby.js app in your default browser via http://localhost:51245/. Wallaby.js will quickly detect that we have our demo project open in our editor which will then automatically load it into the app.

Here is how the app should now look like:

You should be able to see the test’s metrics on the top part of the Wallaby.js app. By default, the Tests tab in the app is opened up. By clicking on the Files tab, you should be able to see the files in your project as well as their test coverage reports.

Back on to the Tests tab, click on the test and you should see the Wallaby.js error reporting feature to the right:

Now we’ve covered all that, go back to the editor, and fix the failing test to make Wallaby.js happy by reverting the line we changed earlier to this:


The Wallaby.js output window should now look like the screenshot below and your test coverage indicators should be all passing now.

Implementing Our Feature

We’ve explored Wallaby.js in the default app created for us by create-react-app. Let’s implement our upvote/downvote feature and write tests for that.

Our application UI should contain two buttons one for upvoting and the other for downvoting and a single counter that will be incremented or decremented depending on the button the user clicks. Let’s modify src/App.js to look like this.

// src/App.js
import React, { useState } from 'react';
import logo from './logo.svg';
import './App.css'; function App() { const [vote, setVote] = useState(0); function upVote() { setVote(vote + 1); } function downVote() { // Note the error, we will fix this later... setVote(vote - 2); } return ( <div className='App'> <header className='App-header'> <img src={logo} className='App-logo' alt='logo' /> <p className='vote' title='vote count'> {vote} </p> <section className='votes'> <button title='upVote' onClick={upVote}> <span role='img' aria-label='Up vote'> 👍🏿 </span> </button> <button title='downVote' onClick={downVote}> <span role='img' aria-label='Down vote'> 👎🏿 </span> </button> </section> </header> </div> );
} export default App;

We will also style the UI just a bit. Add the following rules to src/index.css

.votes { display: flex; justify-content: space-between;
} { font-size: 4rem;
button { padding: 2rem 2rem; font-size: 2rem; border: 1px solid #fff; margin-left: 1rem; border-radius: 100%; transition: all 300ms; cursor: pointer;
} button:focus,
button:hover { outline: none; filter: brightness(40%);

If you look at src/App.js, you will notice some gray indicators from Wallaby.js hinting us that some part of our code isn’t tested yet. Also, you will notice our initial test in src/App.test.js is failing and the Wallaby.js status bar indicator shows that our test coverage has dropped.

These visual clues by Wallaby.js are convenient for test-driven development (TDD) since we get instant feedback on the state of our application regarding tests.

Testing Our App Code

Let’s modify src/App.test.js to check that the app renders correctly.

Note: We will be using React Testing Library for our test which comes out of the box when you run create-react-app. See the docs for usage guide.

We are going to need a couple of extra functions from @testing-library/react, update your @testing-library/react import to:

import { render, fireEvent, cleanup } from '@testing-library/react';

Then let’s replace the single test in src/App.js with:

test('App renders correctly', () => { render(<App />); });

Immediately you will see the indicator go green in both the src/App.test.js line where we test for the render of the app and also where we are calling render in our src/App.js.

Next, we will test that the initial value of the vote state is zero(0).

it('Vote count starts at 0', () => { const { getByTitle } = render(<App />); const voteElement = getByTitle('vote count'); expect(voteElement).toHaveTextContent(/^0$/);

Next, we will test if clicking the upvote 👍🏿 button increments the vote:

it('Vote increments by 1 when upVote button is pressed', () => { const { getByTitle } = render(<App />); const upVoteButtonElement = getByTitle('upVote'); const voteElement = getByTitle('vote count');; expect(voteElement).toHaveTextContent(/^1$/);

We will also test for the downvote 👎🏿 interaction like so:

it('Vote decrements by 1 when downVote button is pressed', () => { const { getByTitle } = render(<App />); const downVoteButtonElement = getByTitle('downVote'); const voteElement = getByTitle('vote count');; expect(voteElement).toHaveTextContent(/^-1$/);

Oops, this test is failing. Let’s work out why. Above the test, click the View story code lens link or the Debug Test link in the Wallaby.js output window and use the debugger to step through to the downVote function. We have a bug… we should have decremented the vote count by 1 but instead, we are decrementing by 2. Let’s fix our bug and decrement by 1.

function downVote() { setVote(vote - 1);

Watch now how Wallaby’s indicators go green and we know that all of our tests are passing:


From this article, you have seen how Wallaby.js improves your developer experience when testing JavaScript applications. We have investigated some key features of Wallaby.js, set it up in VS Code, and then tested a React application with Wallaby.js.

Further Resources

Ecommerce Product Releases: October 15, 2020

Here is a list of product releases and updates for mid-October from companies that offer services to online merchants. There are updates on voice commerce, social commerce, customer-support texting, cybersecurity, on-demand fulfillment, and holiday shipping.

Got an ecommerce product release? Email

Ecommerce Product Releases

Instagram’s shopping cart is coming to IGTV and Reels. Instagram’s in-app shopping is expanding to IGTV and Reels. Shopping in IGTV (the app for long-form videos) is available now, and testing in Reels launches later this year. Using the Instagram Checkout feature, creators and brands can tag products in their videos, which are then available for purchase without leaving the app. Instagram is also testing commercials in IGTV to share ad revenue with creators.

Instagram IGTV

Instagram IGTV

Google is enabling shopping on YouTube. YouTube has started asking creators to use YouTube software to tag and track products featured in their clips. The data will then be linked to analytics and shopping tools from parent-company Google. The result will be a large catalog of items that viewers can browse, click, and purchase directly. Google is also testing an integration with Shopify for selling products through YouTube.

Google Assistant to bring voice control to the eBay app. Android customers can now seamlessly blend voice querying with the eBay app to facilitate an end-to-end shopping experience on their Android device. As one of Google Assistant’s first integrated ecommerce partners, eBay has collaborated with Google using a deep linking architecture on four main use cases, each of them central to the core shopping journey. Integrating the eBay app with Google’s Assistant is eBay’s first mainstream experience in voice commerce.

Tone raises $4 million to help ecommerce brands text with customers. Tone has raised $4 million in seed funding, led by Bling Capital. Tone helps ecommerce brands increase sales and customer satisfaction by allowing shoppers to get help quickly via text and check out faster. Tone’s human agents engage, educate, and convert visitors into customers with lightning-fast text conversations that take just a few minutes. Tone increases sales and customer satisfaction by giving online businesses a human that consumers can text with 24/7. Its app can be installed in less than 10 minutes for Shopify and Magento users or through a custom integration.



Apptega launches CyberXchange, a B2B marketplace for cybersecurity. Apptega, a platform that helps companies automate cybersecurity and compliance programs, has announced the launch of CyberXchange, a B2B ecommerce marketplace dedicated solely to cybersecurity and compliance. CyberXchange eliminates the ad hoc searching, guesswork, and complexity of cybersecurity commerce by matching providers with buyers. CyberXchange’s AI technology engine, called Harmony, maps leading cybersecurity products and services to over 10,000 categories and compliance standards, giving organizations an efficient way to find and buy the products and services they need to counter the expanding threat landscape.

USPS releases 2020 holiday shipping deadlines. The U.S. Postal Service has released its 2020 holiday shipping deadlines, in a post entitled “Despite 2020’s Best Efforts, the Holidays Are Approaching.” In addition to the shipping deadlines, the update provides a list of tips for a successful holiday mailing and shipping season. USPS anticipates Dec. 14 will be its busiest day online, with more than 13 million consumers predicted to visit

Tradeswell launches neural AI to enhance ecommerce margins. Tradeswell is launching a neural artificial intelligence platform for real-time ecommerce and direct-to-consumer brands. Tradeswell brings optimization across six key areas of ecommerce in real-time, including marketing, retail, inventory, logistics, forecasting, lifetime value, and financials, so brands know what to sell, to whom, where, and at what price with full visibility into customer data.



Ware2Go launches FulfillmentVu technology platform. Ware2Go, a UPS company offering on-demand fulfillment through an integrated tech platform for one- to two-day shipping, has announced FulfillmentVu, a new technology platform with a combined warehouse management system, order management system, and transportation management system. The platform helps merchants meet customer expectations across all sales channels. Its flexible pricing and operational model eliminate long-term contracts, as well as order minimums, to facilitate multichannel growth at any level.

Voice Shopping from Speak2web available on WooCommerce. WooCommerce store owners can now install the Voice Shopping WordPress plugin from Speak2web. Shoppers can utilize the world’s first web-based voice shopping experience from product discovery through checkout. With the Voice Shopping plugin, customers can search, add to cart, check out, and more.

Braze reveals multi-cloud support with Microsoft Azure. Braze, a customer engagement platform, announced several product and partnership updates. Braze is adding Microsoft Azure to its list of supported cloud platforms, with general availability expected in early 2021. Braze also has improved native reporting capabilities through a new Report Builder and Funnel Reports. With Report Builder, brands can quickly compare the results of multiple campaigns directly in Braze. Funnel Reports allow customers to visualize where conversion drop-offs occur and then quickly optimize outcomes at every step of the funnel.


Managing Long-Running Tasks In A React App With Web Workers

Response time is a big deal when it comes to web applications. Users demand instantaneous responses, no matter what your app may be doing. Whether it’s only displaying a person’s name or crunching numbers, web app users demand that your app responds to their command every single time. Sometimes that can be hard to achieve given the single-threaded nature of JavaScript. But in this article, we’ll learn how we can leverage the Web Worker API to deliver a better experience.

In writing this article, I made the following assumptions:

  1. To be able to follow along, you should have at least some familiarity with JavaScript and the document API;
  2. You should also have a working knowledge of React so that you can successfully start a new React project using Create React App.

If you need more insights into this topic, I’ve included a number of links in the “Further Resources” section to help you get up to speed.

First, let’s get started with Web Workers.

What Is A Web Worker?

To understand Web Workers and the problem they’re meant to solve, it is necessary to get a grasp of how JavaScript code is executed at runtime. During runtime, JavaScript code is executed sequentially and in a turn-by-turn manner. Once a piece of code ends, then the next one in line starts running, and so on. In technical terms, we say that JavaScript is single-threaded. This behavior implies that once some piece of code starts running, every code that comes after must wait for that code to finish execution. Thus, every line of code “blocks” the execution of everything else that comes after it. It is therefore desirable that every piece of code finish as quickly as possible. If some piece of code takes too long to finish our program would appear to have stopped working. On the browser, this manifests as a frozen, unresponsive page. In some extreme cases, the tab will freeze altogether.

Imagine driving on a single-lane. If any of the drivers ahead of you happen to stop moving for any reason, then, you have a traffic jam. With a program like Java, traffic could continue on other lanes. Thus Java is said to be multi-threaded. Web Workers are an attempt to bring multi-threaded behavior to JavaScript.

The screenshot below shows that the Web Worker API is supported by many browsers, so you should feel confident in using it.

Web Workers run in background threads without interfering with the UI, and they communicate with the code that created them by way of event handlers.

An excellent definition of a Web Worker comes from MDN:

“A worker is an object created using a constructor (e.g. Worker() that runs a named JavaScript file — this file contains the code that will run in the worker thread; workers run in another global context that is different from the current window. Thus, using the window shortcut to get the current global scope (instead of self within a Worker will return an error.”

A worker is created using the Worker constructor.

const worker = new Worker('worker-file.js')

It is possible to run most code inside a web worker, with some exceptions. For example, you can’t manipulate the DOM from inside a worker. There is no access to the document API.

Workers and the thread that spawns them send messages to each other using the postMessage() method. Similarly, they respond to messages using the onmessage event handler. It’s important to get this difference. Sending messages is achieved using a method; receiving a message back requires an event handler. The message being received is contained in the data attribute of the event. We will see an example of this in the next section. But let me quickly mention that the sort of worker we’ve been discussing is called a “dedicated worker”. This means that the worker is only accessible to the script that called it. It is also possible to have a worker that is accessible from multiple scripts. These are called shared workers and are created using the SharedWorker constructor, as shown below.

const sWorker = new SharedWorker('shared-worker-file.js')

To learn more about Workers, please see this MDN article. The purpose of this article is to get you started with using Web workers. Let’s get to it by computing the nth Fibonacci number.

Computing The Nth Fibonacci Number

Note: For this and the next two sections, I’m using Live Server on VSCode to run the app. You can certainly use something else.

This is the section you’ve been waiting for. We’ll finally write some code to see Web Workers in action. Well, not so fast. We wouldn’t appreciate the job a Web Worker does unless we run into the sort of problems it solves. In this section, we’re going to see an example problem, and in the following section, we’ll see how a web worker helps us do better.

Imagine you were building a web app that allowed users to calculate the nth Fibonacci number. In case you’re new to the term ‘Fibonacci number’, you can read more about it here, but in summary, Fibonacci numbers are a sequence of numbers such that each number is the sum of the two preceding numbers.

Mathematically, it is expressed as:

Thus the first few numbers of the sequence are:

1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 ...

In some sources, the sequence starts at F0 = 0, in which case the formula below holds for n > 1:

In this article we’ll start at F1 = 1. One thing we can see right away from the formula is that the numbers follow a recursive pattern. The task at hand now is to write a recursive function to compute the nth Fibonacci number (FN).

After a few tries, I believe you can easily come up with the function below.

const fib = n => { if (n < 2) { return n // or 1 } else { return fib(n - 1) + fib(n - 2) }

The function is simple. If n is less than 2, return n (or 1), otherwise, return the sum of the n-1 and n-2 FNs. With arrow functions and ternary operator, we can come up with a one-liner.

const fib = n => (n < 2 ? n : fib(n-1) + fib(n-2))

This function has a time complexity of 0(2n). This simply means that as the value of n increases, the time required to compute the sum increases exponentially. This makes for a really long-running task that could potentially interfere with our UI, for large values of n. Let’s see this in action.

Note: This is by no means the best way to solve this particular problem. My choice of using this method is for the purpose of this article.

To start, create a new folder and name it whatever you like. Now inside that folder create a src/ folder. Also, create an index.html file in the root folder. Inside the src/ folder, create a file named index.js.

Open up index.html and add the following HTML code.

<!DOCTYPE html>
<head> <link rel="stylesheet" href="styles.css">
<body> <div class="heading-container"> <h1>Computing the nth Fibonnaci number</h1> </div> <div class="body-container"> <p id='error' class="error"></p> <div class="input-div"> <input id='number-input' class="number-input" type='number' placeholder="Enter a number" /> <button id='submit-btn' class="btn-submit">Calculate</button> </div> <div id='results-container' class="results"></div> </div> <script src="/src/index.js"></script>

This part is very simple. First, we have a heading. Then we have a container with an input and a button. A user would enter a number then click on “Calculate”. We also have a container to hold the result of the calculation. Lastly, we include the src/index.js file in a script tag.

You may delete the stylesheet link. But if you’re short on time, I have defined some CSS which you can use. Just create the styles.css file at the root folder and add the styles below:

body { margin: 0; padding: 0; box-sizing: border-box; } .body-container, .heading-container { padding: 0 20px; } .heading-container { padding: 20px; color: white; background: #7a84dd; } .heading-container > h1 { margin: 0; } .body-container { width: 50% } .input-div { margin-top: 15px; margin-bottom: 15px; display: flex; align-items: center; } .results { width: 50vw; } .results>p { font-size: 24px; } .result-div { padding: 5px 10px; border-radius: 5px; margin: 10px 0; background-color: #e09bb7; } .result-div p { margin: 5px; } span.bold { font-weight: bold; } input { font-size: 25px; } p.error { color: red; } .number-input { padding: 7.5px 10px; } .btn-submit { padding: 10px; border-radius: 5px; border: none; background: #07f; font-size: 24px; color: white; cursor: pointer; margin: 0 10px; }

Now open up src/index.js let’s slowly develop it. Add the code below.

const fib = (n) => (n < 2 ? n : fib(n - 1) + fib(n - 2)); const ordinal_suffix = (num) => { // 1st, 2nd, 3rd, 4th, etc. const j = num % 10; const k = num % 100; switch (true) { case j === 1 && k !== 11: return num + "st"; case j === 2 && k !== 12: return num + "nd"; case j === 3 && k !== 13: return num + "rd"; default: return num + "th"; }
const textCont = (n, fibNum, time) => { const nth = ordinal_suffix(n); return &lt;p id='timer'&gt;Time: &lt;span class='bold'&gt;${time} ms&lt;/span&gt;&lt;/p&gt; &lt;p&gt;&lt;span class="bold" id='nth'&gt;${nth}&lt;/span&gt; fibonnaci number: &lt;span class="bold" id='sum'&gt;${fibNum}&lt;/span&gt;&lt;/p&gt;;

Here we have three functions. The first one is the function we saw earlier for calculating the nth FN. The second function is just a utility function to attach an appropriate suffix to an integer number. The third function takes some arguments and outputs a markup which we will later insert in the DOM. The first argument is the number whose FN is being computed. The second argument is the computed FN. The last argument is the time it takes to perform the computation.

Still in src/index.js, add the below code just under the previous one.

const errPar = document.getElementById("error");
const btn = document.getElementById("submit-btn");
const input = document.getElementById("number-input");
const resultsContainer = document.getElementById("results-container"); btn.addEventListener("click", (e) => { errPar.textContent = ''; const num = window.Number(input.value); if (num < 2) { errPar.textContent = "Please enter a number greater than 2"; return; } const startTime = new Date().getTime(); const sum = fib(num); const time = new Date().getTime() - startTime; const resultDiv = document.createElement("div"); resultDiv.innerHTML = textCont(num, sum, time); resultDiv.className = "result-div"; resultsContainer.appendChild(resultDiv);

First, we use the document API to get hold of DOM nodes in our HTML file. We get a reference to the paragraph where we’ll display error messages; the input; the calculate button and the container where we’ll show our results.

Next, we attach a “click” event handler to the button. When the button gets clicked, we take whatever is inside the input element and convert it to a number, if we get anything less than 2, we display an error message and return. If we get a number greater than 2, we continue. First, we record the current time. After that, we calculate the FN. When that finishes, we get a time difference that represents how long the computation took. In the remaining part of the code, we create a new div. We then set its inner HTML to be the output of the textCont() function we defined earlier. Finally, we add a class to it (for styling) and append it to the results container. The effect of this is that each computation will appear in a separate div below the previous one.

We can see that as the number increases, the computation time also increases (exponentially). For instance, from 30 to 35, we had the computation time jump from 13ms to 130ms. We can still consider those operations to be “fast”. At 40 we see a computation time of over 1 second. On my machine, this is where I start noticing the page become unresponsive. At this point, I can no longer interact with the page while the computation is on-going. I can’t focus on the input or do anything else.

Recall when we talked about JavaScript being single-threaded? Well, that thread has been “blocked” by this long-running computation, so everything else must “wait” for it to finish. It may start at a lower or higher value on your machine, but you’re bound to reach that point. Notice that it took almost 10s to compute that of 44. If there were other things to do on your web app, well, the user has to wait for Fib(44) to finish before they can continue. But if you deployed a web worker to handle that calculation, your users could carry on with something else while that runs.

Let’s now see how web workers help us overcome this problem.

An Example Web Worker In Action

In this section, we’ll delegate the job of computing the nth FN to a web worker. This will help free up the main thread and keep our UI responsive while the computation is on-going.

Getting started with web workers is surprisingly simple. Let’s see how. Create a new file src/fib-worker.js. and enter the following code.

const fib = (n) => (n < 2 ? n : fib(n - 1) + fib(n - 2)); onmessage = (e) => { const { num } =; const startTime = new Date().getTime(); const fibNum = fib(num); postMessage({ fibNum, time: new Date().getTime() - startTime, });

Notice that we have moved the function that calculates the nth Fibonacci number, fib inside this file. This file will be run by our web worker.

Recall in the section What is a web worker, we mentioned that web workers and their parent communicate using the onmessage event handler and postMessage() method. Here we’re using the onmessage event handler to listen to messages from the parent script. Once we get a message, we destructure the number from the data attribute of the event. Next, we get the current time and start the computation. Once the result is ready, we use the postMessage() method to post the results back to the parent script.

Open up src/index.js let’s make some changes.

... const worker = new window.Worker("src/fib-worker.js"); btn.addEventListener("click", (e) => { errPar.textContent = ""; const num = window.Number(input.value); if (num < 2) { errPar.textContent = "Please enter a number greater than 2"; return; } worker.postMessage({ num }); worker.onerror = (err) => err; worker.onmessage = (e) => { const { time, fibNum } =; const resultDiv = document.createElement("div"); resultDiv.innerHTML = textCont(num, fibNum, time); resultDiv.className = "result-div"; resultsContainer.appendChild(resultDiv); };

The first thing to do is to create the web worker using the Worker constructor. Then inside our button’s event listener, we send a number to the worker using worker.postMessage({ num }). After that, we set a function to listen for errors in the worker. Here we simply return the error. You can certainly do more if you want, like showing it in DOM. Next, we listen for messages from the worker. Once we get a message, we destructure time and fibNum, and continue the process of showing them in the DOM.

Note that inside the web worker, the onmessage event is available in the worker’s scope, so we could have written it as self.onmessage and self.postMessage(). But in the parent script, we have to attach these to the worker itself.

In the screenshot below you would see the web worker file in the sources tab of Chrome Dev Tools. What you should notice is that the UI stays responsive no matter what number you enter. This behavior is the magic of web workers.

We’ve made a lot of progress with our web app. But there’s something else we can do to make it better. Our current implementation uses a single worker to handle every computation. If a new message comes while one is running, the old one gets replaced. To get around this, we can create a new worker for each call to calculate the FN. Let’s see how to do that in the next section.

Working With Multiple Web Workers

Currently, we’re handling every request with a single worker. Thus an incoming request will replace a previous one that is yet to finish. What we want now is to make a small change to spawn a new web worker for every request. We will kill this worker once it’s done.

Open up src/index.js and move the line that creates the web worker inside the button’s click event handler. Now the event handler should look like below.

btn.addEventListener("click", (e) => { errPar.textContent = ""; const num = window.Number(input.value); if (num < 2) { errPar.textContent = "Please enter a number greater than 2"; return; } const worker = new window.Worker("src/fib-worker.js"); // this line has moved inside the event handler worker.postMessage({ num }); worker.onerror = (err) => err; worker.onmessage = (e) => { const { time, fibNum } =; const resultDiv = document.createElement("div"); resultDiv.innerHTML = textCont(num, fibNum, time); resultDiv.className = "result-div"; resultsContainer.appendChild(resultDiv); worker.terminate() // this line terminates the worker };

We made two changes.

  1. We moved this line const worker = new window.Worker("src/fib-worker.js") inside the button’s click event handler.
  2. We added this line worker.terminate() to discard the worker once we’re done with it.

So for every click of the button, we create a new worker to handle the calculation. Thus we can keep changing the input, and each result will hit the screen once the computation finishes. In the screenshot below you can see that the values for 20 and 30 appear before that of 45. But I started 45 first. Once the function returns for 20 and 30, their results were posted, and the worker terminated. When everything finishes, we shouldn’t have any workers on the sources tab.

We could end this article right here, but if this were a react app, how would we bring web workers into it. That is the focus of the next section.

Web Workers In React

To get started, create a new react app using CRA. Copy the fib-worker.js file into the public/ folder of your react app. Putting the file here stems from the fact that React apps are single-page apps. That’s about the only thing that is specific to using the worker in a react application. Everything that follows from here is pure React.

In src/ folder create a file helpers.js and export the ordinal_suffix() function from it.

// src/helpers.js export const ordinal_suffix = (num) => { // 1st, 2nd, 3rd, 4th, etc. const j = num % 10; const k = num % 100; switch (true) { case j === 1 && k !== 11: return num + "st"; case j === 2 && k !== 12: return num + "nd"; case j === 3 && k !== 13: return num + "rd"; default: return num + "th"; }

Our app will require us to maintain some state, so create another file, src/reducer.js and paste in the state reducer.

// src/reducers.js export const reducer = (state = {}, action) => { switch (action.type) { case "SET_ERROR": return { ...state, err: action.err }; case "SET_NUMBER": return { ...state, num: action.num }; case "SET_FIBO": return { ...state, computedFibs: [ ...state.computedFibs, { id:, nth: action.nth, loading: action.loading }, ], }; case "UPDATE_FIBO": { const curr = state.computedFibs.filter((c) => ===[0]; const idx = state.computedFibs.indexOf(curr); curr.loading = false; curr.time = action.time; curr.fibNum = action.fibNum; state.computedFibs[idx] = curr; return { ...state }; } default: return state; }

Let’s go over each action type one after the other.

  1. SET_ERROR: sets an error state when triggered.
  2. SET_NUMBER: sets the value in our input box to state.
  3. SET_FIBO: adds a new entry to the array of computed FNs.
  4. UPDATE_FIBO: here we look for a particular entry and replaces it with a new object which has the computed FN and the time taken to compute it.

We shall use this reducer shortly. Before that, let’s create the component that will display the computed FNs. Create a new file src/Results.js and paste in the below code.

// src/Results.js import React from "react"; export const Results = (props) => { const { results } = props; return ( <div id="results-container" className="results-container"> { => { const { id, nth, time, fibNum, loading } = fb; return ( <div key={id} className="result-div"> {loading ? ( <p> Calculating the{" "} <span className="bold" id="nth"> {nth} </span>{" "} Fibonacci number... </p> ) : ( <> <p id="timer"> Time: <span className="bold">{time} ms</span> </p> <p> <span className="bold" id="nth"> {nth} </span>{" "} fibonnaci number:{" "} <span className="bold" id="sum"> {fibNum} </span> </p> </> )} </div> ); })} </div> );

With this change, we start the process of converting our previous index.html file to jsx. This file has one responsibility: take an array of objects representing computed FNs and display them. The only difference from what we had before is the introduction of a loading state. So now when the computation is running, we show the loading state to let the user know that something is happening.

Let’s put in the final pieces by updating the code inside src/App.js. The code is rather long, so we’ll do it in two steps. Let’s add the first block of code.

import React from "react";
import "./App.css";
import { ordinal_suffix } from "./helpers";
import { reducer } from './reducer'
import { Results } from "./Results";
function App() { const [info, dispatch] = React.useReducer(reducer, { err: "", num: "", computedFibs: [], }); const runWorker = (num, id) => { dispatch({ type: "SET_ERROR", err: "" }); const worker = new window.Worker('./fib-worker.js') worker.postMessage({ num }); worker.onerror = (err) => err; worker.onmessage = (e) => { const { time, fibNum } =; dispatch({ type: "UPDATE_FIBO", id, time, fibNum, }); worker.terminate(); }; }; return ( <div> <div className="heading-container"> <h1>Computing the nth Fibonnaci number</h1> </div> <div className="body-container"> <p id="error" className="error"> {info.err} </p> // ... next block of code goes here ... // <Results results={info.computedFibs} /> </div> </div> );
export default App;

As usual, we bring in our imports. Then we instantiate a state and updater function with the useReducer hook. We then define a function, runWorker(), that takes a number and an ID and sets about calling a web worker to compute the FN for that number.

Note that to create the worker, we pass a relative path to the worker constructor. At runtime, our React code gets attached to the public/index.html file, thus it can find the fib-worker.js file in the same directory. When the computation completes (triggered by worker.onmessage), the UPDATE_FIBO action gets dispatched, and the worker terminated afterward. What we have now is not much different from what we had previously.

In the return block of this component, we render the same HTML we had before. We also pass the computed numbers array to the <Results /> component for rendering.

Let’s add the final block of code inside the return statement.

 <div className="input-div"> <input type="number" value={info.num} className="number-input" placeholder="Enter a number" onChange={(e) => dispatch({ type: "SET_NUMBER", num: window.Number(, }) } /> <button id="submit-btn" className="btn-submit" onClick={() => { if (info.num < 2) { dispatch({ type: "SET_ERROR", err: "Please enter a number greater than 2", }); return; } const id = info.computedFibs.length; dispatch({ type: "SET_FIBO", id, loading: true, nth: ordinal_suffix(info.num), }); runWorker(info.num, id); }} > Calculate </button> </div>

We set an onChange handler on the input to update the info.num state variable. On the button, we define an onClick event handler. When the button gets clicked, we check if the number is greater than 2. Notice that before calling runWorker(), we first dispatch an action to add an entry to the array of computed FNs. It is this entry that will be updated once the worker finishes its job. In this way, every entry maintains its position in the list, unlike what we had before.

Finally, copy the content of styles.css from before and replace the content of App.css.

We now have everything in place. Now start up your react server and play around with some numbers. Take note of the loading state, which is a UX improvement. Also, note that the UI stays responsive even when you enter a number as high as 1000 and click “Calculate”.

Note the loading state and the active worker. Once the 46th value is computed the worker is killed and the loading state is replaced by the final result.


Phew! It has been a long ride, so let’s wrap it up. I encourage you to take a look at the MDN entry for web workers (see resources list below) to learn other ways of using web workers.

In this article, we learned about what web workers are and the sort of problems they’re meant to solve. We also saw how to implement them using plain JavaScript. Finally, we saw how to implement web workers in a React application.

I encourage you to take advantage of this great API to deliver a better experience for your users.

Further Resources

The Wirecard Fiasco: Digital Payments Gone Wrong

What would you do if your credit card processor and merchant account provider were fraudulent? That’s the reality for many thousands of worldwide businesses that relied on Wirecard, the Germany-based financial technology firm that is now in bankruptcy proceedings, having committed, allegedly, sham practices for years.

I’ll describe the Wirecard fiasco in this post. It’s shocking because of the apparent widespread level of fraud and the lessons for the digital payments industry.

Image of Wirecard headquarters near Munich, Germany. Source: Wikipedia.

Wirecard headquarters near Munich, Germany. Source: Wikipedia.

A Sketchy Start?

Wirecard is a multinational payment processor, merchant acquirer, card issuer, and technology service provider. The company declared bankruptcy in August 2020. It was listed on the German DAX, a notable stock index similar to the Dow Jones Industrial Average.

Wirecard launched in 1999 as a payment-technology company. In 2002, Wirecard’s then CEO, Marcus Braun (who is now under arrest), shifted strategy to processing payments for, mainly, gambling and pornography websites.

In 2005, Wirecard raised funds by issuing shares in the Frankfurt Stock Exchange through a reverse IPO, having purchased the listing of a failed call center company named InfoGenie. This allowed Wirecard to expedite going public and, some would say, avoid much of the scrutiny.

With the new capital, Wirecard acquired a German bank called XCOM, which held international merchant acquiring and card issuing licenses, thereby allowing the newly created Wirecard Bank to become both a worldwide issuer and acquirer. Purchasing companies for their licenses is fairly common.

The XCOM transaction transformed Wirecard into a sprawling, complex business. In the ensuing 14 years, Wirecard allegedly used this complexity to artificially inflate profits, hide massive losses, forge contracts, and dupe investors, auditors, and regulators.

Rise and Fall

From 2006 to 2018, Wirecard expanded aggressively. It acquired several smaller, Asia-based payment processors, an Indian payments company, and several Citibank-owned processing and prepaid-card portfolios in Asia and North America.

Business for Wirecard was, reportedly, booming. At its peak in 2018, Wirecard’s public valuation was €24 billion (roughly USD $28 billion at the time of writing). The company had 5,000 employees and claimed to process payments for 250,000 merchants worldwide in addition to its card-issuing and technology operations. Wirecard replaced one of Germany’s largest banks on the DAX-30 index.

So, what went wrong?

A lot. It’s useful to examine the scandal’s timeline to understand the level of fraud, collusion, and deceit. The Financial Times, which exposed the apparent depth of the scandal, offers excellent coverage.

  • 2015 and 2016. The Financial Times and short-sellers begin to probe. BaFin — Federal Financial Supervisory Authority, Germany’s principal financial regulator — sides with Wirecard. As far back as 2008, a small group of Wirecard shareholders complained about what they believed were accounting irregularities. Wirecard hired Ernst & Young, the accounting firm, to investigate. The complainants were silenced, two investors were prosecuted for insider trading, and Wirecard escaped unscathed. Ernst & Young would become Wirecard’s outside auditor for the next 11 years.
  • 2008 through 2015. Wirecard expanded rapidly and, for the most part, avoided controversy. In 2015, however, a Financial Times report alleged significant accounting problems in, primarily, Wirecard’s payment-processing business. Later in 2015, a group of short-sellers claimed that Wirecard’s operations in Asia were much smaller than reported by the company.

Nonetheless, Wirecard acquired a payment processor in India for, reportedly, €340 million (USD $401 million). The Financial Times later alleged that Indian shareholders never received €175 million to €285 million from the sale.

  • 2016. A group of short-sellers published allegations against Wirecard, including money laundering. BaFin, the German regulator, investigated but ultimately sided with Wirecard. This became a recurring pattern: Whistleblowers and journalists accuse Wirecard of improprieties, and regulators side with the company.

Unscathed, Wirecard acquired Citigroup’s North America prepaid-card business, giving Wirecard a foothold in the U.S.

  • 2018 to 2019. In early 2018, a whistleblower in Wirecard’s Singapore office alleged that the company was defrauding investors by engaging in “round-tripping,” a practice of selling something and then refunding the buyer later — the sold assets are never transferred from the seller to the buyer. The transaction is fake (and illegal).

Concerned employees in Singapore took this accusation seriously and initiated an internal investigation. In October 2018, the employees contacted The Financial Times, which published a report on Wirecard’s Singapore operations. Another BaFin investigation occurred. Singaporean law enforcement became involved, leading to a raid of Wirecard’s offices.

BaFin regulators, again, sided with Wirecard. BaFin announced a two-month prohibition on short-selling Wirecard’s stock, claiming that Wirecard is too important to the health of the German economy.

  • 2019. Fake companies and fake profits. In March of 2019, The Financial Times published a report claiming that roughly half of Wirecard’s revenue and most of its profit are from referral fees with smaller processor partners. This isn’t unusual as most large processors work closely with merchant account providers and other partners.

But many of Wirecard’s processing partners did not exist. They were fake. Indeed, when they attempted to visit the offices of Wirecard’s partners in the Philippines, reporters discovered dwellings of uninvolved residents.

Wirecard’s response was to sue The Financial Times and the Singapore authorities, who earlier had named five Wirecard employees and eight partner companies as suspects in a criminal investigation.

Ernst & Young, the auditors, approved Wirecard’s 2018 financial statements and recommended only minor compliance procedures for Wirecard’s Singapore office.

  • October 2019. The Financial Times reported that profits from Wirecard’s operations in Dubai and Ireland were also inflated and that even more of the company’s partners did not exist. Pressure mounting, Wirecard appointed KPMG, another accounting firm and a competitor to Ernst & Young, to conduct an audit.
  • 2020. Wirecard’s demise. After a series of delays, KPMG published its reports. The findings shocked investors, BaFin, and the German police.

KPMG challenged the authenticity of Wirecard’s profits from 2016 to 2018, citing a glaring lack of evidence, such as no bank statements showing income received. KPMG could not confirm at least 34 Wirecard clients and at least €1 billion in cash. Wirecard had fabricated at least three years of profits.

In early June, German authorities raided Wirecard’s headquarters and launched a criminal investigation against Wirecard’s CEO and several other executives.

On June 16, two Philippine banks disclosed that documents provided by Wirecard to authorities to support €1.9 billion in cash balances were “spurious” (fake). Two days later, Wirecard announced that the €1.9 billion is “missing.” Wirecard’s stock price crashed, and its creditors called in approximately €2 billion in loans.

Following its CEO’s resignation, Wirecard’s new management acknowledged the colossal scale of a multi-year accounting fraud, adding a “prevailing likelihood” that €1.9 billion that was supposed to be in its accounts does not exist.

On June 25, following the arrest of its former CEO, Wirecard announced that it would file for insolvency (bankruptcy).

Build And Deploy An Angular Form With Netlify Forms And Edge

Creating the frontend, backend, and deployment workflow of an app takes a lot of work. In instances where your app collects only a limited amount of data submissions from its users, building a whole backend may not seem worth the time and effort. An alternative to developing a complete backend is using Netlify Forms. In this tutorial, I’ll explain how you could use an Angular reactive form with Netlify Forms. Since Netlify Forms only work when deployed on Netlify, I’ll also illustrate how to deploy your app on Netlify Edge.

The Toolkit

An Angular reactive form is a form that has a structured data model created explicitly within a component class using the ReactiveFormsModule providers. A form model is created for each input element within the form view. This form model is an instance of the FormControl class and it keeps track of the value of the form element. The form model is immutable because whenever a change is made to the model the FormControl instance returns a new data model instead of updating the old model. Its immutability makes change detection more efficient and allows data alteration with observable operators. Since form input elements are directly connected to their form models, updates between them are synchronous and do not rely on UI rendering.

Netlify is a platform that allows you to build, deploy, and host sites built with various technologies. Sites built with Angular can be hosted on Netlify. Netlify additionally provides a host of tools that simplify, automate, and augment builds and deployments of these sites. We’re going to use two of its products in this tutorial: Netlify Edge and Netlify Forms.

As described earlier, Netlify Forms is a form handling feature that receives submissions from HTML forms automatically. It does not require any submission processing configuration, like creating APIs, scripts, etc. This feature only works with forms in sites deployed on Netlify. It is enabled by default, further reducing the configuration needed to set up the form submissions. Submission handling is set up during deployment where a site’s HTML files are parsed by Netlify’s build bots.

Netlify Edge is a global application delivery network on which sites and applications are published. It provides features like A/B testing, rollbacks, staging, and phased rollouts. All deployments on Netlify Edge are atomic, meaning a site is only live when all files have been uploaded/updated and changes to the site are ready. Once a site is deployed, it is assigned a subdomain on when deployed to production. Netlify Edge also supports preview and branch deployments (staging, development, etc.).

Netlify Forms submission-handling works because build bots parse HTML forms on a site during deployment. Client-side Javascript rendered forms like those in compiled Angular sites won’t be found by these bots. So the normal set up for Netlify Forms won’t work with Angular Forms.

However, there is a work-around to this. To get it to receive submissions, a hidden plain HTML form is added to the index.html file. This form works with the build bots. When submitting the Angular Form, a post request is made to this hidden form which is then captured by Netlify Forms.

In this article, we will create a reactive form. We’ll also develop a service to make a post request to the hidden HTML form. Lastly, we will deploy the app to Netlify Edge.


To illustrate how to build the app, we will take an example of a feedback form common on many websites. We will use this form to collect comments/complaints, questions, and suggestions from users of the site along with their name and email. We shall also use it to collect their rating of the site.


To follow along with this tutorial, you will need a Netlify account and the Angular CLI installed. If you do not have the CLI, you can install it using npm.

npm install -g @angular/cli

If you’ve not signed up for a Netlify account yet, you can create one here. Netlify offers sign-up through Github, Gitlab, Bitbucket, or Email. Depending on what deployment method you choose to go with, they may be other requirements. They will be stated under each deployment method.

Setting Up The App

To start, we will create the app and call it feedback. When creating it, add routing to it when asked in the prompts.

ng new feedback

Next, we’ll generate three components: a feedback form, a successful submission message page, and a 404 page. Netlify Forms allow you to navigate to a page upon successful form entry submission. That’s what we’ll use the SuccessComponent for.

ng g c feedback
ng g c success
ng g c page-not-found

After generating the components, we’ll add the routes to each page in the AppRoutingModule within the app-routing.module.ts file.

const routes: Routes = [ { path:'', component: FeedbackComponent }, { path: 'success', component: SuccessComponent }, { path: '**', component: PageNotFoundComponent }

We’ll use the FormBuilder service to create our reactive form. This is because it is more convenient and less repetitive than using basic form controls. To have access to it, we’ll need to register the ReactiveFormsModule in the app.module.ts file.

Since we will be making a post request to the hidden HTML form, we also have to register the HttpClientModule.

import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http'; @NgModule({ imports: [ // other imports ReactiveFormsModule, HttpClientModule ]
export class AppModule { }

Proceed to change the contents of app.component.html to just have the router outlet.


The different pages will share some styling. So add the styling below to styles.css.

html, body { height: 100%; width: 100%; display: flex; align-items: flex-start; justify-content: center;
} h1 { margin: 0; text-align: center;
} h1, p, label { font-family: Arial, Helvetica, sans-serif;
} p { max-width: 25rem;
} #container { border: none; padding: .4rem; border-radius: 0; flex-direction: column; display: flex;
} hr { width: 80%;
} button { color: white; background-color: black; font-size: large; padding: .5rem; border-radius: .5rem; margin-top: 1rem;
} @media screen and (min-height: 700px) { html, body { align-items: center; justify-content: center; }
} @media screen and (min-width: 480px) { #container { border: .1rem solid lightgray; padding: 2rem; border-radius: .5rem; } html, body { align-items: center; justify-content: center; }

Create The Reactive Form

In our FeedbackComponent class, we will begin by importing the FormBuilder service which we’ll use to create the form. We’ll also import the Validators class for form input validation.

import { FormBuilder, Validators } from '@angular/forms';

We will then inject the FormBuilder service by adding it to the FeedbackComponent constructor.

constructor(private fb: FormBuilder) { }

Next, we’ll define the form model using the group method of the injected FormBuilder service. We’ll also add an errorMsg property to hold any errors we may encounter when submitting the form input. Also included is a closeError method that will close the error alert that displays on the form.

Each control in the form model will be verified using validators from the Validators class. If any of the inputs fail validation, the form will be invalid and submission will be disabled. You can choose to add multiple validators to a form control like in the case of the email control.

export class FeedbackComponent { feedbackForm ={ firstName: ['', Validators.required], lastName: ['', Validators.required], email: ['', [, Validators.required]], type: ['', Validators.required], description: ['', Validators.required], rating: [0, Validators.min(1)] }); errorMsg = ''; closeError() { this.errorMsg = ''; } // ...

In the component’s template (feedback.component.html), we shall add this.

<div id="container"> <div class="error" [class.hidden]="errorMsg.length == 0"> <p>{{errorMsg}}</p> <span (click)="closeError()" class="close">✖︎</span> </div> <h1>Feedback Form</h1> <hr> <p>We’d like your feedback to improve our website.</p> <form [formGroup]="feedbackForm" name="feedbackForm" (ngSubmit)="onSubmit()"> <div id="options"> <p class="radioOption"> <input formControlName="type" type="radio" id="suggestion" name="type" value="suggestion"> <label for="suggestion">Suggestion</label><br> </p> <p class="radioOption"> <input formControlName="type" type="radio" id="comment" name="type" value="comment"> <label for="comment">Comment</label><br> </p> <p class="radioOption"> <input formControlName="type" type="radio" id="question" name="type" value="question"> <label for="question">Question</label><br> </p> </div> <div class="inputContainer"> <label>Description:</label> <textarea rows="6" formControlName="description"></textarea> </div> <div class="inputContainer"> <div id="ratingLabel"> <label>How would you rate our site?</label> <label id="ratingValue">{{feedbackForm.value?.rating}}</label> </div> <input formControlName="rating" type="range" name="rating" max="5"> </div> <div class="inputContainer"> <label>Name:</label> <div class="nameInput"> <input formControlName="firstName" type="text" name="firstName" placeholder="First"> <input formControlName="lastName" type="text" name="lastName" placeholder="Last"> </div> </div> <div class="inputContainer"> <label>Email:</label> <input formControlName="email" type="email" name="email"> </div> <div class="inputContainer"> <button type="submit" [disabled]="feedbackForm.invalid">Submit Feedback</button> </div> </form>

Note that the form element should have the [formGroup]="feedbackForm" attribute corresponding to the model we just created. Also, each of the input elements should have a formControlName="" attribute corresponding to its counterpart form control in the model.

To style the form, add this tofeedback.component.css.

#options { display: flex; flex-direction: column;
} #options label { margin: 0 0 0 .2rem;
} .radioOption { margin: 0 0 .2rem 0;
} .inputContainer { display: flex; flex-direction: column; margin: .5rem 0 .5rem 0;
} label { margin: .5rem 0 .5rem 0;
} .nameInput { display: flex; flex-direction: column;
} button:disabled { cursor: not-allowed; pointer-events: all; background-color: slategrey;
} #ratingLabel { display: flex; justify-content: space-between; margin: .5rem 0 .5rem 0;
} #ratingValue { font-weight: bolder; font-size: large; border: .1rem solid lightgray; padding: .4rem .6rem .1rem .6rem; margin: 0; vertical-align: middle; border-radius: .3rem;
} .error { color: darkred; background-color: lightsalmon; border: .1rem solid crimson; border-radius: .3rem; padding: .5rem; text-align: center; margin: 0 0 1rem 0; display: flex; width: inherit;
} .error p { margin: 0; flex-grow: 1;
} textarea, input { margin: .1rem; font-family: Arial, Helvetica, sans-serif; padding: 5px; font-size: medium; font-weight: lighter;
} .close { cursor: default;
} .hidden { display: none;
} @media screen and (min-width: 480px) { #options { flex-direction: row; justify-content: space-around; } .nameInput { flex-direction: row; justify-content: space-between; }

This is what the form will look like:

Adding A Hidden HTML Form

As stated earlier, we need to add a hidden HTML form that the Netlify Forms build bots can parse. Submissions will then be sent from our reactive form to the hidden HTML form. The HTML form is put in the index.html file.

This form should have the same name as the reactive form. Additionally, it should contain three other attributes: netlify, netlify-honeypot, and hidden. The bots look for any forms that have the netlify attribute so that Netlify can process inputs from them. The netlify-honeypot attribute is added to prevent captchas from being shown when a submission is made and enables extra spam protection.

<!doctype html>
<html lang="en">
<!-- Head --> <body> <form name="feedbackForm" netlify netlify-honeypot="bot-field" hidden> <input type="text" name="firstName"/> <input type="text" name="lastName"/> <input type="text" name="email"/> <input type="text" name="feedbackType"/> <input type="text" name="description"/> <input type="text" name="rating"/> </form> <app-root></app-root> </body>

It’s important to note that since you can’t set the value of file input elements, you can’t upload a file using this method.

Making A Post Request To The Hidden Form

To send a submission from the reactive form to the HTML form, we’ll make a post request containing the submission to index.html. The operation will be performed in the onSubmit method of the FeedbackComponent.

However, before we can do that, we need to create two things: a Feedback interface and a NetlifyFormsService. Let’s start with the interface.

touch src/app/feedback/feedback.ts

The contents of this file will be:

export interface Feedback { firstName: string; lastName: string; email: string; type: string; description: string; rating: number;

The NetlifyFormsService will contain a public method to submit a feedback entry, a private method to submit a generic entry, and another private one to handle any errors. You could add other public methods for additional forms.

To generate it, run the following:

ng g s netlify-forms/nelify-forms

The submitEntry method returns an Observable<string> because Netlify sends a HTML page with a success alert once we post data to the form. This is the service:

import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse, HttpParams } from '@angular/common/http';
import { Feedback } from '../feedback/feedback';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators'; @Injectable({ providedIn: 'root'
export class NetlifyFormsService { constructor(private http: HttpClient) { } submitFeedback(fbEntry: Feedback): Observable { const entry = new HttpParams({ fromObject: { 'form-name': 'feedbackForm', ...fbEntry, 'rating': fbEntry.rating.toString(), }}); return this.submitEntry(entry); } private submitEntry(entry: HttpParams): Observable { return '/', entry.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, responseType: 'text' } ).pipe(catchError(this.handleError)); } private handleError(err: HttpErrorResponse) { let errMsg = ''; if (err.error instanceof ErrorEvent) { errMsg = A client-side error occurred: ${err.error.message}; } else { errMsg = A server-side error occurred. Code: ${err.status}. Message: ${err.message}; } return throwError(errMsg); }

We’ll send the form submission as HttpParams. A header for the ContentType should be included with the value application/x-www-form-urlencoded. The responseType option is specified as text because if successful, posting to the hidden form will return an HTML page containing a generic success message from Netlify. If you do not include this option, you will get an error because the response will be parsed as JSON. Below is a screenshot of the generic Netlify success message.

In the FeedbackComponent class, we shall import the NetlifyFormsService and Router. We’ll submit the form entry using the NetlifyFormsService.submitEntry method. If the submission is successful, we will redirect to the successful submission page and reset the form. We’ll use the Router service for the redirection. If unsuccessful, the errorMsg property will be assigned the error message and be displayed on the form.

import { Router } from '@angular/router';
import { NetlifyFormsService } from '../netlify-forms/netlify-forms.service';

After that, inject both the NetlifyFormsService and Router in the constructor.

constructor( private fb: FormBuilder, private router: Router, private netlifyForms: NetlifyFormsService
) {}

Lastly, call the NetlifyFormsService.submitEntry method in FeedbackComponent.onSubmit.

onSubmit() {
this.netlifyForms.submitFeedbackEntry(this.feedbackForm.value).subscribe( () => { this.feedbackForm.reset(); this.router.navigateByUrl('/success'); }, err => { this.errorMsg = err; } );

Create A Successful Submission Page

When a user completes a submission, Netlify returns a generic success message shown in the last screenshot of the previous section. However, you can link back to your own custom success message page. You do this by adding the action attribute to the hidden HTML form. Its value is the relative path to your custom success page. This path must start with / and be relative to your root site.

Setting a custom success page, however, does not seem to work when using a hidden HTML form. If the post request to the hidden HTML form is successful, it returns the generic Netlify success message as an HTML page. It does not redirect even when an action attribute is specified. So instead we shall navigate to the success message page after a submission using the Router service.

First, let’s add content to the SuccessComponent we generated earlier. In success.component.html, add:

<div id="container"> <h1>Thank you!</h1> <hr> <p>Your feedback submission was successful.</p> <p>Thank you for sharing your thoughts with us!</p> <button routerLink="/">Give More Feedback</button>

To style the page, add this to success.component.css:

p { margin: .2rem 0 0 0; text-align: center;

This is what the page looks like:

In the FeedbackComponent class, we already added the Routerservice as an import and injected it into the constructor. In its onSubmitmethod, after the request is successful and the form has reset, we navigate to the successful submission page, /success. We use the navigateByUrl method of the router to do that.

Creating The 404 Page

The 404 page may not be necessary but is a nice to have. The contents of page-not-found.component.html would be:

<div id="container"> <h1>Page Not Found!</h1> <hr> <p>Sorry! The page does not exist.</p> <button routerLink="/">Go to Home</button>

To style it, add this to page-not-found.component.css:

p { text-align: center;

This is what the 404 page will look like.

Fix Routing Before Deployment

Since we’re using the Router service, all our routing is done on the client. If a link to a page in our app is pasted in the address bar (deep link) or there is a page refresh, that request we’ll be sent to our server. The server does not contain any of our routes because they were configured in the frontend, in our app. We’ll receive a 404 status in these instances.

To fix this, we need to tell the Netlify server to redirect all requests to our index.html page. This way our Angular router can handle them. If you’re interested, you can read more about this phenomenon here and here.

We’ll start by creating a _redirects file in our src folder. The _redirects file is a plain text file that specifies redirect and rewrite rules for the Netlify site. It should reside in the site publish site directory (dist/<app_name>). We’ll place it in the src folder and specify it as an asset in the angular.json file. When the app is compiled, it will be placed in dist/<app_name>.

touch src/_redirects

This file will contain the rule below. It indicates that all requests to the server should be redirected to index.html. We also add a HTTP status code option at the end to indicate that these redirects should return a 200 status. By default, a 301 status is returned.

/* /index.html 200

The last thing we have to do is add the below option in our angular.json und er projects > {your_project_name} > architect > options > assets. Include it in the assets array:

{ "glob": "_redirects", "input": "src", "output": "/"

Preview Your App Locally

Before you can deploy the feedback app, it’s best to preview it. This allows you to make sure your site works as you had intended it. You may unearth issues resulting from the build process like broken paths to resources among other things. First, you’ll have to build your app. We’ll then serve the compiled version using a server. We’ll use lite-server which is a lightweight live-reload server for web apps.

Note: Since the app is not deployed on Netlify just yet, you’ll get a 404 error when you attempt to make the post request. This is because Netlify Forms only work on deployed apps. You’ll see an error on the form as shown in the screenshot below, however, it will work once you’ve deployed it.

  1. To begin, install lite-server:
    npm install lite-server --save-dev
  2. Next, within your app’s workspace directory, build your app. To make sure builds are run every time your files change, pass the --watch flag to it. Once the app is compiled, the results are written to the dist/<app name> output directory. If you are using a version control system, make sure to not check in the dist folder because it is generated and is only for preview purposes.
    ng build --watch
  3. To serve the compiled site, run the lite-server against the build output directory.
    lite-server --baseDir="dist/<app name>"

The site is now served at localhost:3000. Check it out on your browser and make sure it works as expected before you begin its deployment.


There are multiple ways you can deploy your Angular project onto Netlify Edge. We shall cover three here:

  1. Using netlify-builder,
  2. Using Git and the Netlify web UI,
  3. Using the Netlify CLI tool.

1. Using netlify-builder

netlify-builder facilitates the deployment of Angular apps through the Angular CLI. To use this method, your app needs to have been created using Angular CLI v8.3.0 or higher.

  1. From the Sites tab of your Netlify dashboard, create a new project. Since we won’t be using Git to create a project, drag any empty folder to the dotted-border area marked “Drag and drop your site folder here”. This will automatically create a project with a random name. You can change this name under the site’s domain settings later if you wish.

    This is what you should see once your project has been created.
  2. Before you can deploy using this method, you will need to get the Netlify project’s API ID and a Netlify personal access token from your account. You can get the project API ID from the site settings. Under Site Settings > General > Site Details > Site Information you will find your project’s API ID.

    You can get a personal access token in your user settings. At User Settings > Applications > Personal access tokens, click the New Access Token button. When prompted, enter the description of your token, then click the Generate Token button. Copy your token. For persistence’s sake, you can store these values in a .env file within your project but do not check this file in if you are using a version control system.
  3. Next, add netlify-builder to your project using ng add.
    ng add @netlify-builder/deploy

    Once it’s done installing, you will be prompted to add the API ID and personal access token.

    It’s optional to add these here. You could ignore this prompt because they will be added to your angular.json file which is usually checked in if you use a version control system. It’s not safe to store this kind of sensitive information on code repos. If you are not checking this file in, you could just input your API ID and personal access token. The entry below will be modified in your angular.json file under the architect settings.

    "deploy": { "builder": "@netlify-builder/deploy:deploy", "options": { "outputPath": "dist/<app name>", "netlifyToken": "", "siteId": "" }
  4. All that’s left is to deploy your application by running:
    NETLIFY_TOKEN=<access token> NETLIFY_API_ID=<api id> ng deploy

    Alternatively, you could put this in a script and run it when you need to deploy your app.

    # To create the script
    touch && echo "NETLIFY_TOKEN=<access token> NETLIFY_API_ID=<api id> ng deploy" >> && chmod +x # To deploy

    This is the output you should see once you run this command:

2. Using Git And The Netlify Web UI

If your Angular app’s code is hosted on either Github, Bitbucket, or Gitlab, you can host the project using Netlify’s web UI.

  1. From the Sites tab on your Netlify dashboard, click the “New site from Git” button.
  2. Connect to a code repository service. Pick the service where your app code is hosted. You’ll be prompted to authorize Netlify to view your repositories. This will differ from service to service.
  3. Pick your code repository.
  4. Next, you’ll specify the deployments and build settings. In this case, select the branch you’d like to deploy from, specify the build command as ng deploy --prod and the publish directory as dist/<your app name>.
  5. Click the Deploy Site button and you’re done.

3. Using The Netlify CLI Tool

  1. To start, install the Netlify CLI tool as follows:
    npm install netlify-cli -g

    If the installation is successful, you should see these results on your terminal:

  2. Next, log in to Netlify by running:
    netlify login

    When you run this command, it will navigate to a browser window where you will be prompted to authorize the Netlify CLI. Click the Authorize button. You can then proceed to close the tab once authorization is granted.

  3. To create a new Netlify project, run the following on your terminal:
    netlify init

    You will be prompted to either connect your Angular app to an existing Netlify project or create a new one. Choose the Create & configure a new site option. Next, select your team and a name for the site you would like to deploy. Once the project has been created, the CLI tool will list site details for your project.
    After which the CLI tool will prompt you to connect your Netlify account to a Git hosting provider to configure webhooks and deploy keys. You cannot opt-out of this. Pick an option to login in then authorize Netlify.
    Next, you’ll be asked to enter a build command. Use:

    ng build --prod

    Afterward, you’ll be asked to provide a directory to deploy. Enter dist/<app name> with your app’s name.
    At the end of that, the command will complete and display this output.

  4. To deploy the app, run:
    netlify deploy --prod

    Using the --prod flag ensures that the build is deployed to production. If you omit this flag, the netlify deploy command will deploy your build to a unique draft URL that is used for testing and previewing. Once the deployment is complete, you should see this output:

Viewing Form Submissions

Form submissions can be viewed on the Netlify dashboard under the Forms tab of your site. You can find it at<your_site_name>/forms. On this page, all your active forms will be listed. The name attribute that you put down in the hidden form element is the name of the form on the dashboard.

Once you select a form, all the submissions for that form will be listed. You can choose to download all the entries as a CSV file, mark them as spam, or delete them.


Netlify Forms allow you to collect form submission from your app without having to create or configure a backend to do it. This can be useful especially in apps that only need to collect a limited amount of data like contact information, customer feedback, event sign-ups, and so on.

Pairing Angular reactive forms with Netlify forms allow you to structure your data model. Angular reactive forms have the added benefit of having their data model and form elements being in sync with each other. They do not rely on UI rendering.

Although Netlify Forms only work when deployed on Netlify Edge, the hosting platform is pretty robust, provides useful features like A/B testing, and automates app builds and deployments.

You can continue reading more about using Netlify with your forms over here.

SEO: 5 Tips to Convert Visitors to Buyers

Search engine optimization focuses on driving consumers to your site. But traffic alone doesn’t pay the bills. Success depends on converting those visitors to customers.

Organic search traffic converts to revenue, on average, roughly 3 percent of the time, according to the studies I’ve seen, although the top converting sites reach 10 percent or more.

Remember that searchers drop onto your site with no navigational context. Searchers that query your brand typically have a goal in mind. Helping them accomplish that goal increases the likelihood of a purchase or at least a positive impression.

Others won’t know anything about your company. They haven’t seen the rest of your site — most of them won’t see your home page. They have no knowledge of the products you carry, the promotions you’re running, or what distinguishes your site.

Use these five tips to convert more searchers into buyers.

Converting Searchers into Buyers

1. Don’t rely on brand recognition. Most searchers chose your listing because it seemed at a glance to meet their needs.  It wasn’t because they recognized your company or the brands you carry.

For that reason, sites organized by product brand instead of type or feature will struggle in organic search. It’s harder for brand-oriented sites to rank for the non-branded queries that most people search for. It’s also harder for those non-brand searchers to navigate your site for the products and information they’re looking for.

2. Highlight promotions on every page. Prices are a major purchase consideration in most ecommerce sectors — the smaller your brand, the more important the price.

Unfortunately, most consumers coming from organic results will miss out on home page discounts or promotions. Spotlight those promotions on every page in some way.

Some sites use static or rotating banners across the top of all pages. Others display (i) a pop-up modal upon a visitor’s initial entry, (ii) a flag in the corner or on the side of the page, or (iii) a banner that slides up and down from the bottom of the page.

Test which of those work best for your audience and your site’s design.

3. Make them feel confident. There’s a good chance that any searcher you’re trying to convert has never been to your site. He might not have heard of your company.

Brands that are less prominent need more proof points than household names. Help searchers feel confident about purchasing from your site by displaying instant signs of trustworthiness, such as:

  • An about us page to give tangible substance to your business;
  • Trusted symbols and logos such as Google star ratings, Trustpilot, Better Business Bureau, and industry affiliations;
  • Your phone number. If your brand is a household name, placing the phone number in a less prominent position, such as your footer, is fine. If not, make it obvious as a sign that shoppers can contact your company quickly.
  • A chat feature for instant support.

4. Include product reviews. I’ve heard all of the objections to product reviews:

  • What if no one reviews anything?
  • What if there are negative reviews?
  • We don’t have time to moderate.

None of those outweigh the benefits. Reviews offer other shoppers an independent indicator of value. And for SEO, product reviews serve as an excellent source of unique, user-generated content, which enriches the product page‘s keyword theme overall.

If you don’t have product reviews, resist the temptation to add a raft of Google business reviews. They don’t help your SEO, and it reinforces the absence of reviewed products.

5. Help them choose. Most searchers, again, will have no experience with your brand. Thus they’re going to have a harder time choosing among your offerings. Incorrect choices lead to more returns and frustrated buyers.

Offer size guides, help icons, product comparison charts, and other features that inspire confidence and increase the likelihood that shoppers will choose the right product the first time.

Design Shopping: Get A Faster Client Buy-In Through A Guided Design Showcase

Regardless of where you live, getting your driver’s license renewed or the address updated on your personal ID card may often be a painful chore. It’s such a frustrating experience that Disney’s Zootopia movie (IMDb) thought it best to represent DMV employees as sloths. Despite popular opinion, however, the California DMV (California Department of Motor Vehicles) is highly motivated to better serve the 30 million licensed drivers in California.

Better serving those citizens means improving a frustrating website experience — the primary way Californians access necessary services. That’s why the DMV Strike Team hired our design team at 10up to reimagine the entire California DMV website.

In the following case study, we’ll take a close look at a design exercise that we call internally Design Shopping. This exercise allows us to better communicate with the client and to more easily move our collaboration in the right direction.

Note: This article is geared more towards experienced UX/UI design teams working on some rather large projects.

Design Shopping?

Before we get to the finished product (the new DMV website), we have to start at the very beginning: understanding the client. As a government agency, they (therefore, we) had many stakeholders to juggle with varying motivations. This meant gaining client alignment during the discovery phase was our top priority.

While we don’t expect clients to be design-savvy — many find the concept of design confusing or intimidating — we need to get everyone on the same page. But how were we able to move the needle on design in a government works project?

We took them shopping. Design Shopping, that is.

Why “Design Shopping”?

When it comes to client services, first impressions are everything. Leaving a lasting impression, however, isn’t always easy, especially when working with a large and varied stakeholder group. To do so, let’s take a lesson from the great American poet, Maya Angelou:

I’ve learned that people will forget what you said, people will forget what you did, but people will never forget how you made them feel.

— Maya Angelou

Cue the trumpets! From the very first to the very last interactions with our team, to leave a lasting impression on clients, we must make them feel good.

With these precious first moments in mind, our team experimented with various approaches to provide the DMV team a safe space to feel a connection and build rapport so everyone feels confident and comfortable sharing their ideas and opinions without judgment.

What we landed on is a concept that everyone simultaneously understands and immediately puts them at ease: shopping!

Shopping removes the pressure of being right or wrong — a layman immediately understands “picking and choosing” the right “fit.” Shopping also releases dopamine, which activates the brain’s reward and pleasure centers. This is important because it creates positive feelings while driving clients toward action.

Voilà — the birth of the Design Shopping exercise! Good feelings and design discussion equal nerd out!

Live look at design nerd-out! (Image source: ‘Mochimochi Land’ by Anna Hrachovec)

Yay! Now that we’ve set the tone, let’s get into the thick of this process: defining why and what we’re shopping for. This gives our team a much-needed opportunity to slow down and get inspired. In other words — get deep on the project. Our research and final set of artifacts are what frame the design conversation during the exercise.

To have a meaningful “I see you” moment with the DMV team, we needed to demonstrate to the stakeholder group that we were engaged and ready to translate their goals into tangible design results.

Swim In That Research

Ground your design thinking prior to visual exploration and workshop planning. Not all of these are required, but if it’s within your team’s control, have the following ready:

  • Identified project goals;
  • Problem statements;
  • Competitive analysis;
  • Mental model comparisons;
  • Historical context;
  • Project brief;
  • Preliminary UX discovery;
  • Content maps.

Sharing all the love for design. (Image source: ‘Mochimochi Land’ by Anna Hrachovec)

Go Deep

Make time for your brain to wander and get lost. When collecting artifacts for the design shopping exercise consider both sides of the coin: successful and unsuccessful examples that demonstrate what you’ve grocked from the research above.

Lead the conversation with ease by using the artifacts as a path and conversation trigger. Look for opportunities to point at:

  • Aesthetics or preferences;
  • Design understanding toward user experience;
  • Business goals;
  • Specific calls to action;
  • And how design can help support all of that beyond just looks.

This gives clients greater ownership of the design vision based on tangible benefits to their organization or user experience.

Tactical Inspiration

Don’t limit design inspiration to what you’re familiar with. Get weird! Here’s a brief list of where our team looks for inspiration:

Focus Your Artifacts

We’ve found that limiting the number of design shopping artifacts to 5-7 total, helps the team maintain focus and avoid fatigue. To narrow down the website artifacts, choose:

  • One in their vertical for familiarity and showcasing an understanding of fundamentals.
  • One outside the vertical that demonstrates or challenges brand attributes.
  • One that demonstrates challenging design choices (you think they’ll hate it).
  • One that demonstrates thoughtful design choices (you think they’ll love it).
  • Something they outlined in your research (ie. something the client has already pointed out) you want to dive deeper into.
  • One that’s reflective of the mental model of their audience (what you think will appeal to your understanding of their target).

Set Those Wheels In Motion

Choose Your Champions

Pump the breaks — before you jump into showing the client these sites, assign a facilitator. This champion will guide the group through the workshop, encourage participation, and help the group work toward outlined goals. Also assign an official notetaker to document the conversation to allow our facilitator(s) much appreciated space to focus on the participants.

With the DMV, we had an unusually large stakeholder group so we had two facilitators run the room. To achieve the same experience in a virtual setting, using multiple breakout rooms with more than one facilitator also works really well.

Workshop Materials

Next, gather your materials. Virtual or on-site, you should have:

  • Red and green color coding labels aka dot stickers;
  • Emoji stickers — these are a fun addition to the traditional red and green for votes;
  • Post-it notes for comments;
  • Screenshots of websites.

Here’s how it looks set up on a Miro board:

Stop, Collaborate And Listen

Whether your workshop is online or in-person, you must first set ground rules. Take the first 5-10 minutes of the workshop to break the ice — make sure the loudest stakeholders don’t control the entire conversation.

If driving a virtual workshop (the answer these days is a resounding YES), be sure to unblock technology paralysis. Confirm all systems are a go with a quick demo of how to use the tools you’ll be accessing during the workshop. This can take another 5-10 minutes.

If time is of the essence, send the virtual board ahead of time with some DIY instructions so workshop participants are more prepared to participate.

Then, set clear expectations and reiterate the purpose of the workshop.

Voting Parameters

Make sure to limit the number of votes each participant has. Whether these votes are virtual dots or real stickers, limit the amount of dots to 5 green and 5 red, for a total of 10 votes.

We’ve found that limiting the number of votes forces participants to be more deliberate when shopping, which leads to richer conversations and results. As a fun bonus, we provide a set of emojis for those emotionally charged shopping moments. This tends to get a lot of love from participants!

Ready, Set, Shop!

Once your client has all the tools, it’s time to shop shop shop! Set the timer for 15 minutes and invite participants to mingle while they vote on their favorite and least-favorite aspects of the designs before them. While they vote, ask pointed questions about the “whats” and “whys.”

We’ve found that standing while completing this exercise shifts the lens and puts participants on an even playing field. During this pandemic, we’ve explored options to add this experience to the virtual space and found that pre-filling the voting panels with avatars of the participants adds a nice human element and shifts the lens in much the same way as standing together does. Ghost cursors are just weird, am I right?

Guiding The Conversation

When the timer runs out, discuss the team’s shopping success. This is where you get to the heart of the shared language for the project. As you walk through each artifact, discuss patterns that start to emerge — both negative and positive.

We invite shoppers to volunteer. For example, say “I’m seeing a lot of green dots here. Let’s talk about why?”

Rather than singling out participants, this opens up to the room for discussion and evaluation. This is especially important when dealing with a large and varied stakeholder group like the DMV. This is a team effort and every step should feel collaborative.

The intent is to eliminate the pressure of a “correct answer.”

You want to hear the client’s design vocabulary and build upon that. Through this evaluation, you can begin to connect the dots and lead the team to foundational design principles and decisions you’ll be making throughout the following design phases.

Communicate Results

After everyone is done shopping, validate with the team what you’re seeing as a result of the exercise.

Outline all the areas where you have consensus, where you diverge, and note what the next steps look like.

We are not responsible for any impromptu dance moves from participants.

Once More, With Feeling

While we just outlined all the major ways you can run the design shopping workshop, this isn’t limited to a one-time exercise. We often have one final round of Design Shopping at another date.

This is where you take what was established in the first workshop and create.

Once in the middle of prototypes, moodboards, style tiles, and design principles — whatever you deem to be the best platform for expressing the design direction — you can use the Design Shopping exercise as a way to validate your own design solutions. If still stuck, throw in a Design Spectrum exercise to get really specific.

With a comfortable rapport already built and the client already familiar with the workshop, you can move past the initial aches of building a design vocabulary and move straight to evaluating and discussing the design.


The Design Shopping workflow allows you to move quickly toward a shared design vocabulary and foundational design principles in a deliberate, meaningful way. It’s like taking a road trip across California: you need to review your destinations and how you want to experience them before hitting the road.

As an industry, we are always talking about educating the client, but rarely structure that education in a way that’s not only painless but fun. Considering the amount of pressure the DMV team was under, the last thing our team wanted to do was add further stress, as further stress leads to poor outcomes for client relationships and of course, design.

The thing to remember is that the fun has a purpose — you’re gaining collective agreement for future communications to preemptively head off conflict. The visual nature of this workshop also adds value for stakeholders that may have missed the live session. When they refer back to the shopping exercise, it’s clear what direction to double down on and what to avoid.

In the end, Design Shopping is less about shopping for nice fonts or colors and more about showcasing the ROI (Return on Investment) of thoughtful design. It’s about creating a forum that allows creative possibilities to be discussed openly in an accessible way.

When done right, this exercise allows your client to see real potential because you showed them real examples where design has come to life. The abstract has now become concrete! With that foundation in place, you can look forward to creative innovation and confidence from your toughest critics: your clients.

Special Thanks

All the Kudos to Lea Alcantara (Visual Design Director at 10up), my design nerd out partner (who helps me craft useful workshops for our clients at 10up, including this one), and for the additional article tweaks and research done!