A button has no inherent line count. All it has is the width left over for it and the length of the content it has to hold, and the line count is what those two produce when they meet. On one Shopify product page that meeting landed on three: the buy button's label broke into three lines on an iPhone 12 Pro, at 390 pixels wide.
The buy box was custom built rather than a theme default, because the theme itself was built from scratch. The page put the product gallery on the left and the buy box on the right, holding the variant picker, the add to cart button, and the subscription option.
Where the number three comes from
The buy row was a single row, display: flex, with two children: a quantity stepper on the left at a fixed width, then the add to cart button absorbing the rest with flex: 1.
That fixed width is recorded as roughly 116 pixels, and the number can be traced back to the stepper's own contents. Two buttons of 40 pixels flank a 36 pixel input, so 40 + 36 + 40 = 116, and the 1.5 pixel border on each side is why the figure is written as roughly.
From there the arithmetic runs straight. A 390 pixel viewport minus the 116 pixel stepper leaves 274 pixels for the button. Even that is not the final figure, because there is still a gap between the two elements whose value comes from the theme's spacing token, so the real room for the button is a little tighter than 274, not wider.
What got packed into that room was not a single piece of text. The button label had two children, the words add to cart on the left and the product price on the right, pushed apart with justify-content: space-between, and there was no white-space: nowrap anywhere in that chain.
The rest is the most ordinary browser behaviour there is, which is to break text that does not fit at the nearest spaces. The row itself was never given flex-wrap, so what folded was not the row, it was the text inside the button.
And because the break point is born out of that arithmetic, three is not a property of the button. Add a few characters to the price, or take a little off the available width, and the next chunk moves down to a new line. A layout whose line count is decided by its content cannot be settled by chasing the number three.
The first attempt, which I threw out myself
What I worked on first was giving the button more room. If one row cannot hold two elements, split the row on mobile: stepper on the first row, full width add to cart on the second.
The wrapping should have gone, but the shape was awkward. The small stepper stood alone on the left with empty space beside it, while the add to cart button went flat across the bottom row. I rejected that version myself.
The reason was not only taste. That media query never touched why the label was too long, it only moved the consequence into a different shape.
The same price, three times on one screen
What finally opened the case was not a new CSS property, it was counting how many times that product price appeared on the same screen.
The first appearance is in the price row above the buy box, where a price belongs. The second is in the purchase option cards, each of which prints its own price. The third is inside the add to cart label.
That third appearance is what the notes record as the root cause: redundant content forcing a fragile layout. That narrow strip was not being spent on information anyone needed, it was being spent on a number the visitor had already read twice on the same screen.
The fix that shipped
The first step was deleting, not writing. The price span inside the button was removed from the template entirely, and the single row layout was kept exactly as it was.
The CSS then only had to tidy up after it:
.buy-row__cta {
flex: 1;
min-width: 0;
justify-content: center;
letter-spacing: 0.06em;
white-space: nowrap;
}
.buy-row__cta [data-label] {
white-space: nowrap;
}
.buy-row__stepper {
flex-shrink: 0;
}justify-content went from space-between to center, because pushing two elements apart only makes sense when there are two of them. With a single label, center is the right answer. white-space: nowrap itself is set twice, on the button and again on the label child inside it.
The remaining two declarations keep the split of space in that row explicit. min-width: 0 on the button drops the auto default that flex items carry, the one that refuses to shrink below the content size and pushes the whole row wider instead. flex-shrink: 0 on the stepper says that its fixed width really is fixed, so when space gets tight the button is what adapts, and the stepper is not dragged along and squashed.
The CSS rule for that price span went out with it, since the element no longer existed. Searching the theme for that class name now returns nothing, and the button markup really is down to a single label span.
The fix was pushed straight to the live theme through the Shopify CLI. That sounds reckless, but the storefront was still password locked at the time, and the live theme was also the one I had open in preview. I checked it myself once it landed and it was fine. The client's reply had not come in by the time that session closed.
Deleting a duplicate is cheaper than rearranging
What I took away from this case is not about flexbox. If a piece of information already appears in the context that surrounds it, do not repeat it in the button label. That repetition is what makes the layout fragile, and removing the duplicate is safer than restacking the mobile layout.
The order of checks changed for me too. Before adding a breakpoint to make room for something, count how many times that something already appears on the same screen. If the answer is more than once, width may not be the thing that needs fixing.