# Featured product: free Shopify section

> One product with its gallery, price, variant buttons, quantity and add to cart on any page. Works without JavaScript; every shopper-facing word is editable.

Featured product is a free Shopify section that puts one product you choose on any page, with its image gallery, price and sale label, variant buttons, quantity and an add to cart button. The form posts to Shopify's cart, so it works even without JavaScript, and price, availability and image follow the chosen variant. It works in Online Store 2.0 themes without an app.

- **Category:** Product and collection discovery
- **Use on:** Home, Landing pages, Collection, Any page
- **Reads:** One product
- **Size:** 41 KB in 2 files, with a small script
- **License:** MIT, © 2026 Lintel
- **Page:** https://lintelagency.com/resources/shopify-sections/featured-product
- **Live demo:** https://lintelagency.com/resources/shopify-sections/demo/lib-featured-product.html
- **Added:** 2026-11-02

## What it’s for

- Hero product or best seller on the home page, buyable without leaving the page
- Launch or campaign landing page built around a single product
- Gift guide, blog or About page that sells the product it talks about

## Editor settings

In the theme editor the section is called “Lib · Featured product”. IDs, types, labels and defaults come from the section’s schema; the labels are what the theme editor shows.

### Product

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `product` | `product` | Product | – |  |
| `show_vendor` | `checkbox` | Show vendor | `true` |  |
| `heading_tag` | `select` | Product title tag | h2 | `h1`, `h2`, `h3`, `p` |
| `heading_size` | `select` | Product title size | medium | `small`, `medium`, `large` |
| `show_description` | `checkbox` | Show product description | `true` |  |
| `show_quantity` | `checkbox` | Show quantity selector | `true` |  |

### Gallery

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `show_thumbnails` | `checkbox` | Show thumbnails | `true` |  |
| `media_ratio` | `select` | Image shape | square | `adapt`, `square`, `portrait`, `landscape` |
| `corner_radius` | `range` | Corner radius | `12` | 0–32 px, step 2 |
| `details_label` | `text` | Details link label | View full details |  |
| `details_style` | `select` | Details link style | text | `primary`, `outline`, `text` |
| `details_new_tab` | `checkbox` | Open details link in a new tab | `false` |  |

### Shopper-facing words

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `button_label` | `text` | Add to cart button | Add to cart |  |
| `sold_out_label` | `text` | Sold out | Sold out |  |
| `unavailable_label` | `text` | Unavailable combination | Unavailable |  |
| `sale_badge_label` | `text` | Sale badge | Sale |  |
| `sale_price_label` | `text` | Sale price (screen readers) | Sale price |  |
| `regular_price_label` | `text` | Regular price (screen readers) | Regular price |  |
| `quantity_label` | `text` | Quantity (screen readers) | Quantity |  |
| `decrease_label` | `text` | Decrease quantity (screen readers) | Decrease quantity |  |
| `increase_label` | `text` | Increase quantity (screen readers) | Increase quantity |  |
| `thumbnail_label` | `text` | Thumbnail button (screen readers) | Show image |  |

### Layout

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `media_position` | `select` | Gallery position on desktop | left | `left`, `right` |
| `media_width` | `select` | Gallery width on desktop | half | `half`, `wide` |
| `mobile_order` | `select` | Order on mobile | media_first | `media_first`, `info_first` |
| `section_width` | `select` | Width | page | `page`, `full` |
| `text_alignment` | `select` | Text alignment on desktop | left | `left`, `center` |
| `text_alignment_mobile` | `select` | Text alignment on mobile | left | `left`, `center` |

### Colors

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `color_bg` | `color` | Background | #FBF8F4 |  |
| `color_text` | `color` | Text | #231F1B |  |
| `color_accent` | `color` | Add to cart button, sale price and badge | #7C2D12 |  |
| `color_accent_text` | `color` | Text on button and sale badge | #FFFFFF |  |
| `color_border` | `color` | Borders | #D9D2C8 |  |

### Spacing, visibility and motion

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `padding_top` | `range` | Top padding on desktop | `80` | 0–200 px, step 4 |
| `padding_bottom` | `range` | Bottom padding on desktop | `80` | 0–200 px, step 4 |
| `padding_top_mobile` | `range` | Top padding on mobile | `40` | 0–160 px, step 4 |
| `padding_bottom_mobile` | `range` | Bottom padding on mobile | `48` | 0–160 px, step 4 |
| `show_on` | `select` | Show on | all | `all`, `desktop`, `mobile` |
| `anchor_id` | `text` | Anchor ID | – |  |
| `animate` | `checkbox` | Fade in on scroll | `false` |  |

## Install

### In Shopify admin

1. Work on a copy, not your live theme: in Shopify admin go to **Online Store > Themes**, open the **…** menu on your current theme and choose **Duplicate**.
2. On the copy, open the **…** menu and choose **Edit code**.
3. In the `sections` folder, add a new file named `lib-featured-product.liquid` and paste the contents of `sections/lib-featured-product.liquid` from the source below. Save.
4. In the `snippets` folder, add `lib-button.liquid` the same way. If a snippet is already there from another Lib section, it is the same file; keep one copy.
5. Open **Customize** on the copy, choose **Add section** and pick **Lib · Featured product**. Replace the sample content with your own and set the colors.
6. Preview the copy, then publish it when it looks right.

### With Shopify CLI

From your local theme folder, download the files and push only them to an unpublished theme (find its ID with `shopify theme list`):

```sh
curl -fsSL -o sections/lib-featured-product.liquid https://lintelagency.com/resources/shopify-sections/files/lib-featured-product.liquid
curl -fsSL -o snippets/lib-button.liquid https://lintelagency.com/resources/shopify-sections/files/lib-button.liquid
shopify theme push --theme <unpublished-theme-id> --only sections/lib-featured-product.liquid --only snippets/lib-button.liquid
```

