# Contact form: free Shopify section

> Shopify's own contact form with name, email, phone, message and extra fields, plus an info column with an image, address, map link, email and hours.

Contact form is a free Shopify section built on Shopify's own contact form, so messages reach the store's sender email without an app. Name, email, phone and message are built in, extra fields (short text, long text, choice list, checkbox) are blocks, and an optional column beside the form shows an image, the address with a map link, email, phone and opening hours. Errors are listed above the fields, every field has a visible label, and it works in Online Store 2.0 themes.

- **Category:** Forms and newsletter
- **Use on:** Contact, About, Landing pages, Any page
- **Reads:** Shopify contact form
- **Size:** 36 KB in 2 files, no JavaScript
- **License:** MIT, © 2026 Lintel
- **Page:** https://lintelagency.com/resources/shopify-sections/contact-form
- **Live demo:** https://lintelagency.com/resources/shopify-sections/demo/lib-contact-form.html
- **Added:** 2026-11-02

## What it’s for

- The Contact page, with the studio address, opening hours and a photo of the shop
- A wholesale or custom-order enquiry form with a choice list and an order-number field
- A short "Ask us anything" form at the bottom of an About or landing page

## Editor settings

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

### Content

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `heading` | `inline_richtext` | Heading | Get in touch |  |
| `heading_tag` | `select` | Heading tag | h2 | `h1`, `h2`, `h3`, `p` |
| `heading_size` | `select` | Heading size | large | `small`, `medium`, `large`, `xlarge` |
| `text` | `richtext` | Text | `<p>Questions about an order, a product or a custom piece? Send us a note and we will reply by email.</p>` |  |

### Form fields

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `name_field` | `select` | Name field | required | `hidden`, `optional`, `required` |
| `phone_field` | `select` | Phone field | hidden | `hidden`, `optional`, `required` |
| `message_required` | `checkbox` | Message is required | `true` |  |

### Form text

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `name_label` | `text` | Name label | Name |  |
| `email_label` | `text` | Email label | Email |  |
| `phone_label` | `text` | Phone label | Phone |  |
| `message_label` | `text` | Message label | Message |  |
| `required_text` | `text` | Required field marker | (required) |  |
| `submit_label` | `text` | Send button | Send message |  |
| `success_message` | `text` | Message after sending | Thanks for writing. We will reply by email. |  |
| `error_heading` | `text` | Error summary heading | Please check the form: |  |
| `privacy_text` | `richtext` | Privacy note | `<p>We only use your details to answer you. Read our <a href="/policies/privacy-policy" title="Privacy policy">privacy policy</a>.</p>` |  |

### Info column

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `image` | `image_picker` | Desktop image | – |  |
| `image_mobile` | `image_picker` | Mobile image | – |  |
| `info_heading` | `text` | Heading | Visit the studio |  |

### Layout

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `info_position` | `select` | Info column on desktop | right | `right`, `left` |
| `mobile_order` | `select` | Order on mobile | form_first | `form_first`, `info_first` |
| `section_width` | `select` | Width | page | `page`, `full` |
| `text_alignment` | `select` | Heading alignment on desktop | left | `left`, `center` |
| `text_alignment_mobile` | `select` | Heading alignment on mobile | left | `left`, `center` |

### Colors

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `color_bg` | `color` | Background | #FFFFFF |  |
| `color_bg_gradient` | `color_background` | Background gradient | – |  |
| `color_text` | `color` | Text | #1F1B17 |  |
| `color_accent` | `color` | Accent (button, focus, titles) | #7A5A3A |  |
| `color_accent_text` | `color` | Text on accent | #FFFFFF |  |
| `color_field` | `color` | Field background | #FFFFFF |  |
| `color_border` | `color` | Field border | #8A8178 |  |
| `color_panel` | `color` | Info column background | #F6F1EA |  |
| `color_error` | `color` | Errors | #B42318 |  |

### Spacing and visibility

| 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 | `48` | 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 | – |  |

### Motion

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `animate` | `checkbox` | Fade in on scroll | `false` |  |

## Blocks

Up to 12 blocks in total.

### Extra field (`field`)

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `label` | `text` | Label | Order number |  |
| `field_type` | `select` | Type | text | `text`, `textarea`, `select`, `checkbox` |
| `options` | `textarea` | Choices | – |  |
| `prompt` | `text` | First option text | Choose one |  |
| `checked_value` | `text` | Value when ticked | Yes |  |
| `unchecked_value` | `text` | Value when not ticked | No |  |
| `help` | `text` | Help text | – |  |
| `required` | `checkbox` | Required | `false` |  |
| `width` | `select` | Width on desktop | half | `half`, `full` |

### Address (`address`, up to 1)

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `title` | `text` | Title | Address |  |
| `address` | `textarea` | Address | 14 Kiln Lane Portland, OR 97214 |  |
| `map_link_label` | `text` | Map link text | Open in Maps |  |
| `new_tab_text` | `text` | Screen reader text for the map link | (opens in a new tab) |  |

