An HTML attribute potentially worth $4.4M to Chipotle

Written by Jason Grigsby on

I recently found myself racing to fill out Chipotle’s online order form before my mother could find her credit card. In the process, I discovered a bug that could cost Chipotle $4.4 million annually.

My parents are retired. They continue to try to pay for meals. I don’t want them to. So we often end up in a competition to see who can pay first.

In this case, I knew I had an advantage. My card details were already stored in the browser. I just needed to use autofill to fill in the form quickly and complete the order before my mother could find her card.

Unfortunately, the form returned errors upon submission:

Chipotle's order form displaying two errors along with text that says 'Change Payment Method.'

At that moment, I was still in a race against my mother’s parental instincts, so I quickly copied my card information from 1Password and completed the transaction.

Later, I wondered if this was a problem with autofill. I’ve written extensively about autofill in the past, and if there was a problem with autofill, I wanted to know about it.

Chipotle’s cryptic error messages

Chipotle’s error messages didn’t give me much to go on. The first error said:

We encountered an error saving your card. Please check your card info and try again.

What? I didn’t ask Chipotle to save my card. I intentionally left that box unchecked.

While this was irritating, my hunch was that this error wasn’t causing the problem. I got the same error if I checked the box. I suspected the second error was what prevented me from buying lunch.

Unfortunately, the second error didn’t provide any useful information:

We ran into an error submitting your order, please try again.

Every time I autofilled the form and submitted it, I got the same two errors. If I didn’t use autofill, the form worked.

Then I noticed something. The credit card expiration date changed after it was filled in.

Video of autofill on the Chipotle order form. Autofill tries to enter 2023 for the year, but it changes to 20 instead.

There was the culprit. My credit card expires in 2023. The field only accepts two digits and instead of 23 to represent 2023, the form received 20.

Now that I could replicate the problem, I wanted to figure out why it was happening. Is it a problem with autofill or with Chipotle’s form?

Time to put on my detective’s cap

I enjoy figuring out why browsers aren’t working the way we expect. It’s like a good mystery novel, but one you get to solve. In this case, the first clue I examined was the HTML for the expiration year field:

<input 
    class="form-control payment-control ng-pristine ng-isolate-scope ng-valid-mask ng-empty ng-invalid ng-invalid-required ng-touched" 
    aria-label="expiration year" 
    cmg-cc-expiration-validator="cmg-cc-expiration-validator" 
    cmg-cc-expiration-validator-func="$ctrl.isCardExpired($ctrl.card.expiration)" 
    ng-model-options="{ updateOn: 'default blur', debounce: { default: 500, blur: 0 } }" 
    id="expirationDateYear" 
    name="expdateyear" 
    ng-required="true" 
    ng-model="$ctrl.card.expiration.year" 
    placeholder="YY" 
    autocomplete="cc-exp-year" 
    ui-mask="99" 
    type="text" 
    required="required" 
    aria-invalid="true" 
    style="">

Many of the classes starting with ng-. This is a telltale sign of Angular.

Next, I dug into Chipotle’s JavaScript to look for something that was referencing cmg-cc-expiration-validator which sounded like it might be a key attribute for validation on this field. I found what I was looking for in Chipotle’s app JS file. I’ve pulled the relevant section into a gist for reference in case Chipotle changes in their JavaScript in the future.

Among the 400 lines or so in the validation section, this reference stood out to me:

