Shopify Builds>LUS Brands>"Start with the basics" — a merchandiser-authored 3-step routine section with measured read-more clamping

"Start With the Basics" — a Merchandiser-Authored 3-Step Routine Section With Measured Read-More Clamping

A homepage routine band whose three step cards are theme-editor blocks tied to real products, whose heading takes a [[...]] highlight token, and whose "Read more" button is unhidden only after the browser has measured that the copy overflows its four-line clamp.

Liquid renders on the server and can't count the lines a paragraph will occupy, so a server-side "read more" either always shows or always hides. This homepage routine section makes the browser decide: each step card's description is clamped to four lines, measured after layout settles, and the button appears only where the copy actually overflows. Steps, products, images and the highlighted heading are all authored in the theme editor.

The Problem

Liquid can't measure text. A section rendered on Shopify's servers knows the characters in a setting, not the lines they'll occupy at a given viewport width in a given font. So a "read more" decided in Liquid is a guess: it either shows on every card, including the ones with two lines of copy, or it never shows and long copy is silently cut.

That mattered here because curl shoppers arrive not knowing what to buy first, and LUS's answer is a routine — cleanse, condition, style. The homepage needed a band that teaches the three-step system, shows each step's real product and price, links each card to that product, and lets one step be promoted as the routine's hero. The benefit lines under each step are rewritten by the marketing team and vary in length, and yet the three cards have to sit at the same height on desktop.

The Constraint

The whole section had to be authorable in the theme editor by people who don't write code: 24 schema entries, 19 of them controls, plus a repeatable step block, with no build step behind it. It had to render as a full-bleed background image band inside Dawn 15.4.1's page-width layout, where the theme's own sections stop at the content column.

The cards had to stay the same height on desktop whether a step's copy runs two lines or six, which rules out letting the paragraph size the card. The section had to carry no extra asset request — its CSS and script ship inline. The theme editor re-renders a section on every settings change, so anything the script binds has to survive being bound again. And there was no way to get a line count out of Liquid, so the clamp decision had to move to the client entirely.

What We Built

One section file, start-with-basics.liquid — roughly 1,100 lines with its stylesheet and schema — with a repeatable step block carrying step_label, step_number, heading, description, product, custom_image, bundle_text, a highlighted checkbox and a read_more_color. On desktop the steps render as a flex row with align-items: stretch; on mobile they stack. A highlighted card swaps to the brand pink with white type, which is how the all-in-one styler is promoted as the routine's hero without a separate block type.

Each card's image comes from custom_image and falls back to the picked product's featured_image; the card links to the product's URL, and shows its price with an optional bundle_text line beside it.

The heading highlight. The merchandiser types Start with the [[Essentials]]. Liquid splits the heading on [[, then splits the remainder on ]], and wraps the inner run in a start-with-basics__heading-highlight span. The same bracket convention is read by de_multicolumn, curl-journey-cards and the shared highlight-text snippet, so it's a theme-wide way of authoring inline emphasis rather than something this section invented.

The measured read-more. The server renders every description with a --clamped class (-webkit-line-clamp: 4) and a hidden button. An inline IIFE scoped to #StartWithBasics-{{ section.id }} then runs syncReadMoreWrap on every [data-swb-desc-wrap]: it applies the clamp class, compares the paragraph's scrollHeight to its clientHeight with a one-pixel tolerance, and unhides the button only where the text overflows. The sync is scheduled through a double requestAnimationFrame, run again on window.load, and re-run on resize through a 150ms debounce. After the first pass the root gets data-swb-ready="true".

The expand animation. animateHeight measures the paragraph's height in its clamped state, flips the classes to the expanded state, measures again, then sets an explicit height and transitions it over 0.35s with cubic-bezier(0.25, 1, 0.5, 1). It listens for transitionend filtered to the height property on the paragraph itself, and arms a 400ms fallback timer; finishAnimation is idempotent through a finished flag, and the button is disabled for the duration.

On desktop the description block is a fixed calc(4 * 2.8rem + 2.6rem) — four lines plus the read-more row — so the three cards' text areas are identical whether or not a card ends up with a button.

Why This Way

Measuring in the browser is the only way to know whether a clamp is needed, and re-measuring on resize is the only way to stay right across breakpoints. The double requestAnimationFrame exists because a measurement taken before fonts and layout settle is the reason clamp-based read-more buttons flicker on and off; waiting two frames, then measuring again on load, catches the late web font.