### Contact detail (`detail`)

| ID | Type | Label | Default | Options or range |
| --- | --- | --- | --- | --- |
| `title` | `text` | Title | Email |  |
| `email` | `text` | Email address | – |  |
| `phone` | `text` | Phone number | – |  |
| `text` | `richtext` | Text | – |  |

## 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-contact-form.liquid` and paste the contents of `sections/lib-contact-form.liquid` from the source below. Save.
4. In the `snippets` folder, add `lib-image.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 · Contact form**. 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-contact-form.liquid https://lintelagency.com/resources/shopify-sections/files/lib-contact-form.liquid
curl -fsSL -o snippets/lib-image.liquid https://lintelagency.com/resources/shopify-sections/files/lib-image.liquid
shopify theme push --theme <unpublished-theme-id> --only sections/lib-contact-form.liquid --only snippets/lib-image.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-contact-form.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-contact-form.liquid): goes in `sections/`
- [snippets/lib-image.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-image.liquid): goes in `snippets/`

## Questions

### Where do the messages go?

To the sender email set in the Shopify admin under Settings > Notifications, like your theme's own contact page. The section uses Shopify's contact form, so there is no app or third-party service, and the store's spam protection settings still apply.

### Can I add my own fields?

Yes. Add Extra field blocks: short text, long text, a choice list (one choice per line) or a checkbox, each with a help line, required or optional, and half or full width. The label becomes the field's name in the email you receive.

### Is it accessible?

Every field has a visible label and the required marker is text, not only a star. After sending, errors are listed above the fields in an alert, with the email field marked invalid, and the success message is announced. Name, email and phone use autocomplete, so browsers can fill them in.

### Does the map link use a maps API?

No. The address becomes a plain link that opens it in Google Maps in a new tab, so nothing loads on your page and no API key is needed. Leave the link text empty to hide it.

## Source

### sections/lib-contact-form.liquid

```liquid
{%- comment -%}
  MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections
{%- endcomment -%}
{%- comment -%}
  Contact form: Shopify's own contact form, so messages reach the store's sender email without an
  app. Name, email, phone and message are built in (phone optional, any of them can be required
  except email, which Shopify always needs), plus extra fields as blocks (text, long text, choice
  list, checkbox). Beside it an optional column with an image, address, a map link (no maps API),
  email, phone and opening hours. Errors are listed above the fields and tied to them. No JavaScript.
  Depends on: snippets/lib-image.liquid
{%- endcomment -%}

{%- liquid
  assign s = section.settings
  assign heading_tag = s.heading_tag | default: 'h2'
  assign form_id = 'LibContact-' | append: section.id
  assign info_count = 0
  for block in section.blocks
    if block.type == 'address' or block.type == 'detail'
      assign info_count = info_count | plus: 1
    endif
  endfor
  assign show_aside = false
  if s.image != blank or s.image_mobile != blank or s.info_heading != blank or info_count > 0
    assign show_aside = true
  endif
  assign name_required = false
  if s.name_field == 'required'
    assign name_required = true
  endif
  assign phone_required = false
  if s.phone_field == 'required'
    assign phone_required = true
  endif
  assign eager = false
  if section.index <= 2
    assign eager = true
  endif
-%}

{%- capture required_mark -%}
  <span class="lib-contact__required">{{ s.required_text | escape }}</span>
{%- endcapture -%}