angular.module('ui.mask', []).value('uiMaskConfig', {

An Angular module that masks UI? A quick search found the github repo for ui-mask. Chipotle’s JavaScript code I found earlier appeared to be leveraging this module.

What is ui-mask doing for the expiration field?

After discovering the ui-mask module, one of the input field’s attributes that I had overlooked earlier suddenly took on increased significance:

ui-mask="99"

The ui-mask documentation provides this example:

For example, we use '9' here to accept any numeric values for a phone number: ui-mask="(999) 999-9999"

So ui-mask="99" on the expiration year field is telling the ui-mask module to only accept two numeric values. When autofill tries to enter 2023, this ui-mask only lets the first two characters be entered.

Web standards alternatives

I’m sure there are good reasons why Chipotle is using the ui-mask module. I don’t want to sound like I’m questioning their decisions. I suspect that the module is useful for multiple fields and serves other purposes on the site.

However, for this expiration year field, we could accomplish the same thing using two standard HTML5 attributes:

pattern="\d\d"
maxlength="2"

The pattern attribute uses regular expressions to validate that the user’s input matches what we expect. In this case, \d tells the browser we want a digit character. By supplying \d twice, we tell the browser that we want two digits so that 01 will be accepted, but 1 will not.1

The maxlength attribute tells the browser how many characters are allowed and will prevent users from typing more than the allotted amount.

Fixing autofill

To test what would happen if Chipotle’s form used these standards, I opened my browser’s developer tools and edited the expiration year field:

Video of autofill on the Chipotle order form after `maxlength=”2″` has been added using developer tools. It works!

Adding the maxlength attribute to the field fixes the problem. This makes sense. We’re telling the browser, and by extension the autofill feature, how many digits it should use for the expiration year.

Autofill is smart enough to know that if we only want two digits for a year field, that the form needs the last two digits of the year. We just need to tell the browser how many digits we expect.

And we need to tell it in a standard way.

What impact does this have on Chipotle’s business?

Now that the mystery was solved, I wondered how much this problem might cost Chipotle. The first time I encountered this error—long before the credit card race with my mother—I thought Chipotle’s website was broken. I gave up trying to order online.

How many other people have failed to finish an order because the form doesn’t support autofill and the error messages aren’t helpful?

We know that when people use autofill, they complete forms 30% faster, but we don’t know how many people use autofill overall.

So because we don’t know how many people use autofill, let’s use a conservative number.

What if Chipotle could gain half a percentage point increase in transactions from fixing autofill?

Could we calculate how much revenue Chipotle might see from half a percentage point increase in transactions? We might be able to.

In their April 2019 quarterly report, Chipotle revealed that “the company’s relaunched website is attracting 1 million transactions a week on average”. In 2018, Chipotle said that their average online order was “$16 to $17.”

I’m going to use the upper end of that range ($17) for the average online order because that average is over a year old and in July of this year, Chipotle reported that “online sales nearly doubled in the second quarter” and “average check jumped by 3.5%.”

Based on the information in the most recent earnings report, using $17 for the average per online order and 1 million transactions per week is probably a low. That’s perfect for our attempt to make a conservative estimate.

How much is maxlength="2" worth?

Let’s do the math:

   1,000,000  Online transactions per week
*       .005  Half a percentage point
=      5,000  Equals 5k transactions per week
*        $17  Times average online order in 2018
=    $85,000  Equals $85k per week
*         52  Multiplied by the weeks in a year
= $4,420,000  for maxlength="2"

If fixing autofill caused a half of a percentage point increase in online orders, it would increase Chipotle’s revenue by $4.4 million.

But perhaps half of a percentage point is too high. Let’s use a quarter of a percentage point instead. That’s still $2.2 million.

I’m not sure how low you would need to go before adding the maxlength attribute wouldn’t be worth it.

Lessons for your web forms

There are three key takeaways from Chipotle’s order form that you should consider:

1. Use HTML5 input features

Select the correct input type for your field. Use other input attributes such as maxlength, minlength, and pattern to provide additional clues to the browser about what information the field expects.

2. Support autofill

Add the autocomplete attribute with the appropriate token to tell autofill the purpose of the field. This is now the standard way to support autofill.

3. Make autofill part of your test plans

Once your form works with autofill, make sure it continues to work by including autofill in your QA process. This will help you identify problems like the one with Chipotle’s form before they impact users.


P.S. Chipotle, if this is helpful, may I suggest you buy my parents’ lunch next time? 🙂

P.P.S. Oh, and if you make $4.4 million off this article, you should hire us to help you with progressive web apps and the payment request API.


  1. Originally I suggested changing the type of field from text to number to force a numeric value. However, Amier pointed out in the comments that number doesn’t accept maxlength. Instead, number expects you to use min and max to set the minimum and maximum values. I did some testing and found that using those values doesn’t solve the autofill issues. That’s fine. We want 01 not 1 for the expiration year which means we’re actually asking for two digits, not a number. Therefore, it makes more sense for the input type to be set to text and use the pattern attribute to constrain the input to digits. 
Jason Grigsby

Jason Grigsby is one of the co-founders of Cloud Four, Mobile Portland and Responsive Field Day. He is the author of Progressive Web Apps from A Book Apart. Follow him at @grigs.

Never miss an article!

Get Weekly Digests


Comments

Add a comment

Wow, what a cool finding. I was under the impression that maxlength did nothing on number inputs. In my quick test the behavior of maxlength is inconsistent when manually entering content. On Windows, in Chrome and Firefox maxlength isn’t enforced for number inputs. Edge (non-chromium) does enforce maxlength on number inputs when numbers are entered but allows letters to be entered and doesn’t enforce maxlength in that case. Chrome, Firefox and Edge enforce maxlength for text inputs. On Android, in both Chrome and Firefox, maxlength wasn’t enforced for both number and text inputs. https://jsfiddle.net/x4qgrafv/1/

Replies

Thanks for the comment Amier. You raise a great question. Thanks for the jsfiddle example.

I never tested the exact combination you looked at. I was mostly trying to figure out how to get autofill to work. Adding maxlength solved autofill so I didn’t go one step further to strip out all of the Angular code to make sure that maxlength not only fixed autofill, but constrained the length of the input.

I also never bothered to test changing input type from text to number because I started with maxlength and it fixed the problem.

Looking at the spec for maxlength, I didn’t see anything that indicates that is shouldn’t work on number fields. But you’re right that I missed the section in the number specthat specifically says that maxlength should not be used with number.

If we use number, we’re supposed to instead use min and max to define the minimum and maximum values. In 2019, that would probably work for a credit card date field that accepts two digits, but it wouldn’t work around the turn of the century where the expiration year might be 01 which the number field would treat as 1 because 01 isn’t a number.

Thinking aloud here, I wonder if the suggestion to use number for this field is the real problem. This might be a case where using text with a pattern would be the better solution because we want two digits which may, or may not, be a real numerical value.

🤔

BTW, comments on most sites are a cesspool, but we’ve kept our comments open because we tend to get smart comments that contribute to the conversation. Thanks Amier for providing yet another example of how wonderful our readers are.

A bigger problem is that this type of autofill allows anyone who has someone’s credit card for 10 seconds to use it at Chipotle or anywhere else they know the formula. I’m not implying that YOU use your mom’s credit card for your OWN lunch until she happens to notice, but that you COULD. Hardly secure.

Replies

All technology can be abused and used for bad purposes. In your example autofill is not the issue but rather the fact, that all information that’s needed to buy is written on the card, is. A fraudster could just as easily take a picture of the card’s front and back.

Be vigilant to transactions you don’t recognize and consider (if possible) to set up additional security to your credit card (eg Visa 3D Secure).

Browsers always ask you if you’d like to save your CC info for autocompletion when submitting. So your Mom would have to opt in for making this insecure if you use the same device.

I think select elements are a lot less error prone for expiration dates. Not sure how that would affect autofill or Chipotle’s revenues.

Replies

Short: it takes longer
Long: on first checkout, it takes far longer for the majority of users to fill in on a select based form than an input based one. The ideal is always that it always works with autofill so users are 99% more lazy, but it’s still there.

When the payments API is supported (and the default with a fallback available) it offers eeverything in a well done fashion. Base it off that I guess…

Hello Jason,

Nice troubleshooting! As a former AngularJS developer, your solution can pose problems with the form validation that AngularJS does since it doesn’t hook into the html5 validators directly.

Your solution can still be implemented AND play nice with AngularJS by using the directives for validators (ng-maxlength and ng-pattern).

Nice article!

Great article but ironically, when I tried to subscribe to weekly posts, the form’s button doesn’t fire. Not trying to be a smart, just thought you should know. Anyway, love the article and the real use case example is great.

I wonder if, for even more reliability, it’s better to just use the full 4 digits. It’s more explicit but then again, more to type for non autofillers. And then there’s the infamous drop down select menu…

Replies

I am not sure about everyone, but it is prob less to type, because I myself would try to put in the full year, and then watch how my full year vanished, and left the wrong part of the year, so then I go back and type the end part.. so that ends up being 6 chars typed not 4. Not even counting the deleting of the invalid value.

Awesome article, especially because it explains in detail and with numbers the impact on the business that a decision can have.

Following the same idea of the article, I wanted to share this article on twitter and I didn’t find a button to do it automatically, so I gave up and I’m going to keep working, how much money will this cost you?

Regards,
David

Replies

Hi David,

Thanks for the comment. Regarding your questions about social sharing buttons, that was a conscious decision not to include them on our part. We don’t use them for three reasons:

  1. Studies say they don’t get used often. Moovweb found that “Only 0.2% of users ever click on a mobile sharing button”. Smashing Magazine found that removing the buttons increased inbound traffic.
  2. These social icons often allow the Facebook, Twitter and others to track what content you’re visiting. This has obvious privacy implications.
  3. Social media buttons—particularly those added by third-party scripts—add page bloat and affect performance.

So we have good reasons for not including them. That said, I am interested in adding support for the Web Share API which can be used to trigger the native sharing mechanism in browsers that support it. We can test for browser support and add it without too much additional JavaScript.

As for the amount of money not including social buttons costs us? It is unlikely to cost us anything. We don’t make money off of the articles we write. We don’t have advertising. We don’t have subscriptions.

We write articles to share what we’ve learned and to help move the web forward. Any monetary relationship between what we write and the work we do is indirect at best.

Thanks for your note and the reminder that we should add Web Share API support to our backlog.

-Jason

I’ve had this happen on several sites, and always blamed LastPass and made a mental note to check on it later. Thanks for your discovery & explaining this!

I always try to use input type=’tel’ for fields where I want a number, because on mobile if you’re typing it in, that will bring up the number pad keyboard, without the UI showing the annoying stepper arrows that type=’num’ adds. Could ‘tel’ replace ‘text’ in your suggestion?

Well-written article, thanks!

Holy heck! Chipotle should pay you at least $85,000 for discovering that bug. (Though I am a little puzzled as to why you save cards in the browser if you already have 1Password.)

Sallie Goetsch (rhymes with 'sketch')

Reply

Replies

Storing them in the browsers is faster when filling out forms. Plus, 1Password is a power user experience and is part of the reason why I ignored autofill for so many years. So now I like to use autofill so I can see where it breaks.

I always fall into the trap of ignoring if a “number” is really a number, especially in the case of input field types. I have to think about something a friend once told me – if you aren’t doing math on it, it’s probably just a string in disguse. It’s really useful to remember for databases, too. I’ve seen too many databases where things like phone numbers are stored as BIGINT.

It would be really nice if we could specify a keyboard without specifying a field type for situations like this.

These are the little cracks that happen when you have the traditional designer / developer approach. Likely the individual who built this form was more of a our engineer type just implementing from a static mockup. Teams with front end specialists who focus on building standardized components are able to anticipate these kinds of things.

To clarify- when autofill enters 2024 only the 20 get through. This would leave to me believe that for credit cards that expire in 2020 would not experience the issue because coincidentally the first and last two digits are the same. How many credit cards expire in 2020? I would super roughly ballpark guess 33% because credit cards typically expire 3 years after issue. You should therefore subtract 33% from your revenue loss estimates.

Replies

Sure. That’s fine. The cost estimate is certainly wrong. I’ve had people argue it is too high. I’ve had others argue is too low. The half a percent increase in revenue was just a wild guess anyways.

The article wasn’t about the potential revenue. It was to highlight autofill, the potential impact it can have, and the relatively small amount of markup it takes to support it.

You gonna be a star! Even some Russian news agencies made a post about your finding 😉
Thanks for article! Very nice!
P.S. I agree with Madeline Bernard about input type=tel for mobile. It provides a better user experience.

Nice article! Good effort in debugging and figuring out this issue and writing! However, I didn’t like the click-bait factor in it. I came to read thinking it was something but it’s actually a completely different thing.
Cheers!

Replies

Thanks for your feedback. I can understand how someone might think the title is clickbait, but I disagree.

First, the headline describes the content of the article. Chipotle’s web form is missing a single html attribute that blocks autofill and that attribute has the potential to cause them to lose millions of dollars of revenue. I wrote multiple variations of the headline before settling on a simple, straightforward description of the content.

Second, sure I like it when articles I like gain traction, but I never know when it will happen. Things that I’ve thought were certain to be shared widely have received no notice. Other things that I wrote that seemed like throw away articles have been incredibly popular.

Because of this, I gave up long ago trying to drive traffic to things. I write about what I find interesting and that I think others might too. If it gets shared, that’s cool. If it doesn’t, that’s cool too.

As a former journalism major, I’d like to believe my professors would be pleased with the headline. It seems to fit all of the standards we discussed for writing newspaper headlines. That was long before going viral and clickbait were even things. The web was just starting.

But hell, maybe I’m wrong and it turns out that my idea of a simple, direct headline lands for readers as a clickbait headline. It’s always hard to know for certain how something you write will be perceived.

As Washington once said, “I am unconscious of intentional error, I am nevertheless too sensible of my defects not to think it probable that I may have committed many errors.” That’s the story of my life.

Replies

Thanks Jason for the explanation! appreciate the effort you put in the comment.
I wasn’t sure about your motives before you replied but now I understand where you come from, so I now know that this wasn’t your intention. But this is a User Experience issue. It’s not like you can disagree with it. It’s as if you’re developing an app and a user says that I think that this particular button is not in a good position and I find it not comfortable to use. You can’t just say that you disagree with that user. It’s just something he/she experienced. We all make mistakes and my intention wasn’t to call you out on yours, it was just to point out what I thought unpleasant, and maybe others might feel the same but didn’t bother to write it. I just wanted to share it with you because I’d need to know if any user is having a bad experience in my app and maybe you’d want to know too

Maher Santina

Replies

But this is a User Experience issue. It’s not like you can disagree with it.

I’m conflicted about this.

On the one hand, I generally agree that when you write or say something and it lands in a way that isn’t what you intended, the intention doesn’t matter which is the point you’re making.

On the other hand, I believe—and this may be where we disagree—that there is a generally agreed-upon definition of clickbait that we can use to evaluate the headline. For example, the WikiPedia entry describes clickbait as:

Clickbait is a form of false advertisement which uses hyperlink text or a thumbnail link that is designed to attract attention and entice users to follow that link and read, view, or listen to the linked piece of online content, with a defining characteristic of being deceptive, typically sensationalized or misleading.

Or as TechCrunch put it in WTF is Clickbait:

Clickbaiting is the intentional act of over-promising or otherwise misrepresenting — in a headline, on social media, in an image, or some combination — what you’re going to find when you read a story on the web.

I don’t think the headline fits those definitions. As I said, I’m mostly proud of the headline because it states clearly what the article is about in as few words as possible.

All that said, I can’t ignore that I’m writing in a period of time where people do encounter clickbait headlines all the time. So when I say that I can understand why the headline feels like clickbait to others, I mean that genuinely. I get it. Just because it doesn’t fit the definition of clickbait doesn’t mean that it doesn’t land as clickbait. I have no way of knowing what someone else’s experience will be like.

Plus, maybe others disagree with the definition of clickbait headlines or disagree with my assertion that the headline doesn’t fit the definition.

I just wanted to share it with you because I’d need to know if any user is having a bad experience in my app and maybe you’d want to know too

I honestly do want to know. I haven’t taken offense. Nor do I feel defensive. And I appreciate how cordial and gracious your correspondence has been.

I like thinking about these questions and about the impact our words have. I appreciate the opportunity to reflect on the headline, how challenging it was to write it, and think about how I might have written it differently.

At the moment, I don’t think that I would have written differently because this was the best of a bunch of bad headlines and because I do think it accurately describes the content. But you’ve given me food for thought and who knows, maybe a couple weeks from now I’ll think of a different headline that I wished I’d used instead.

Thanks Maher!

Replies

Thank Jason for the detailed comments!
Ohh I agree it doesn’t fit the click-bait definitions that you shared. I might be mistaken when I said that, I would just say that it’s misleading, at least for me.
You know what I thought when I read the title? I thought that Chiptole have designed a new HTML attribute and they’re selling it for 4.4M 😂 But to tell you honestly after I read the article and looked back at the title it made total sense
I’m happy that you’re not offended as this also wasn’t my intention!
You reminded me of school days when we had to write an essay and the title had to be attractive to get a good grade! I always stumbled like 10 minutes just to write a title and then it would end up the cringiest thing 😂 So good job on that!
Yes I totally understand that a person wouldn’t know the effect of their post or app design up front. One can only iterate on the data they collect from users after releasing something
Thank you too! I would like to add you on Linked In but couldn’t find you😂 If you want to connect please add me “Maher Santina”

It seems like the lessons from Y2K have been forgotten. While I realize that it might seem silly to ask for the full 4 digits of the expiration year, I think if you asked someone that lived through the Y2K debacle it would send shivers down their spine not to. Am I correct that if this had been coded for a 4-digit year this problem never would have occurred?

Replies

Yes, if it asked for four digits for year, autofill would have worked properly.

From the department of famous last words, I’m not certain how much we need to worry about an online credit card form still being in use 80 years from now.

Credit cards expire frequently which means you don’t have an existing data problem. Credit card processors can handle two or four digits so if in 2090 you were still collecting two digits, they could change it to four digits until they passed 2100 and then switch back to two digits without too much work.

That is assuming Chipotle still exists in 80 years, the form is still in use and we’re still using credit cards.

Again, famous last words.

This is AngularJS, not Angular.

Replies

For those who, like me, may be unfamiliar with this distinction, in 2017, the Angular community decided that Angular 1.0 would be called “AngularJS” and everything after 1.0 would be “Angular.”

Thanks for the clarification about what version is being used here.

I would clarify that ‘pattern’ attribute is for input validation, whereas an input mask actually restricts typing invalid characters. So in theory (ie if implemented correctly, which was not the case here) the masking approach will reduce errors, which will likely increase conversions. With pattern I can still type “;;;;;hash” into a field that wants 4 numeric digits. So I’m suggesting changing the mask to “9999” will be a better, preventative, approach and maybe net Chipotle a couple extra milli. (Especially if they accept 4 digit and 2 digit years both as valid which should be easy enough on the backend)

I love the idea of the HTML form stuff but unfortunately the implementation varies so much from browser to browser that it often ends up a lil half baked. Max length is reliable though and very handy way of enforcing a limit without JS shenanigans. it’s really a shame it’s not supported by number inputs but max=9999 can achieve the same thing.

Reply to Michael’s Comment

Please be kind, courteous and constructive. You may use simple HTML or Markdown in your comments. All fields are required.


Let’s discuss your project! Email Us