Never push to the live theme: always pass the ID of an unpublished theme and never use `--live` or `--allow-live`. Then add the section in the theme editor as above.

## Files

- [sections/lib-featured-product.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-featured-product.liquid): goes in `sections/`
- [snippets/lib-button.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-button.liquid): goes in `snippets/`

## Questions

### Can shoppers add to cart without opening the product page?

Yes. The section contains a real product form that posts the chosen variant and quantity to /cart/add, so the item lands in the cart and Shopify takes the shopper to the cart as usual. A "View full details" link to the product page is optional.

### How are sold-out variants shown?

Sold-out options stay visible and can still be picked: they are struck through and screen readers hear "sold out" after the value. When the chosen variant is sold out the button reads "Sold out", is disabled, and a sold-out label shows next to the price. Combinations that do not exist show "Unavailable".

### Does it work without JavaScript?

Yes. Without JavaScript the options are one menu of every variant with its price, and sold-out variants are disabled. With JavaScript the menu becomes option buttons, and the price, compare-at price, sale label, availability and main image update for the chosen variant; the page URL is left alone.

### Does it show reviews or stock levels?

No. It only shows what the product itself has: images, title, vendor, price, compare-at price, variants, availability and description. Nothing is invented, such as ratings, low-stock counts or viewer numbers.

### Can I translate the button and labels?

Every word a shopper sees or hears is a setting: Add to cart, Sold out, Unavailable, Sale, the sale and regular price labels, the quantity buttons and the thumbnail buttons. Change them in the theme editor or translate them with Translate & Adapt.

## Source

### sections/lib-featured-product.liquid