<div
  class="lib-contact lib-contact--{{ s.section_width }}"
  {% if s.anchor_id != blank %}
    id="{{ s.anchor_id | handleize }}"
  {% endif %}
  data-show="{{ s.show_on }}"
  data-aside="{% if show_aside %}{{ s.info_position }}{% else %}none{% endif %}"
  data-mobile-order="{{ s.mobile_order }}"
  {% if s.animate %}
    data-animate
  {% endif %}
  style="
    --lib-bg: {{ s.color_bg }};
    {% if s.color_bg_gradient != blank %}--lib-bg-image: {{ s.color_bg_gradient }};{% endif %}
    --lib-text: {{ s.color_text }};
    --lib-accent: {{ s.color_accent }};
    --lib-accent-text: {{ s.color_accent_text }};
    --lib-field: {{ s.color_field }};
    --lib-border: {{ s.color_border }};
    --lib-panel: {{ s.color_panel }};
    --lib-error: {{ s.color_error }};
    --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-contact__inner">
    <div class="lib-contact__main" data-align="{{ s.text_alignment }}" data-align-m="{{ s.text_alignment_mobile }}">
      {%- if s.heading != blank -%}
        <{{ heading_tag }} class="lib-contact__heading lib-contact__heading--{{ s.heading_size }}">
          {{- s.heading -}}
        </{{ heading_tag }}>
      {%- endif -%}
      {%- if s.text != blank -%}
        <div class="lib-contact__text">{{ s.text }}</div>
      {%- endif -%}

      {%- form 'contact', id: form_id, class: 'lib-contact__form' -%}
        {%- if form.posted_successfully? -%}
          <p class="lib-contact__msg lib-contact__msg--success" role="status" tabindex="-1" autofocus>
            <svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false">
              <path d="m5 10.5 3.2 3L15 6.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>
            <span>{{ s.success_message | escape }}</span>
          </p>
        {%- elsif form.errors -%}
          <div class="lib-contact__msg lib-contact__msg--error" role="alert" tabindex="-1" autofocus>
            <p class="lib-contact__msg-title">{{ s.error_heading | escape }}</p>
            <ul role="list">
              {%- for field in form.errors -%}
                <li>
                  {%- if field == 'form' -%}
                    {{ form.errors.messages[field] }}
                  {%- else -%}
                    {{ form.errors.translated_fields[field] | capitalize }} {{ form.errors.messages[field] }}
                  {%- endif -%}
                </li>
              {%- endfor -%}
            </ul>
          </div>
        {%- endif -%}

        <div class="lib-contact__fields">
          {%- if s.name_field != 'hidden' -%}
            <div class="lib-contact__field lib-contact__field--half">
              <label class="lib-contact__label" for="{{ form_id }}-name">
                {{- s.name_label | escape -}}
                {%- if name_required %} {{ required_mark }}{% endif -%}
              </label>
              <input
                class="lib-contact__input"
                type="text"
                id="{{ form_id }}-name"
                name="contact[name]"
                value="{{ form.name | escape }}"
                autocomplete="name"
                {% if name_required %}
                  required aria-required="true"
                {% endif %}
              >
            </div>
          {%- endif -%}

          <div class="lib-contact__field lib-contact__field--half">
            <label class="lib-contact__label" for="{{ form_id }}-email">
              {{- s.email_label | escape }} {{ required_mark -}}
            </label>
            <input
              class="lib-contact__input"
              type="email"
              id="{{ form_id }}-email"
              name="contact[email]"
              value="{{ form.email | escape }}"
              autocomplete="email"
              autocapitalize="off"
              spellcheck="false"
              required
              aria-required="true"
              {% if form.errors contains 'email' %}
                aria-invalid="true"
              {% endif %}
            >
          </div>

          {%- if s.phone_field != 'hidden' -%}
            <div class="lib-contact__field lib-contact__field--half">
              <label class="lib-contact__label" for="{{ form_id }}-phone">
                {{- s.phone_label | escape -}}
                {%- if phone_required %} {{ required_mark }}{% endif -%}
              </label>
              <input
                class="lib-contact__input"
                type="tel"
                id="{{ form_id }}-phone"
                name="contact[phone]"
                value="{{ form.phone | escape }}"
                autocomplete="tel"
                {% if phone_required %}
                  required aria-required="true"
                {% endif %}
              >
            </div>
          {%- endif -%}

          {%- for block in section.blocks -%}
            {%- if block.type != 'field' -%}{%- continue -%}{%- endif -%}
            {%- assign b = block.settings -%}
            {%- if b.label == blank -%}{%- continue -%}{%- endif -%}
            {%- assign field_id = form_id | append: '-' | append: block.id -%}
            {%- assign field_name = 'contact[' | append: b.label | append: ']' -%}
            <div class="lib-contact__field lib-contact__field--{{ b.width }}" {{ block.shopify_attributes }}>
              {%- case b.field_type -%}
                {%- when 'checkbox' -%}
                  <div class="lib-contact__check">
                    <input type="hidden" name="{{ field_name | escape }}" value="{{ b.unchecked_value | escape }}">
                    <input
                      class="lib-contact__checkbox"
                      type="checkbox"
                      id="{{ field_id }}"
                      name="{{ field_name | escape }}"
                      value="{{ b.checked_value | escape }}"
                      {% if b.required %}
                        required aria-required="true"
                      {% endif %}
                    >
                    <label for="{{ field_id }}">
                      {{- b.label | escape -}}
                      {%- if b.required %} {{ required_mark }}{% endif -%}
                    </label>
                  </div>
                {%- when 'select' -%}
                  {%- assign choices = b.options | newline_to_br | split: '<br />' -%}
                  <label class="lib-contact__label" for="{{ field_id }}">
                    {{- b.label | escape -}}
                    {%- if b.required %} {{ required_mark }}{% endif -%}
                  </label>
                  <div class="lib-contact__select-wrap">
                    <select
                      class="lib-contact__input lib-contact__select"
                      id="{{ field_id }}"
                      name="{{ field_name | escape }}"
                      {% if b.required %}
                        required aria-required="true"
                      {% endif %}
                    >
                      <option value="">{{ b.prompt | escape }}</option>
                      {%- for choice in choices -%}
                        {%- assign choice = choice | strip -%}
                        {%- if choice != blank -%}
                          <option value="{{ choice | escape }}">{{ choice | escape }}</option>
                        {%- endif -%}
                      {%- endfor -%}
                    </select>
                    <svg class="lib-contact__chevron" viewBox="0 0 12 12" width="12" height="12" aria-hidden="true" focusable="false">
                      <path d="m2.5 4.5 3.5 3.5 3.5-3.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
                    </svg>
                  </div>
                {%- when 'textarea' -%}
                  <label class="lib-contact__label" for="{{ field_id }}">
                    {{- b.label | escape -}}
                    {%- if b.required %} {{ required_mark }}{% endif -%}
                  </label>
                  <textarea
                    class="lib-contact__input lib-contact__textarea"
                    id="{{ field_id }}"
                    name="{{ field_name | escape }}"
                    rows="4"
                    {% if b.required %}
                      required aria-required="true"
                    {% endif %}
                  ></textarea>
                {%- else -%}
                  <label class="lib-contact__label" for="{{ field_id }}">
                    {{- b.label | escape -}}
                    {%- if b.required %} {{ required_mark }}{% endif -%}
                  </label>
                  <input
                    class="lib-contact__input"
                    type="text"
                    id="{{ field_id }}"
                    name="{{ field_name | escape }}"
                    {% if b.required %}
                      required aria-required="true"
                    {% endif %}
                  >
              {%- endcase -%}
              {%- if b.help != blank -%}
                <p class="lib-contact__help">{{ b.help | escape }}</p>
              {%- endif -%}
            </div>
          {%- endfor -%}

          <div class="lib-contact__field lib-contact__field--full">
            <label class="lib-contact__label" for="{{ form_id }}-body">
              {{- s.message_label | escape -}}
              {%- if s.message_required %} {{ required_mark }}{% endif -%}
            </label>
            <textarea
              class="lib-contact__input lib-contact__textarea"
              id="{{ form_id }}-body"
              name="contact[body]"
              rows="6"
              {% if s.message_required %}
                required aria-required="true"
              {% endif %}
            >{{ form.body }}</textarea>
          </div>
        </div>

        <div class="lib-contact__submit-row">
          <button type="submit" class="lib-contact__submit">{{ s.submit_label | escape }}</button>
          {%- if s.privacy_text != blank -%}
            <div class="lib-contact__privacy">{{ s.privacy_text }}</div>
          {%- endif -%}
        </div>
      {%- endform -%}
    </div>

    {%- if show_aside -%}
      <aside class="lib-contact__aside"{% if s.info_heading != blank %} aria-labelledby="{{ form_id }}-info"{% endif %}>
        {%- if s.image != blank or s.image_mobile != blank -%}
          <div class="lib-contact__media">
            {%- render 'lib-image',
              image: s.image,
              image_mobile: s.image_mobile,
              class: 'lib-contact__img',
              sizes: '(min-width: 990px) 40vw, 100vw',
              eager: eager
            -%}
          </div>
        {%- endif -%}
        <div class="lib-contact__info">
          {%- if s.info_heading != blank -%}
            <h3 class="lib-contact__info-heading" id="{{ form_id }}-info">{{ s.info_heading | escape }}</h3>
          {%- endif -%}
          {%- if info_count > 0 -%}
            <dl class="lib-contact__details">
              {%- for block in section.blocks -%}
                {%- assign b = block.settings -%}
                {%- case block.type -%}
                  {%- when 'address' -%}
                    {%- if b.address != blank -%}
                      <div {{ block.shopify_attributes }}>
                        <dt>{{ b.title | escape }}</dt>
                        <dd>
                          <address>{{ b.address | escape | newline_to_br }}</address>
                          {%- if b.map_link_label != blank -%}
                            {%- assign map_query = b.address | strip_newlines | url_encode -%}
                            <a class="lib-contact__map" href="https://www.google.com/maps/search/?api=1&amp;query={{ map_query }}" target="_blank" rel="noopener">
                              {{- b.map_link_label | escape -}}
                              <span class="lib-contact__sr"> {{ b.new_tab_text | escape }}</span>
                            </a>
                          {%- endif -%}
                        </dd>
                      </div>
                    {%- endif -%}
                  {%- when 'detail' -%}
                    {%- if b.email != blank or b.phone != blank or b.text != blank -%}
                      <div {{ block.shopify_attributes }}>
                        <dt>{{ b.title | escape }}</dt>
                        {%- if b.email != blank -%}
                          <dd><a href="mailto:{{ b.email | strip | escape }}">{{ b.email | strip | escape }}</a></dd>
                        {%- endif -%}
                        {%- if b.phone != blank -%}
                          {%- assign phone_href = b.phone | remove: ' ' | remove: '(' | remove: ')' | remove: '-' | remove: '.' -%}
                          <dd><a href="tel:{{ phone_href | escape }}">{{ b.phone | escape }}</a></dd>
                        {%- endif -%}
                        {%- if b.text != blank -%}
                          <dd class="lib-contact__rte">{{ b.text }}</dd>
                        {%- endif -%}
                      </div>
                    {%- endif -%}
                {%- endcase -%}
              {%- endfor -%}
            </dl>
          {%- endif -%}
        </div>
      </aside>
    {%- endif -%}
  </div>