The height is animated between two measured values instead of a max-height guess because both end states are known: the easing lands exactly on the content height with no dead travel at the end. Keeping the CSS in {% stylesheet %}, the per-instance spacing in {% style %} and the script inline means the section costs no extra request.

There's a price for deciding after paint. On a slow connection there's a frame where the clamp exists and the button doesn't, and the sync runs at least twice on every load. The state also lives in a trio of classes — clamped, animating, expanded — that the stylesheet and the script must agree on; rename one side and the other stops working without an error. And every measure pass forces layout on each card.

Implementation Notes

  • A section-id-scoped {% style %} block writes desktop and mobile padding and margin from eight range settings, plus the background image's opacity from background_opacity | divided_by: 100.0.
  • The background renders through image_url with format: 'webp' at widths 390, 750, 1100 and 1440 and sizes: 100vw, positioned absolutely behind a page-width inner wrapper, which is how the band bleeds full width inside Dawn's content column.
  • The highlight parser is two split calls, on [[ and then ]], so a heading with no token passes through unchanged and one with a token gets a single span.
  • root.dataset.swbReadMoreBound and btn.dataset.swbReadMoreBound guard against double binding when the theme editor re-renders the section without a full page load.
  • read_more_color per block defaults to #ffffff on a highlighted card and #de7f88 otherwise, and reaches the button through a --swb-read-more-color custom property on the wrapper.
  • The "Read more" and "Read less" labels are section settings passed into the script through the json filter and swapped alongside aria-expanded, so the text is editable without touching the script.
  • The preset ships three steps — Cleanse, Condition, Style — with the third highlighted; the live homepage instance links real product handles and custom images.

Edge Cases

  • A step with no description renders a --empty wrapper and no read-more button at all, so the measure pass has nothing to inspect.
  • Image alt text falls back from the image's own alt to the block heading to the product title to "Product"; the background alt falls back to the section heading, then "Background".
  • The product link falls back to # when no product is picked, so the card image stays a clickable-safe anchor.
  • clearDescAnimation removes the transitionend listener and clears the fallback timer before a new animation starts, so a rapid double click can't strand a card mid-transition.
  • Buttons are disabled during the animation and re-enabled in finishAnimation, which runs once even if both the event and the timer fire.
  • The resize sync skips a wrapper that's mid-animation, and leaves an expanded card expanded — it only refreshes the label and aria-expanded.

Platform Primitives Used

  • Liquid section schema and settings — 24 schema entries, 19 of them controls, for the band, its copy, its buttons and its spacing, plus the preset that seeds the three steps.
  • Theme blocks — the repeatable step block is the unit a merchandiser adds, reorders and points at a product.

Where It Runs

On the home page, through one template: index. The live instance carries three steps, each linked to a product and each with its own custom image, under a heading that uses the highlight token. The section is a preset, so a merchandiser could add a second instance to another page without a code change.

What This Demonstrates

  • Product education, how-to and routine content — the primary capability: a routine taught as three steps, each a real product, authored in the theme editor. The engineering underneath is progressive disclosure decided by measurement rather than by a setting.

How We Know

One section file read from the theme, roughly 1,100 lines, and four client working-session records that discuss the homepage content treatment this band belongs to. The records establish what was being asked for; they don't describe the clamp, so the measure-then-animate mechanism on this page is documented from the code. A live check of the storefront's homepage on 2026-09-07 found the section rendering three product-linked steps under a highlighted heading, with the read-more controls in place.

Related Builds

Provenance & Evidence

  • Client: LUS Brands — loveurcurls.com
  • Surface: Home page
  • Templates served: one — index
  • Complexity: Medium — one section, with its stylesheet, script and schema in the same file
  • Attribution: Deploi-authored. The section, its block schema, its stylesheet and its read-more script are ours. It runs inside a theme based on Dawn 15.4.1, which we did not author.
  • Status: Live, verified 2026-09-07
  • Evidence: One section file, plus four client working-session records and a live homepage check
  • Confidence: Strong — the code is read directly from the theme and the client records place the work; the mechanism is documented from the code
  • Primary capability: Product education, how-to and routine content

Ready to Teach the Routine Before the Sale?

If your product page assumes the shopper already knows what to buy first, your homepage is doing less than it could. Contact us today and we'll show you what a routine your team can rewrite in the theme editor looks like on your storefront.

More builds