```liquid
{%- comment -%}
  MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections
{%- endcomment -%}
{%- comment -%}
  Featured product: one product you pick, with its image gallery, title, price, variant picker,
  quantity and add to cart, for a home page or landing page. The form posts to /cart/add, so it
  works without JavaScript (a plain variant menu); with JavaScript the options become buttons and
  the price, sale label, availability and image follow the chosen variant. Every word a shopper
  sees is a setting. Reads the product only; no ratings, stock counts or other made-up data.
  Depends on: snippets/lib-button.liquid
{%- endcomment -%}

{%- liquid
  assign s = section.settings
  assign p = s.product
  assign heading_tag = s.heading_tag | default: 'h2'
  assign uid = 'lib-fp-' | append: section.id
  assign eager = false
  if section.index <= 2
    assign eager = true
  endif

  assign render_section = true
  if p == blank and request.design_mode == false
    assign render_section = false
  endif

  if p != blank
    assign current = p.selected_or_first_available_variant
    assign price = current.price | default: p.price
    assign compare = current.compare_at_price | default: 0
    assign on_sale = false
    if compare > price
      assign on_sale = true
    endif
    assign has_options = true
    if p.has_only_default_variant or p.variants.size < 2
      assign has_options = false
    endif
    assign first_media = p.media | where: 'media_type', 'image' | first
    assign option_names = p.options_with_values | map: 'name' | join: ' / '
  endif

  assign media_sizes = '(min-width: 990px) 50vw, 100vw'
  if s.media_width == 'wide'
    assign media_sizes = '(min-width: 990px) 58vw, 100vw'
  endif
-%}

{%- if render_section -%}
<lib-featured-product
  class="lib-featured-product lib-featured-product--{{ s.section_width }}"
  {% if s.anchor_id != blank %}
    id="{{ s.anchor_id | handleize }}"
  {% endif %}
  data-show="{{ s.show_on }}"
  data-media-position="{{ s.media_position }}"
  data-media-width="{{ s.media_width }}"
  data-mobile-order="{{ s.mobile_order }}"
  data-ratio="{{ s.media_ratio }}"
  {% if s.animate %}
    data-animate
  {% endif %}
  style="
    --lib-bg: {{ s.color_bg }};
    --lib-text: {{ s.color_text }};
    --lib-accent: {{ s.color_accent }};
    --lib-accent-text: {{ s.color_accent_text }};
    --lib-border: {{ s.color_border }};
    --lib-radius: {{ s.corner_radius }}px;
    {% if first_media != blank %}--lib-ratio: {{ first_media.preview_image.width }} / {{ first_media.preview_image.height }};{% endif %}
    --lib-pt: {{ s.padding_top }}px;
    --lib-pb: {{ s.padding_bottom }}px;
    --lib-pt-m: {{ s.padding_top_mobile }}px;
    --lib-pb-m: {{ s.padding_bottom_mobile }}px;
  "
>
  <div class="lib-featured-product__inner">
    <div class="lib-featured-product__media">
      {%- if p != blank and p.media.size > 0 -%}
        <div class="lib-featured-product__stage">
          {%- assign shown = 0 -%}
          {%- for media in p.media -%}
            {%- if media.media_type != 'image' -%}{%- continue -%}{%- endif -%}
            {%- liquid
              assign loading = 'lazy'
              assign priority = 'auto'
              if shown == 0 and eager
                assign loading = 'eager'
                assign priority = 'high'
              endif
            -%}
            <div
              class="lib-featured-product__slide"
              data-media-id="{{ media.id }}"
              data-index="{{ shown }}"
              {% if shown > 0 %}
                hidden
              {% endif %}
            >
              {{
                media.preview_image
                | image_url: width: 1600
                | image_tag:
                  class: 'lib-featured-product__img',
                  loading: loading,
                  fetchpriority: priority,
                  sizes: media_sizes,
                  widths: '400, 600, 800, 1000, 1200, 1600',
                  alt: media.alt
              }}
            </div>
            {%- assign shown = shown | plus: 1 -%}
          {%- endfor -%}
        </div>

        {%- if s.show_thumbnails and shown > 1 -%}
          <ul class="lib-featured-product__thumbs" role="list" data-thumbs hidden>
            {%- assign thumb = 0 -%}
            {%- for media in p.media -%}
              {%- if media.media_type != 'image' -%}{%- continue -%}{%- endif -%}
              <li>
                <button
                  type="button"
                  class="lib-featured-product__thumb"
                  data-show-index="{{ thumb }}"
                  aria-label="{{ s.thumbnail_label | escape }} {{ thumb | plus: 1 }}"
                  {% if thumb == 0 %}
                    aria-current="true"
                  {% endif %}
                >
                  {{
                    media.preview_image
                    | image_url: width: 160
                    | image_tag: class: 'lib-featured-product__thumb-img', loading: 'lazy', sizes: '80px', alt: ''
                  }}
                </button>
              </li>
              {%- assign thumb = thumb | plus: 1 -%}
            {%- endfor -%}
          </ul>
        {%- endif -%}
      {%- else -%}
        <div class="lib-featured-product__stage">
          <div class="lib-featured-product__slide">
            {{ 'product-1' | placeholder_svg_tag: 'lib-featured-product__img lib-featured-product__placeholder' }}
          </div>
        </div>
      {%- endif -%}
    </div>

    <div class="lib-featured-product__info" data-align="{{ s.text_alignment }}" data-align-m="{{ s.text_alignment_mobile }}">
      {%- if p == blank -%}
        {%- comment -%} Only reachable in the theme editor (no product chosen yet). {%- endcomment -%}
        <p class="lib-featured-product__vendor">Choose a product in the section settings</p>
        <{{ heading_tag }} class="lib-featured-product__title lib-featured-product__title--{{ s.heading_size }}">
          Your product's name
        </{{ heading_tag }}>
      {%- else -%}
        {%- if s.show_vendor and p.vendor != blank -%}
          <p class="lib-featured-product__vendor">{{ p.vendor | escape }}</p>
        {%- endif -%}

        <{{ heading_tag }} class="lib-featured-product__title lib-featured-product__title--{{ s.heading_size }}">
          {{- p.title | escape -}}
        </{{ heading_tag }}>

        <div class="lib-featured-product__price" aria-live="polite" aria-atomic="true">
          <span class="lib-featured-product__sr" data-sale-label{% unless on_sale %} hidden{% endunless %}>{{ s.sale_price_label | escape }}</span>
          <span class="lib-featured-product__now{% if on_sale %} is-sale{% endif %}" data-price>{{ price | money }}</span>
          <span class="lib-featured-product__sr" data-was-label{% unless on_sale %} hidden{% endunless %}>{{ s.regular_price_label | escape }}</span>
          <s class="lib-featured-product__was" data-compare{% unless on_sale %} hidden{% endunless %}>
            {%- if on_sale -%}{{ compare | money }}{%- endif -%}
          </s>
          <span class="lib-featured-product__badge" data-badge-sale{% unless on_sale %} hidden{% endunless %}>{{ s.sale_badge_label | escape }}</span>
          <span class="lib-featured-product__badge lib-featured-product__badge--muted" data-badge-sold{% if current.available %} hidden{% endif %}>{{ s.sold_out_label | escape }}</span>
        </div>

        {%- form 'product', p, id: uid, class: 'lib-featured-product__form', novalidate: 'novalidate' -%}
          <input type="hidden" name="id" value="{{ current.id }}" data-variant-id{% if has_options %} disabled{% endif %}>

          {%- if has_options -%}
            {%- comment -%} Without JavaScript: one menu of every variant. {%- endcomment -%}
            <div class="lib-featured-product__fallback" data-fallback>
              <label class="lib-featured-product__label" for="{{ uid }}-variant">{{ option_names | escape }}</label>
              <select id="{{ uid }}-variant" class="lib-featured-product__select" name="id">
                {%- for variant in p.variants -%}
                  <option
                    value="{{ variant.id }}"
                    {% if variant.id == current.id %}
                      selected
                    {% endif %}
                    {% unless variant.available %}
                      disabled
                    {% endunless %}
                  >
                    {{- variant.title | escape }} · {{ variant.price | money -}}
                    {%- unless variant.available %} · {{ s.sold_out_label | escape }}{% endunless -%}
                  </option>
                {%- endfor -%}
              </select>
            </div>

            {%- comment -%}
              With JavaScript: a group of buttons per option. Their form attribute names no form, so
              they are never posted; only the variant id is.
            {%- endcomment -%}
            {%- for option in p.options_with_values -%}
              {%- assign option_index = forloop.index0 -%}
              <fieldset class="lib-featured-product__option" data-option hidden>
                <legend class="lib-featured-product__label">
                  {{- option.name | escape -}}
                </legend>
                <div class="lib-featured-product__values">
                  {%- for value in option.values -%}
                    {%- liquid
                      assign value_available = false
                      for variant in p.variants
                        if variant.options[option_index] == value and variant.available
                          assign value_available = true
                          break
                        endif
                      endfor
                    -%}
                    <span class="lib-featured-product__choice"{% unless value_available %} data-sold-out{% endunless %}>
                      <input
                        type="radio"
                        class="lib-featured-product__radio"
                        id="{{ uid }}-{{ option.position }}-{{ forloop.index }}"
                        name="{{ uid }}-option-{{ option.position }}"
                        value="{{ value | escape }}"
                        form="{{ uid }}-options"
                        {% if option.selected_value == value %}
                          checked
                        {% endif %}
                      >
                      <label class="lib-featured-product__value" for="{{ uid }}-{{ option.position }}-{{ forloop.index }}">
                        {{- value | escape -}}
                        <span class="lib-featured-product__sr" data-sold-out-text{% if value_available %} hidden{% endif %}>, {{ s.sold_out_label | escape }}</span>
                      </label>
                    </span>
                  {%- endfor -%}
                </div>
              </fieldset>
            {%- endfor -%}
          {%- endif -%}

          <div class="lib-featured-product__buy">
            {%- if s.show_quantity -%}
              <div class="lib-featured-product__qty-wrap">
                <label class="lib-featured-product__sr" for="{{ uid }}-qty">{{ s.quantity_label | escape }}</label>
                <div class="lib-featured-product__qty">
                  <button type="button" class="lib-featured-product__step" data-step="-1" aria-label="{{ s.decrease_label | escape }}" hidden>
                    <svg viewBox="0 0 12 12" width="12" height="12" aria-hidden="true" focusable="false"><path d="M2.5 6h7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
                  </button>
                  <input
                    id="{{ uid }}-qty"
                    class="lib-featured-product__qty-input"
                    type="number"
                    name="quantity"
                    value="1"
                    min="1"
                    step="1"
                    inputmode="numeric"
                  >
                  <button type="button" class="lib-featured-product__step" data-step="1" aria-label="{{ s.increase_label | escape }}" hidden>
                    <svg viewBox="0 0 12 12" width="12" height="12" aria-hidden="true" focusable="false"><path d="M2.5 6h7M6 2.5v7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
                  </button>
                </div>
              </div>
            {%- endif -%}
            <button
              type="submit"
              name="add"
              class="lib-featured-product__add"
              data-add
              data-label="{{ s.button_label | escape }}"
              data-sold-out="{{ s.sold_out_label | escape }}"
              data-unavailable="{{ s.unavailable_label | escape }}"
              {% unless current.available %}
                disabled
              {% endunless %}
            >
              {%- if current.available -%}{{ s.button_label | escape }}{%- else -%}{{ s.sold_out_label | escape }}{%- endif -%}
            </button>
          </div>
        {%- endform -%}

        {%- if s.show_description and p.description != blank -%}
          <div class="lib-featured-product__description">{{ p.description }}</div>
        {%- endif -%}

        {%- if s.details_label != blank -%}
          <div class="lib-featured-product__details">
            {%- render 'lib-button',
              label: s.details_label,
              url: p.url,
              style: s.details_style,
              new_tab: s.details_new_tab
            -%}
          </div>
        {%- endif -%}

        <script type="application/json" data-variants>
          [
            {%- for variant in p.variants -%}
              {
                "id": {{ variant.id | json }},
                "options": {{ variant.options | json }},
                "available": {{ variant.available | json }},
                "price": {{ variant.price | money | json }},
                "compare": {% if variant.compare_at_price > variant.price %}{{ variant.compare_at_price | money | json }}{% else %}null{% endif %},
                "media": {% if variant.featured_media != blank %}{{ variant.featured_media.id | json }}{% else %}null{% endif %}
              }{% unless forloop.last %},{% endunless %}
            {%- endfor -%}
          ]
        </script>
      {%- endif -%}
    </div>
  </div>
</lib-featured-product>
{%- endif -%}

{% stylesheet %}
  .lib-featured-product {
    display: block;
    background-color: var(--lib-bg);
    color: var(--lib-text);
    padding: var(--lib-pt) 1rem var(--lib-pb);
  }
  .lib-featured-product__inner {
    display: grid;
    gap: clamp(1.75rem, 4vw, 4rem);
    max-width: 1200px;
    margin: 0 auto;
  }
  .lib-featured-product--full .lib-featured-product__inner { max-width: none; }
  .lib-featured-product__inner > * { min-width: 0; }
  @media (max-width: 749px) {
    .lib-featured-product { padding-top: var(--lib-pt-m); padding-bottom: var(--lib-pb-m); }
    .lib-featured-product[data-show='desktop'] { display: none; }
  }
  @media (min-width: 750px) {
    .lib-featured-product[data-show='mobile'] { display: none; }
  }
  @media (max-width: 989px) {
    .lib-featured-product[data-mobile-order='info_first'] .lib-featured-product__info { order: -1; }
  }
  @media (min-width: 990px) {
    .lib-featured-product__inner { grid-template-columns: 1fr 1fr; align-items: start; }
    .lib-featured-product[data-media-width='wide'] .lib-featured-product__inner { grid-template-columns: 7fr 5fr; }
    .lib-featured-product[data-media-position='right'] .lib-featured-product__media { order: 2; }
    .lib-featured-product[data-media-position='right'][data-media-width='wide'] .lib-featured-product__inner { grid-template-columns: 5fr 7fr; }
    .lib-featured-product__info { position: sticky; top: 2rem; }
  }

  /* Gallery: one image at a time, thumbnails underneath. */
  .lib-featured-product__stage {
    position: relative;
    overflow: hidden;
    border-radius: var(--lib-radius);
    background: color-mix(in srgb, var(--lib-text) 6%, var(--lib-bg));
    aspect-ratio: 1;
  }
  .lib-featured-product[data-ratio='portrait'] .lib-featured-product__stage { aspect-ratio: 4 / 5; }
  .lib-featured-product[data-ratio='landscape'] .lib-featured-product__stage { aspect-ratio: 4 / 3; }
  .lib-featured-product[data-ratio='adapt'] .lib-featured-product__stage { aspect-ratio: var(--lib-ratio, 1); }
  .lib-featured-product__slide { position: absolute; inset: 0; }
  .lib-featured-product__slide[hidden] { display: none; }
  .lib-featured-product__img { display: block; width: 100%; height: 100%; object-fit: cover; }
  .lib-featured-product__placeholder { fill: color-mix(in srgb, var(--lib-text) 25%, transparent); }
  .lib-featured-product__thumbs {
    display: flex;
    gap: 0.625rem;
    margin: 0.875rem 0 0;
    padding: 0.25rem;
    list-style: none;
    overflow-x: auto;
    scrollbar-width: thin;
  }
  .lib-featured-product__thumbs[hidden] { display: none; }
  .lib-featured-product__thumb {
    display: block;
    width: 4.5rem;
    height: 4.5rem;
    padding: 0;
    border: 1px solid var(--lib-border);
    border-radius: calc(var(--lib-radius) / 2);
    overflow: hidden;
    background: color-mix(in srgb, var(--lib-text) 6%, var(--lib-bg));
    cursor: pointer;
    opacity: 0.7;
    transition: opacity 0.2s ease, border-color 0.2s ease;
  }
  .lib-featured-product__thumb:hover { opacity: 1; }
  .lib-featured-product__thumb[aria-current='true'] { opacity: 1; border-color: var(--lib-text); box-shadow: inset 0 0 0 1px var(--lib-text); }
  .lib-featured-product__thumb:focus-visible { outline: 2px solid var(--lib-text); outline-offset: 2px; }
  .lib-featured-product__thumb-img { display: block; width: 100%; height: 100%; object-fit: cover; }

  /* Info column and alignment: desktop, then mobile override. */
  .lib-featured-product__info { display: flex; flex-direction: column; align-items: flex-start; text-align: left; }
  .lib-featured-product__info[data-align='center'] { align-items: center; text-align: center; }
  @media (max-width: 749px) {
    .lib-featured-product__info[data-align-m='left'] { align-items: flex-start; text-align: left; }
    .lib-featured-product__info[data-align-m='center'] { align-items: center; text-align: center; }
  }
  .lib-featured-product__info > * { margin: 0; }
  .lib-featured-product__vendor {
    margin-bottom: 0.75rem;
    font-size: 0.8rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    opacity: 0.75;
  }
  .lib-featured-product__title {
    font-size: clamp(1.75rem, 3vw, 2.5rem);
    line-height: 1.1;
    letter-spacing: -0.02em;
    text-wrap: balance;
  }
  .lib-featured-product__title--small { font-size: clamp(1.375rem, 2vw, 1.75rem); }
  .lib-featured-product__title--large { font-size: clamp(2rem, 4vw, 3.25rem); }
  .lib-featured-product__price {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.25rem 0.625rem;
    margin-top: 1rem;
    font-size: 1.25rem;
  }
  .lib-featured-product__info[data-align='center'] .lib-featured-product__price { justify-content: center; }
  @media (max-width: 749px) {
    .lib-featured-product__info[data-align-m='left'] .lib-featured-product__price { justify-content: flex-start; }
    .lib-featured-product__info[data-align-m='center'] .lib-featured-product__price { justify-content: center; }
  }
  .lib-featured-product__now { font-weight: 600; }
  .lib-featured-product__now.is-sale { color: var(--lib-accent); }
  .lib-featured-product__was { font-size: 1rem; opacity: 0.65; }
  .lib-featured-product__badge {
    align-self: center;
    padding: 0.3em 0.7em;
    border-radius: 999px;
    background: var(--lib-accent);
    color: var(--lib-accent-text);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.04em;
  }
  .lib-featured-product__badge--muted { background: color-mix(in srgb, var(--lib-text) 12%, transparent); color: var(--lib-text); }
  .lib-featured-product__badge[hidden],
  .lib-featured-product__was[hidden] { display: none; }

  .lib-featured-product__form { display: flex; flex-direction: column; gap: 1.25rem; width: 100%; max-width: 30rem; margin-top: 1.75rem; text-align: left; }
  .lib-featured-product__label { display: block; margin: 0 0 0.5rem; padding: 0; font-size: 0.875rem; font-weight: 600; }
  .lib-featured-product__select {
    width: 100%;
    min-height: 2.75rem;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--lib-border);
    border-radius: calc(var(--lib-radius) / 2);
    background: transparent;
    color: inherit;
    font: inherit;
  }
  .lib-featured-product__option { min-width: 0; margin: 0; padding: 0; border: 0; }
  .lib-featured-product__option[hidden],
  .lib-featured-product__fallback[hidden] { display: none; }
  .lib-featured-product__values { display: flex; flex-wrap: wrap; gap: 0.5rem; }
  .lib-featured-product__choice { position: relative; display: inline-flex; }
  .lib-featured-product__radio { position: absolute; inset: 0; z-index: 1; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
  .lib-featured-product__value {
    display: inline-flex;
    align-items: center;
    min-height: 2.75rem;
    padding: 0.5rem 1.125rem;
    border: 1px solid var(--lib-border);
    border-radius: 999px;
    font-size: 0.9375rem;
    cursor: pointer;
    transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease;
  }
  .lib-featured-product__radio:hover + .lib-featured-product__value { border-color: var(--lib-text); }
  .lib-featured-product__radio:checked + .lib-featured-product__value { background: var(--lib-text); border-color: var(--lib-text); color: var(--lib-bg); }
  .lib-featured-product__radio:focus-visible + .lib-featured-product__value { outline: 2px solid var(--lib-text); outline-offset: 2px; }
  .lib-featured-product__choice[data-sold-out] .lib-featured-product__value { text-decoration: line-through; text-decoration-thickness: 1px; opacity: 0.6; }
  .lib-featured-product__choice[data-sold-out] .lib-featured-product__radio:checked + .lib-featured-product__value { opacity: 0.75; }

  .lib-featured-product__buy { display: flex; flex-wrap: wrap; gap: 0.75rem; }
  .lib-featured-product__qty {
    display: inline-flex;
    align-items: center;
    height: 3rem;
    border: 1px solid var(--lib-border);
    border-radius: 999px;
  }
  .lib-featured-product__step {
    display: grid;
    place-items: center;
    width: 2.75rem;
    height: 100%;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    cursor: pointer;
  }
  .lib-featured-product__step[hidden] { display: none; }
  .lib-featured-product__step:focus-visible { outline: 2px solid var(--lib-text); outline-offset: -4px; border-radius: 999px; }
  .lib-featured-product__qty-input {
    width: 3.5rem;
    height: 100%;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    text-align: center;
    appearance: textfield;
    -moz-appearance: textfield;
  }
  .lib-featured-product__qty-input::-webkit-inner-spin-button,
  .lib-featured-product__qty-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
  .lib-featured-product__qty-input:focus-visible { outline: 2px solid var(--lib-text); outline-offset: -2px; border-radius: 4px; }
  .lib-featured-product__add {
    flex: 1 1 12rem;
    min-height: 3rem;
    padding: 0.85rem 1.5rem;
    border: 0;
    border-radius: 999px;
    background: var(--lib-accent);
    color: var(--lib-accent-text);
    font: inherit;
    font-weight: 600;
    cursor: pointer;
    transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
  }
  .lib-featured-product__add:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 8px 20px -10px rgb(0 0 0 / 0.45); }
  .lib-featured-product__add:focus-visible { outline: 2px solid var(--lib-accent); outline-offset: 3px; }
  .lib-featured-product__add:disabled { cursor: not-allowed; opacity: 0.55; }

  .lib-featured-product__description { margin-top: 1.75rem; max-width: 34rem; font-size: 1rem; line-height: 1.65; opacity: 0.85; }
  .lib-featured-product__description > :first-child { margin-top: 0; }
  .lib-featured-product__description > :last-child { margin-bottom: 0; }
  .lib-featured-product__description a { color: inherit; }
  .lib-featured-product__details { margin-top: 1.5rem; }

  .lib-featured-product__sr {
    position: absolute !important;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
  }
  .lib-featured-product__sr[hidden] { display: none; }

  /* Motion: gallery and info fade up as they scroll into view (scroll-driven animations only). */
  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      .lib-featured-product[data-animate] .lib-featured-product__media,
      .lib-featured-product[data-animate] .lib-featured-product__info > * {
        animation: lib-featured-product-rise linear both;
        animation-timeline: view();
        animation-range: entry 0% entry 50%;
      }
    }
  }
  @keyframes lib-featured-product-rise {
    from { opacity: 0; transform: translateY(20px); }
  }
  @media (prefers-reduced-motion: reduce) {
    .lib-featured-product__add, .lib-featured-product__value, .lib-featured-product__thumb { transition: none; }
    .lib-featured-product__add:hover:not(:disabled) { transform: none; }
  }
{% endstylesheet %}

{% javascript %}
  if (!customElements.get('lib-featured-product')) {
    customElements.define(
      'lib-featured-product',
      class extends HTMLElement {
        connectedCallback() {
          const data = this.querySelector('script[data-variants]');
          this.form = this.querySelector('form');
          if (!data || !this.form) return;
          try {
            this.variants = JSON.parse(data.textContent);
          } catch (error) {
            console.error('lib-featured-product: variant data could not be read', error);
            return;
          }
          this.cleanup = [];
          const listen = (target, type, fn) => {
            target.addEventListener(type, fn);
            this.cleanup.push(() => target.removeEventListener(type, fn));
          };

          this.idInput = this.querySelector('[data-variant-id]');
          this.groups = [...this.querySelectorAll('[data-option]')];
          this.slides = [...this.querySelectorAll('.lib-featured-product__slide[data-media-id]')];
          this.thumbs = [...this.querySelectorAll('[data-show-index]')];
          this.addButton = this.querySelector('[data-add]');

          // Swap the plain variant menu for option buttons.
          const fallback = this.querySelector('[data-fallback]');
          if (fallback && this.groups.length) {
            fallback.hidden = true;
            fallback.querySelector('select').disabled = true;
            this.idInput.disabled = false;
            this.groups.forEach((group) => {
              group.hidden = false;
              listen(group, 'change', () => this.update());
            });
          }

          const thumbList = this.querySelector('[data-thumbs]');
          if (thumbList) thumbList.hidden = false;
          this.thumbs.forEach((thumb) => listen(thumb, 'click', () => this.showSlide(Number(thumb.dataset.showIndex))));

          const qty = this.querySelector('.lib-featured-product__qty-input');
          this.querySelectorAll('[data-step]').forEach((button) => {
            button.hidden = false;
            listen(button, 'click', () => {
              const next = Math.max(1, (parseInt(qty.value, 10) || 1) + Number(button.dataset.step));
              qty.value = String(next);
            });
          });

          if (this.groups.length) this.update();
        }

        disconnectedCallback() {
          (this.cleanup || []).forEach((fn) => fn());
          this.cleanup = [];
        }

        selectedValues() {
          return this.groups.map((group) => group.querySelector('input:checked')?.value);
        }

        update() {
          const selected = this.selectedValues();
          const matches = (variant, index, value) =>
            variant.options.every((option, i) => (i === index ? option === value : option === selected[i]));

          // Mark every value that would lead to a sold-out or missing combination.
          this.groups.forEach((group, index) => {
            group.querySelectorAll('input').forEach((input) => {
              const variant = this.variants.find((v) => matches(v, index, input.value));
              const soldOut = !variant || !variant.available;
              const choice = input.closest('.lib-featured-product__choice');
              choice.toggleAttribute('data-sold-out', soldOut);
              choice.querySelector('[data-sold-out-text]').hidden = !soldOut;
            });
          });

          const variant = this.variants.find((v) => v.options.every((option, i) => option === selected[i]));
          const button = this.addButton;
          if (!variant) {
            button.disabled = true;
            button.textContent = button.dataset.unavailable;
            return;
          }
          this.idInput.value = variant.id;
          button.disabled = !variant.available;
          button.textContent = variant.available ? button.dataset.label : button.dataset.soldOut;

          const onSale = Boolean(variant.compare);
          const price = this.querySelector('[data-price]');
          price.textContent = variant.price;
          price.classList.toggle('is-sale', onSale);
          const compare = this.querySelector('[data-compare]');
          compare.textContent = variant.compare || '';
          compare.hidden = !onSale;
          this.querySelector('[data-sale-label]').hidden = !onSale;
          this.querySelector('[data-was-label]').hidden = !onSale;
          this.querySelector('[data-badge-sale]').hidden = !onSale;
          this.querySelector('[data-badge-sold]').hidden = variant.available;

          if (variant.media !== null) {
            const index = this.slides.findIndex((slide) => slide.dataset.mediaId === String(variant.media));
            if (index > -1) this.showSlide(index);
          }
        }

        showSlide(index) {
          this.slides.forEach((slide, i) => {
            slide.hidden = i !== index;
          });
          this.thumbs.forEach((thumb, i) => {
            if (i === index) thumb.setAttribute('aria-current', 'true');
            else thumb.removeAttribute('aria-current');
          });
        }
      },
    );
  }
{% endjavascript %}

{% schema %}
{
  "name": "Lib · Featured product",
  "tag": "section",
  "class": "lib-section",
  "settings": [
    { "type": "header", "content": "Product" },
    { "type": "product", "id": "product", "label": "Product" },
    { "type": "checkbox", "id": "show_vendor", "label": "Show vendor", "default": true },
    {
      "type": "select",
      "id": "heading_tag",
      "label": "Product title tag",
      "info": "Use H1 only when this is the page's main heading.",
      "options": [
        { "value": "h1", "label": "H1" },
        { "value": "h2", "label": "H2" },
        { "value": "h3", "label": "H3" },
        { "value": "p", "label": "Paragraph" }
      ],
      "default": "h2"
    },
    {
      "type": "select",
      "id": "heading_size",
      "label": "Product title size",
      "options": [
        { "value": "small", "label": "Small" },
        { "value": "medium", "label": "Medium" },
        { "value": "large", "label": "Large" }
      ],
      "default": "medium"
    },
    { "type": "checkbox", "id": "show_description", "label": "Show product description", "default": true },
    { "type": "checkbox", "id": "show_quantity", "label": "Show quantity selector", "default": true },

    { "type": "header", "content": "Gallery" },
    { "type": "checkbox", "id": "show_thumbnails", "label": "Show thumbnails", "default": true },
    {
      "type": "select",
      "id": "media_ratio",
      "label": "Image shape",
      "options": [
        { "value": "adapt", "label": "Adapt to first image" },
        { "value": "square", "label": "Square" },
        { "value": "portrait", "label": "Portrait" },
        { "value": "landscape", "label": "Landscape" }
      ],
      "default": "square"
    },
    { "type": "range", "id": "corner_radius", "label": "Corner radius", "min": 0, "max": 32, "step": 2, "unit": "px", "default": 12 },

    { "type": "text", "id": "details_label", "label": "Details link label", "info": "Links to the product page. Leave empty to hide.", "default": "View full details" },
    {
      "type": "select",
      "id": "details_style",
      "label": "Details link style",
      "options": [
        { "value": "primary", "label": "Solid" },
        { "value": "outline", "label": "Outline" },
        { "value": "text", "label": "Text link" }
      ],
      "default": "text"
    },
    { "type": "checkbox", "id": "details_new_tab", "label": "Open details link in a new tab", "default": false },

    { "type": "header", "content": "Shopper-facing words" },
    { "type": "text", "id": "button_label", "label": "Add to cart button", "default": "Add to cart" },
    { "type": "text", "id": "sold_out_label", "label": "Sold out", "default": "Sold out" },
    { "type": "text", "id": "unavailable_label", "label": "Unavailable combination", "default": "Unavailable" },
    { "type": "text", "id": "sale_badge_label", "label": "Sale badge", "default": "Sale" },
    { "type": "text", "id": "sale_price_label", "label": "Sale price (screen readers)", "default": "Sale price" },
    { "type": "text", "id": "regular_price_label", "label": "Regular price (screen readers)", "default": "Regular price" },
    { "type": "text", "id": "quantity_label", "label": "Quantity (screen readers)", "default": "Quantity" },
    { "type": "text", "id": "decrease_label", "label": "Decrease quantity (screen readers)", "default": "Decrease quantity" },
    { "type": "text", "id": "increase_label", "label": "Increase quantity (screen readers)", "default": "Increase quantity" },
    { "type": "text", "id": "thumbnail_label", "label": "Thumbnail button (screen readers)", "info": "The image number is added, e.g. \"Show image 2\".", "default": "Show image" },

    { "type": "header", "content": "Layout" },
    {
      "type": "select",
      "id": "media_position",
      "label": "Gallery position on desktop",
      "options": [
        { "value": "left", "label": "Left" },
        { "value": "right", "label": "Right" }
      ],
      "default": "left"
    },
    {
      "type": "select",
      "id": "media_width",
      "label": "Gallery width on desktop",
      "options": [
        { "value": "half", "label": "Half" },
        { "value": "wide", "label": "Wide" }
      ],
      "default": "half"
    },
    {
      "type": "select",
      "id": "mobile_order",
      "label": "Order on mobile",
      "options": [
        { "value": "media_first", "label": "Gallery first" },
        { "value": "info_first", "label": "Product info first" }
      ],
      "default": "media_first"
    },
    {
      "type": "select",
      "id": "section_width",
      "label": "Width",
      "options": [
        { "value": "page", "label": "Page width" },
        { "value": "full", "label": "Full width" }
      ],
      "default": "page"
    },
    {
      "type": "select",
      "id": "text_alignment",
      "label": "Text alignment on desktop",
      "options": [
        { "value": "left", "label": "Left" },
        { "value": "center", "label": "Center" }
      ],
      "default": "left"
    },
    {
      "type": "select",
      "id": "text_alignment_mobile",
      "label": "Text alignment on mobile",
      "options": [
        { "value": "left", "label": "Left" },
        { "value": "center", "label": "Center" }
      ],
      "default": "left"
    },

    { "type": "header", "content": "Colors" },
    { "type": "color", "id": "color_bg", "label": "Background", "default": "#FBF8F4" },
    { "type": "color", "id": "color_text", "label": "Text", "default": "#231F1B" },
    { "type": "color", "id": "color_accent", "label": "Add to cart button, sale price and badge", "default": "#7C2D12" },
    { "type": "color", "id": "color_accent_text", "label": "Text on button and sale badge", "default": "#FFFFFF" },
    { "type": "color", "id": "color_border", "label": "Borders", "default": "#D9D2C8" },

    { "type": "header", "content": "Spacing, visibility and motion" },
    { "type": "range", "id": "padding_top", "label": "Top padding on desktop", "min": 0, "max": 200, "step": 4, "unit": "px", "default": 80 },
    { "type": "range", "id": "padding_bottom", "label": "Bottom padding on desktop", "min": 0, "max": 200, "step": 4, "unit": "px", "default": 80 },
    { "type": "range", "id": "padding_top_mobile", "label": "Top padding on mobile", "min": 0, "max": 160, "step": 4, "unit": "px", "default": 40 },
    { "type": "range", "id": "padding_bottom_mobile", "label": "Bottom padding on mobile", "min": 0, "max": 160, "step": 4, "unit": "px", "default": 48 },
    {
      "type": "select",
      "id": "show_on",
      "label": "Show on",
      "options": [
        { "value": "all", "label": "All devices" },
        { "value": "desktop", "label": "Desktop only" },
        { "value": "mobile", "label": "Mobile only" }
      ],
      "default": "all"
    },
    { "type": "text", "id": "anchor_id", "label": "Anchor ID", "info": "Optional. Link to this section with #your-id." },
    { "type": "checkbox", "id": "animate", "label": "Fade in on scroll", "info": "Only in browsers that support scroll-driven animations; never when reduced motion is on.", "default": false }
  ],
  "presets": [
    { "name": "Lib · Featured product" }
  ]
}
{% endschema %}
```