</div>

{% stylesheet %}
  .lib-contact {
    --lib-radius: 10px;
    background-color: var(--lib-bg);
    background-image: var(--lib-bg-image, none);
    color: var(--lib-text);
    padding: var(--lib-pt) 1rem var(--lib-pb);
  }
  .lib-contact__inner {
    display: grid;
    gap: clamp(2rem, 5vw, 4.5rem);
    max-width: 1200px;
    margin: 0 auto;
  }
  .lib-contact--full .lib-contact__inner { max-width: none; }
  .lib-contact[data-aside='none'] .lib-contact__inner { max-width: 46rem; }
  .lib-contact__inner > * { min-width: 0; }
  @media (min-width: 990px) {
    .lib-contact[data-aside='right'] .lib-contact__inner { grid-template-columns: 3fr 2fr; }
    .lib-contact[data-aside='left'] .lib-contact__inner { grid-template-columns: 2fr 3fr; }
    .lib-contact[data-aside='left'] .lib-contact__aside { order: -1; }
  }
  @media (max-width: 749px) {
    .lib-contact { padding-top: var(--lib-pt-m); padding-bottom: var(--lib-pb-m); }
    .lib-contact[data-show='desktop'] { display: none; }
    .lib-contact[data-mobile-order='info_first'] .lib-contact__aside { order: -1; }
  }
  @media (min-width: 750px) {
    .lib-contact[data-show='mobile'] { display: none; }
  }

  .lib-contact__main[data-align='center'] :is(.lib-contact__heading, .lib-contact__text) { text-align: center; margin-inline: auto; }
  @media (max-width: 749px) {
    .lib-contact__main[data-align-m='left'] :is(.lib-contact__heading, .lib-contact__text) { text-align: left; margin-inline: 0; }
    .lib-contact__main[data-align-m='center'] :is(.lib-contact__heading, .lib-contact__text) { text-align: center; margin-inline: auto; }
  }
  .lib-contact__heading {
    margin: 0;
    font-size: clamp(2rem, 4vw, 3rem);
    line-height: 1.1;
    letter-spacing: -0.025em;
    text-wrap: balance;
  }
  .lib-contact__heading--small { font-size: clamp(1.5rem, 2.5vw, 2rem); }
  .lib-contact__heading--medium { font-size: clamp(1.75rem, 3vw, 2.5rem); }
  .lib-contact__heading--xlarge { font-size: clamp(2.5rem, 6vw, 4.25rem); }
  .lib-contact__text { margin-top: 1rem; max-width: 36rem; font-size: 1.0625rem; line-height: 1.65; opacity: 0.82; }
  .lib-contact__text > :first-child { margin-top: 0; }
  .lib-contact__text > :last-child { margin-bottom: 0; }
  .lib-contact__text a { color: inherit; text-underline-offset: 0.2em; }

  .lib-contact__form { margin-top: clamp(1.5rem, 3vw, 2.25rem); }
  .lib-contact__fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.125rem 1rem; }
  .lib-contact__field { grid-column: 1 / -1; display: flex; flex-direction: column; gap: 0.45rem; min-width: 0; }
  @media (min-width: 750px) {
    .lib-contact__field--half { grid-column: span 1; }
  }
  .lib-contact__label { font-size: 0.9375rem; font-weight: 600; }
  .lib-contact__required { font-weight: 400; opacity: 0.7; }
  .lib-contact__input {
    width: 100%;
    min-height: 3rem;
    padding: 0.75rem 0.9rem;
    border: 1px solid var(--lib-border);
    border-radius: var(--lib-radius);
    background: var(--lib-field);
    color: inherit;
    font: inherit;
    font-size: 1rem;
    line-height: 1.4;
  }
  .lib-contact__textarea { min-height: 8rem; resize: vertical; }
  .lib-contact__input:focus-visible { outline: 2px solid var(--lib-accent); outline-offset: 2px; border-color: var(--lib-accent); }
  .lib-contact__input[aria-invalid='true'] { border-color: var(--lib-error); }
  .lib-contact__select-wrap { position: relative; }
  .lib-contact__select { appearance: none; padding-inline-end: 2.5rem; }
  .lib-contact__chevron { position: absolute; inset-inline-end: 1rem; top: 50%; transform: translateY(-50%); pointer-events: none; }
  .lib-contact__check { display: flex; align-items: flex-start; gap: 0.7rem; font-size: 0.9375rem; line-height: 1.45; }
  .lib-contact__checkbox { flex: none; width: 1.25rem; height: 1.25rem; margin: 0.1rem 0 0; accent-color: var(--lib-accent); }
  .lib-contact__checkbox:focus-visible { outline: 2px solid var(--lib-accent); outline-offset: 2px; }
  .lib-contact__help { margin: 0; font-size: 0.8125rem; opacity: 0.72; }

  .lib-contact__submit-row { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem 1.5rem; margin-top: 1.5rem; }
  .lib-contact__submit {
    min-height: 3rem;
    padding: 0.85rem 1.75rem;
    border: 0;
    border-radius: var(--lib-btn-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;
  }
  .lib-contact__submit:hover { transform: translateY(-1px); box-shadow: 0 8px 18px -10px rgb(0 0 0 / 0.45); }
  .lib-contact__submit:focus-visible { outline: 2px solid var(--lib-accent); outline-offset: 3px; }
  .lib-contact__privacy { flex: 1 1 16rem; font-size: 0.8125rem; line-height: 1.5; opacity: 0.75; }
  .lib-contact__privacy > * { margin: 0; }
  .lib-contact__privacy a { color: inherit; text-underline-offset: 0.2em; }

  .lib-contact__msg { display: flex; gap: 0.6rem; margin: 0 0 1.25rem; padding: 0.9rem 1rem; border-radius: var(--lib-radius); font-size: 0.9375rem; }
  .lib-contact__msg--success { align-items: center; background: color-mix(in srgb, var(--lib-accent) 12%, transparent); }
  .lib-contact__msg--error {
    flex-direction: column;
    gap: 0.35rem;
    border: 1px solid var(--lib-error);
    background: color-mix(in srgb, var(--lib-error) 8%, transparent);
  }
  .lib-contact__msg--error ul { margin: 0; padding-inline-start: 1.1rem; }
  .lib-contact__msg-title { margin: 0; font-weight: 600; }
  .lib-contact__msg svg { flex: none; }

  .lib-contact__aside {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    border-radius: calc(var(--lib-radius) * 2);
    background: var(--lib-panel);
  }
  .lib-contact__media { aspect-ratio: 4 / 3; background: color-mix(in srgb, var(--lib-text) 6%, transparent); }
  .lib-contact__img { display: block; width: 100%; height: 100%; object-fit: cover; }
  .lib-contact__img:is(svg) { fill: color-mix(in srgb, var(--lib-text) 25%, transparent); }
  .lib-contact__info { padding: clamp(1.5rem, 3vw, 2.25rem); }
  .lib-contact__info-heading { margin: 0 0 1.25rem; font-size: 1.25rem; line-height: 1.3; }
  .lib-contact__details { display: grid; gap: 1.1rem; margin: 0; }
  .lib-contact__details dt {
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--lib-accent);
  }
  .lib-contact__details dd { margin: 0.3rem 0 0; font-size: 0.9875rem; line-height: 1.55; }
  .lib-contact__details address { font-style: normal; }
  .lib-contact__details a { display: inline-block; padding-block: 0.15rem; color: inherit; text-underline-offset: 0.2em; overflow-wrap: anywhere; }
  .lib-contact__details a:focus-visible { outline: 2px solid var(--lib-accent); outline-offset: 2px; }
  .lib-contact__map { margin-top: 0.35rem; font-weight: 600; }
  .lib-contact__details dd + dd { margin-top: 0.15rem; }
  .lib-contact__rte > * { margin: 0; }
  .lib-contact__rte > * + * { margin-top: 0.25rem; }
  .lib-contact__sr {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
  }

  @media (prefers-reduced-motion: reduce) {
    .lib-contact__submit { transition: none; }
    .lib-contact__submit:hover { transform: none; }
  }
  /* Motion: the form and the info column rise in as they scroll into view. */
  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      .lib-contact[data-animate] .lib-contact__inner > * {
        animation: lib-contact-rise linear both;
        animation-timeline: view();
        animation-range: entry 0% entry 45%;
      }
    }
  }
  @keyframes lib-contact-rise {
    from { opacity: 0; transform: translateY(20px); }
  }
{% endstylesheet %}

{% schema %}
{
  "name": "Lib · Contact form",
  "tag": "section",
  "class": "lib-section",
  "max_blocks": 12,
  "settings": [
    { "type": "header", "content": "Content" },
    { "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Get in touch" },
    {
      "type": "select",
      "id": "heading_tag",
      "label": "Heading 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": "Heading size",
      "options": [
        { "value": "small", "label": "Small" },
        { "value": "medium", "label": "Medium" },
        { "value": "large", "label": "Large" },
        { "value": "xlarge", "label": "Extra large" }
      ],
      "default": "large"
    },
    { "type": "richtext", "id": "text", "label": "Text", "default": "<p>Questions about an order, a product or a custom piece? Send us a note and we will reply by email.</p>" },

    { "type": "header", "content": "Form fields" },
    { "type": "paragraph", "content": "Messages go to the sender email in Settings > Notifications. Email is always shown and required. Add more fields with Extra field blocks." },
    {
      "type": "select",
      "id": "name_field",
      "label": "Name field",
      "options": [
        { "value": "hidden", "label": "Hidden" },
        { "value": "optional", "label": "Optional" },
        { "value": "required", "label": "Required" }
      ],
      "default": "required"
    },
    {
      "type": "select",
      "id": "phone_field",
      "label": "Phone field",
      "options": [
        { "value": "hidden", "label": "Hidden" },
        { "value": "optional", "label": "Optional" },
        { "value": "required", "label": "Required" }
      ],
      "default": "hidden"
    },
    { "type": "checkbox", "id": "message_required", "label": "Message is required", "default": true },

    { "type": "header", "content": "Form text" },
    { "type": "text", "id": "name_label", "label": "Name label", "default": "Name" },
    { "type": "text", "id": "email_label", "label": "Email label", "default": "Email" },
    { "type": "text", "id": "phone_label", "label": "Phone label", "default": "Phone" },
    { "type": "text", "id": "message_label", "label": "Message label", "default": "Message" },
    { "type": "text", "id": "required_text", "label": "Required field marker", "default": "(required)" },
    { "type": "text", "id": "submit_label", "label": "Send button", "default": "Send message" },
    { "type": "text", "id": "success_message", "label": "Message after sending", "default": "Thanks for writing. We will reply by email." },
    { "type": "text", "id": "error_heading", "label": "Error summary heading", "default": "Please check the form:" },
    { "type": "richtext", "id": "privacy_text", "label": "Privacy note", "default": "<p>We only use your details to answer you. Read our <a href=\"/policies/privacy-policy\" title=\"Privacy policy\">privacy policy</a>.</p>" },

    { "type": "header", "content": "Info column" },
    { "type": "paragraph", "content": "Shown when it has an image, a heading or Address and Contact detail blocks." },
    { "type": "image_picker", "id": "image", "label": "Desktop image" },
    { "type": "image_picker", "id": "image_mobile", "label": "Mobile image", "info": "Optional. Shown below 750px." },
    { "type": "text", "id": "info_heading", "label": "Heading", "default": "Visit the studio" },

    { "type": "header", "content": "Layout" },
    {
      "type": "select",
      "id": "info_position",
      "label": "Info column on desktop",
      "options": [
        { "value": "right", "label": "Right" },
        { "value": "left", "label": "Left" }
      ],
      "default": "right"
    },
    {
      "type": "select",
      "id": "mobile_order",
      "label": "Order on mobile",
      "options": [
        { "value": "form_first", "label": "Form first" },
        { "value": "info_first", "label": "Info first" }
      ],
      "default": "form_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": "Heading alignment on desktop",
      "options": [
        { "value": "left", "label": "Left" },
        { "value": "center", "label": "Center" }
      ],
      "default": "left"
    },
    {
      "type": "select",
      "id": "text_alignment_mobile",
      "label": "Heading 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": "#FFFFFF" },
    { "type": "color_background", "id": "color_bg_gradient", "label": "Background gradient", "info": "Optional. Covers the background color." },
    { "type": "color", "id": "color_text", "label": "Text", "default": "#1F1B17" },
    { "type": "color", "id": "color_accent", "label": "Accent (button, focus, titles)", "default": "#7A5A3A" },
    { "type": "color", "id": "color_accent_text", "label": "Text on accent", "default": "#FFFFFF" },
    { "type": "color", "id": "color_field", "label": "Field background", "default": "#FFFFFF" },
    { "type": "color", "id": "color_border", "label": "Field border", "default": "#8A8178" },
    { "type": "color", "id": "color_panel", "label": "Info column background", "default": "#F6F1EA" },
    { "type": "color", "id": "color_error", "label": "Errors", "default": "#B42318" },

    { "type": "header", "content": "Spacing and visibility" },
    { "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": 48 },
    { "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": "header", "content": "Motion" },
    { "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 }
  ],
  "blocks": [
    {
      "type": "field",
      "name": "Extra field",
      "settings": [
        { "type": "text", "id": "label", "label": "Label", "info": "Also the field's name in the email you receive.", "default": "Order number" },
        {
          "type": "select",
          "id": "field_type",
          "label": "Type",
          "options": [
            { "value": "text", "label": "Short text" },
            { "value": "textarea", "label": "Long text" },
            { "value": "select", "label": "Choice list" },
            { "value": "checkbox", "label": "Checkbox" }
          ],
          "default": "text"
        },
        { "type": "textarea", "id": "options", "label": "Choices", "info": "Choice list only. One per line.", "visible_if": "{{ block.settings.field_type == 'select' }}" },
        { "type": "text", "id": "prompt", "label": "First option text", "default": "Choose one", "visible_if": "{{ block.settings.field_type == 'select' }}" },
        { "type": "text", "id": "checked_value", "label": "Value when ticked", "default": "Yes", "visible_if": "{{ block.settings.field_type == 'checkbox' }}" },
        { "type": "text", "id": "unchecked_value", "label": "Value when not ticked", "default": "No", "visible_if": "{{ block.settings.field_type == 'checkbox' }}" },
        { "type": "text", "id": "help", "label": "Help text", "info": "Optional. Shown under the field." },
        { "type": "checkbox", "id": "required", "label": "Required", "default": false },
        {
          "type": "select",
          "id": "width",
          "label": "Width on desktop",
          "options": [
            { "value": "half", "label": "Half" },
            { "value": "full", "label": "Full" }
          ],
          "default": "half"
        }
      ]
    },
    {
      "type": "address",
      "name": "Address",
      "limit": 1,
      "settings": [
        { "type": "paragraph", "content": "Shown in the info column." },
        { "type": "text", "id": "title", "label": "Title", "default": "Address" },
        { "type": "textarea", "id": "address", "label": "Address", "default": "14 Kiln Lane\nPortland, OR 97214" },
        { "type": "text", "id": "map_link_label", "label": "Map link text", "info": "Opens the address in Google Maps. Leave empty to hide.", "default": "Open in Maps" },
        { "type": "text", "id": "new_tab_text", "label": "Screen reader text for the map link", "default": "(opens in a new tab)" }
      ]
    },
    {
      "type": "detail",
      "name": "Contact detail",
      "settings": [
        { "type": "paragraph", "content": "Shown in the info column: an email, a phone number, opening hours or any short note." },
        { "type": "text", "id": "title", "label": "Title", "default": "Email" },
        { "type": "text", "id": "email", "label": "Email address", "info": "Optional. Becomes a mailto link." },
        { "type": "text", "id": "phone", "label": "Phone number", "info": "Optional. Becomes a tap-to-call link." },
        { "type": "richtext", "id": "text", "label": "Text" }
      ]
    }
  ],
  "presets": [
    {
      "name": "Lib · Contact form",
      "blocks": [
        { "type": "field", "settings": { "label": "Order number", "help": "Optional. Helps us find your order faster." } },
        { "type": "address" },
        { "type": "detail", "settings": { "title": "Email", "email": "hello@example.com" } },
        { "type": "detail", "settings": { "title": "Opening hours", "text": "<p>Wed–Sat, 10:00–17:00</p>" } }
      ]
    }
  ]
}
{% endschema %}
```

### snippets/lib-image.liquid

```liquid
{%- comment -%}
  MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections
{%- endcomment -%}
{% doc %}
  Responsive image with optional art direction: a separate mobile image (below 750px),
  each honouring its own focal point from the theme editor. Falls back to a placeholder.

  @param {image} [image] - Desktop image (also used on mobile when no mobile image is set)
  @param {image} [image_mobile] - Mobile image, below 750px
  @param {string} [class] - Class for the img element
  @param {string} [sizes] - sizes attribute for the desktop image (default 100vw)
  @param {boolean} [eager] - Load immediately with high fetch priority (use for the first sections on a page)
  @param {string} [placeholder] - placeholder_svg_tag name when no image is set (default 'lifestyle-1')

  @example
  {% render 'lib-image', image: section.settings.image, image_mobile: section.settings.image_mobile, class: 'lib-hero__img', sizes: '(min-width: 990px) 50vw, 100vw', eager: true %}
{% enddoc %}

{%- liquid
  assign desktop = image
  if desktop == blank
    assign desktop = image_mobile
  endif
  assign img_sizes = sizes | default: '100vw'
  assign img_loading = 'lazy'
  assign img_priority = 'auto'
  if eager
    assign img_loading = 'eager'
    assign img_priority = 'high'
  endif
  assign art = false
  if image != blank and image_mobile != blank
    assign art = true
  endif
-%}

{%- if desktop != blank -%}
  <picture
    class="lib-picture{% if art %} lib-picture--art{% endif %}"
    {% if art %}
      style="--lib-pos-m: {{ image_mobile.presentation.focal_point | default: '50% 50%' }};"
    {% endif %}
  >
    {%- if art -%}
      <source
        media="(max-width: 749px)"
        srcset="
          {{ image_mobile | image_url: width: 375 }} 375w,
          {{ image_mobile | image_url: width: 750 }} 750w,
          {{ image_mobile | image_url: width: 1100 }} 1100w,
          {{ image_mobile | image_url: width: 1500 }} 1500w
        "
        sizes="100vw"
        width="{{ image_mobile.width }}"
        height="{{ image_mobile.height }}"
      >
    {%- endif -%}
    {{
      desktop
      | image_url: width: 2400
      | image_tag:
        class: class,
        loading: img_loading,
        fetchpriority: img_priority,
        sizes: img_sizes,
        widths: '600, 900, 1200, 1600, 2000, 2400'
    }}
  </picture>
{%- else -%}
  {{ placeholder | default: 'lifestyle-1' | placeholder_svg_tag: class }}
{%- endif -%}

{% stylesheet %}
  .lib-picture { display: contents; }
  @media (max-width: 749px) {
    /* image_tag sets the desktop focal point inline; the mobile image needs its own. */
    .lib-picture--art img { object-position: var(--lib-pos-m) !important; }
  }
{% 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.