### snippets/lib-button.liquid

```liquid
{%- comment -%}
  MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections
{%- endcomment -%}
{% doc %}
  Animated call-to-action link. Light sweep on hover, arrow slides right.
  Colors come from the parent section's --lib-accent / --lib-accent-text / --lib-text vars.
  Inspired by Uiverse.io button patterns; rewritten for themes, no dependencies.

  @param {string} label - Button text
  @param {string} [url] - Link target; renders a disabled-looking span when blank
  @param {string} [style] - 'primary' (default), 'outline' or 'text'
  @param {boolean} [new_tab] - Open the link in a new tab
  @param {boolean} [arrow] - Show the trailing arrow (default true)

  @example
  {% render 'lib-button', label: section.settings.button_label, url: section.settings.button_url, style: 'outline' %}
{% enddoc %}

{%- liquid
  assign btn_style = style | default: 'primary'
  assign show_arrow = true
  if arrow == false
    assign show_arrow = false
  endif
-%}

{%- unless label == blank -%}
{%- capture inner -%}
  <span class="lib-btn__label">{{ label | escape }}</span>
  {%- if show_arrow -%}
    <svg class="lib-btn__arrow" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false">
      <path d="M3 8h9M8.5 4.5 12 8l-3.5 3.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
    </svg>
  {%- endif -%}
{%- endcapture -%}

{%- if url != blank -%}
  <a
    class="lib-btn lib-btn--{{ btn_style }}"
    href="{{ url }}"
    {% if new_tab %}
      target="_blank" rel="noopener"
    {% endif %}
  >
    {{- inner -}}
  </a>
{%- else -%}
  <span class="lib-btn lib-btn--{{ btn_style }}" aria-disabled="true">{{ inner }}</span>
{%- endif -%}
{%- endunless -%}

{% stylesheet %}
  .lib-btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 0.5em;
    padding: 0.85em 1.5em;
    border-radius: var(--lib-radius, 999px);
    font: inherit;
    font-weight: 600;
    line-height: 1;
    text-decoration: none;
    overflow: hidden;
    isolation: isolate;
    transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
  }
  .lib-btn--primary {
    background: var(--lib-accent, #111);
    color: var(--lib-accent-text, #fff);
    box-shadow: 0 1px 2px rgb(0 0 0 / 0.12);
  }
  .lib-btn--outline {
    background: transparent;
    color: var(--lib-text, #111);
    box-shadow: inset 0 0 0 1.5px currentColor;
  }
  .lib-btn--text {
    padding-inline: 0;
    background: none;
    color: var(--lib-text, #111);
    text-decoration: underline;
    text-underline-offset: 0.3em;
    overflow: visible;
  }
  .lib-btn--text::before { display: none; }
  .lib-btn--text:hover { transform: none; box-shadow: none; }
  .lib-btn::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    background: linear-gradient(110deg, transparent 30%, rgb(255 255 255 / 0.35) 50%, transparent 70%);
    transform: translateX(-120%);
    transition: transform 0.6s ease;
  }
  .lib-btn--outline::before {
    background: linear-gradient(110deg, transparent 30%, rgb(127 127 127 / 0.18) 50%, transparent 70%);
  }
  .lib-btn:hover {
    transform: translateY(-2px);
    box-shadow: 0 8px 20px -8px rgb(0 0 0 / 0.35);
  }
  .lib-btn:hover::before { transform: translateX(120%); }
  .lib-btn__arrow { transition: transform 0.2s ease; }
  .lib-btn:hover .lib-btn__arrow { transform: translateX(3px); }
  .lib-btn:focus-visible {
    outline: 2px solid var(--lib-accent, #111);
    outline-offset: 3px;
  }
  .lib-btn[aria-disabled='true'] { pointer-events: none; }
  @media (prefers-reduced-motion: reduce) {
    .lib-btn, .lib-btn::before, .lib-btn__arrow { transition: none; }
    .lib-btn:hover { transform: none; }
  }
{% endstylesheet %}
```

## License

MIT License, © 2026 Lintel. Free to use, change and ship in your own store or in client work; keep the license line at the top of each file.
