Exemplos
Cada exemplo abaixo é autocontido: schema, Liquid, SCSS e JavaScript juntos. Adapte o que precisar - os contratos gerais estão em Schema e em Arquivos e subarquivos.
Seção simples com texto e cor
Seção intitulada “Seção simples com texto e cor”O ponto de partida para qualquer seção. Campos de settings controlam aparência; campos de content controlam o que é exibido.
{ "title": "Destaque da Home", "description": "Seção com título e cor editáveis.", "component_config": { "settings": { "background_color": { "type": "string", "title": "Cor de fundo", "default": "#F5F5F5" } }, "content": { "title": { "type": "string", "title": "Título", "default": "Bem-vindo" }, "body_text": { "type": "string", "title": "Texto", "format": "markdown" } } }, "defaultContent": { "settings": { "background_color": "#F5F5F5" }, "content": { "title": "Bem-vindo à nossa loja", "body_text": "Explore nossa coleção completa." } }}{% assign settings = component_configs.settings %}{% assign content = component_configs.content %}
<section class="destaque-home" style="--destaque-bg: {{ settings.background_color }};"> <div class="container"> {% if content.title != blank %} <h2 class="destaque-home__title">{{ content.title }}</h2> {% endif %}
{% if content.body_text != blank %} <div class="destaque-home__text">{{ content.body_text }}</div> {% endif %} </div></section>.destaque-home { background: var(--destaque-bg, #f5f5f5); padding: 4.8rem 0;
&__title { font-size: 3.2rem; margin-bottom: 1.6rem; }
&__text { font-size: 1.6rem; line-height: 1.6; }}JavaScript
Seção intitulada “JavaScript”const DestaqueHome = { init() {},};
export default DestaqueHome;Seção com imagem e texto
Seção intitulada “Seção com imagem e texto”Mostra como combinar upload de imagem com texto e como renderizar a imagem corretamente com lazyload e proporção.
{ "title": "Banner com texto", "description": "Imagem à esquerda e texto à direita.", "component_config": { "settings": { "background_color": { "type": "string", "title": "Cor de fundo", "default": "#FFFFFF" } }, "content": { "banner_image_url": { "type": "string", "title": "Imagem" }, "title": { "type": "string", "title": "Título" }, "body_text": { "type": "string", "title": "Texto", "format": "markdown" }, "cta_label": { "type": "string", "title": "Texto do botão" }, "cta_url": { "type": "string", "title": "Link do botão" } } }, "defaultContent": { "content": { "title": "Nossa história", "body_text": "Conte aqui um pouco sobre a marca ou a campanha.", "cta_label": "Saiba mais", "cta_url": "#" } }}{% assign settings = component_configs.settings %}{% assign content = component_configs.content %}
<section class="banner-texto" style="background-color: {{ settings.background_color }};"> <div class="container banner-texto__grid">
{% if content.banner_image_url != blank %} <figure class="image -horizontal banner-texto__imagem"> <img class="lazy" loading="lazy" data-src="{{ content.banner_image_url }}" alt="{{ content.title }}" /> </figure> {% endif %}
<div class="banner-texto__conteudo"> {% if content.title != blank %} <h2 class="banner-texto__title">{{ content.title }}</h2> {% endif %}
{% if content.body_text != blank %} <div class="banner-texto__text">{{ content.body_text }}</div> {% endif %}
{% if content.cta_label != blank and content.cta_url != blank %} <a href="{{ content.cta_url }}" class="button-default">{{ content.cta_label }}</a> {% endif %} </div>
</div></section>.banner-texto { padding: 4.8rem 0;
&__grid { display: grid; gap: 3.2rem;
@include media('md') { grid-template-columns: 1fr 1fr; align-items: center; } }
&__title { font-size: 3.2rem; margin-bottom: 1.6rem; }
&__text { font-size: 1.6rem; line-height: 1.6; margin-bottom: 2.4rem; }}JavaScript
Seção intitulada “JavaScript”const BannerTexto = { init() {},};
export default BannerTexto;Lista repetível com imagem
Seção intitulada “Lista repetível com imagem”O lojista pode adicionar, remover e reordenar itens. Cada item tem imagem, título e link.
{ "title": "Cards de categorias", "description": "Coleção de cards com imagem e link.", "component_config": { "settings": { "columns": { "type": "string", "title": "Colunas", "oneOf": [ { "const": "3", "title": "3 colunas" }, { "const": "4", "title": "4 colunas" } ], "default": "4" } }, "content": { "items": [ { "cover_image_url": { "type": "string", "title": "Imagem" }, "title": { "type": "string", "title": "Título" }, "link_url": { "type": "string", "title": "Link" } } ] } }, "defaultContent": { "settings": { "columns": "4" }, "content": { "items": [ { "title": "Categoria 1", "link_url": "#" }, { "title": "Categoria 2", "link_url": "#" }, { "title": "Categoria 3", "link_url": "#" }, { "title": "Categoria 4", "link_url": "#" } ] } }}{% assign settings = component_configs.settings %}{% assign content = component_configs.content %}
<section class="cards-categorias" data-columns="{{ settings.columns }}"> <div class="container"> <ul class="cards-categorias__lista"> {% for item in content.items %} {% if item.title != blank %} <li class="cards-categorias__item"> <a href="{{ item.link_url | default: '#' }}" class="cards-categorias__link">
{% if item.cover_image_url != blank %} <figure class="image -square cards-categorias__imagem"> <img class="lazy" loading="lazy" data-src="{{ item.cover_image_url }}" alt="{{ item.title }}" /> </figure> {% endif %}
<span class="cards-categorias__nome">{{ item.title }}</span> </a> </li> {% endif %} {% endfor %} </ul> </div></section>.cards-categorias { padding: 4.8rem 0;
&__lista { display: grid; gap: 1.6rem; grid-template-columns: repeat(2, 1fr); list-style: none; padding: 0;
@include media('md') { grid-template-columns: repeat(var(--columns, 4), 1fr); } }
&__link { display: flex; flex-direction: column; gap: 0.8rem; }
&__nome { font-size: 1.4rem; text-align: center; }}JavaScript
Seção intitulada “JavaScript”const CardsCategorias = { init() { const section = document.querySelector('.cards-categorias'); if (!section) return; const columns = section.dataset.columns; section.style.setProperty('--columns', columns); },};
export default CardsCategorias;Mobile com configuração própria
Seção intitulada “Mobile com configuração própria”Quando o layout mobile diverge do desktop - colunas, tamanhos ou imagens diferentes - use settings_mobile e content_mobile. O Liquid faz fallback para o desktop quando o campo mobile não está preenchido.
{ "title": "Destaque responsivo", "description": "Layout diferente por breakpoint.", "component_config": { "settings": { "background_color": { "type": "string", "title": "Cor de fundo", "default": "#F5F5F5" } }, "content": { "banner_image_url": { "type": "string", "title": "Imagem (desktop)" }, "title": { "type": "string", "title": "Título" } }, "settings_mobile": { "background_color": { "type": "string", "title": "Cor de fundo (mobile)", "default": "#FFFFFF" } }, "content_mobile": { "banner_image_url": { "type": "string", "title": "Imagem (mobile)" } } }, "defaultContent": { "settings": { "background_color": "#F5F5F5" }, "settings_mobile": { "background_color": "#FFFFFF" }, "content": { "title": "Novidade" } }}{% assign settings = component_configs.settings_mobile | default: component_configs.settings %}{% assign content = component_configs.content_mobile | default: component_configs.content %}
{% comment %}Para campos individuais, use o desktop como fallback explícito:{% endcomment %}{% assign imagem = component_configs.content_mobile.banner_image_url | default: component_configs.content.banner_image_url %}
<section class="destaque-responsivo" style="background-color: {{ settings.background_color }};"> <div class="container">
{% if imagem != blank %} <figure class="image -horizontal destaque-responsivo__imagem"> <img class="lazy" loading="lazy" data-src="{{ imagem }}" alt="{{ component_configs.content.title }}" /> </figure> {% endif %}
{% if component_configs.content.title != blank %} <h2>{{ component_configs.content.title }}</h2> {% endif %}
</div></section>Seção com vitrine de produtos (tag estática)
Seção intitulada “Seção com vitrine de produtos (tag estática)”Cada ocorrência desta seção tem sua própria coleção de produtos, identificada por uma tag gerada automaticamente.
{ "title": "Vitrine de produtos", "description": "Produtos associados a uma tag própria.", "requires_external_content": { "type": "static", "static_tag_name": "home-produtos", "instructions": { "title": "Associe produtos à vitrine", "steps": [ "1. Use a tag sugerida nesta seção", "2. Acesse [Tags]({{ADMIN_URL}}/tags) e [Produtos]({{ADMIN_URL}}/produtos)", "3. Vincule a tag aos produtos desejados" ] } }, "component_config": { "settings": {}, "content": { "title": { "type": "string", "title": "Título da vitrine" } } }, "defaultContent": { "content": { "title": "Produtos em destaque" } }}{% assign content = component_configs.content %}
{% if content.tag_name != blank %} {% load_products tag: content.tag_name per_page: 10 %}
{% if products.size > 0 %} <section class="vitrine-produtos"> <div class="container"> {% if content.title != blank %} <h2 class="vitrine-produtos__titulo">{{ content.title }}</h2> {% endif %}
<div class="vitrine-produtos__lista"> {% for product in products %} {% render 'partials/components/product_block', product: product, mobile: mobile, component_configs: product_block_config %} {% endfor %} </div> </div> </section> {% endif %}{% endif %}.vitrine-produtos { padding: 4.8rem 0;
&__titulo { font-size: 3.2rem; margin-bottom: 3.2rem; text-align: center; }
&__lista { display: grid; gap: 1.6rem; grid-template-columns: repeat(2, 1fr);
@include media('md') { grid-template-columns: repeat(4, 1fr); } }}Abas com vitrines por coleção (tags dinâmicas)
Seção intitulada “Abas com vitrines por coleção (tags dinâmicas)”Cada aba representa uma coleção diferente. A tag é gerada automaticamente a partir do título de cada aba.
{ "title": "Abas de produtos", "description": "Uma coleção por aba.", "requires_external_content": { "type": "dynamic", "prefix": "home-tab-", "instructions": { "title": "Associe produtos às abas", "steps": [ "1. Confira a tag sugerida em cada aba", "2. Acesse [Tags]({{ADMIN_URL}}/tags) e [Produtos]({{ADMIN_URL}}/produtos)", "3. Vincule a tag aos produtos de cada aba" ] } }, "component_config": { "settings": {}, "content": { "items": [ { "title": { "type": "string", "title": "Título da aba" } } ] } }, "defaultContent": { "content": { "items": [ { "title": "Novidades" }, { "title": "Mais vendidos" } ] } }}{% assign content = component_configs.content %}{% assign tabs_markup = '' %}{% assign panels_markup = '' %}{% assign has_active_tab = false %}
{% for item in content.items %} {% if item.title != blank and item.tag_name != blank %} {% load_products tag: item.tag_name per_page: 8 %}
{% if products.size > 0 %} {% assign active_class = '' %} {% unless has_active_tab %} {% assign active_class = ' -active' %} {% assign has_active_tab = true %} {% endunless %}
{% capture tab %} <button type="button" class="abas-produtos__tab{{ active_class }}" data-tab="{{ forloop.index }}" >{{ item.title }}</button> {% endcapture %} {% assign tabs_markup = tabs_markup | append: tab %}
{% capture panel %} <div class="abas-produtos__painel{{ active_class }}" data-painel="{{ forloop.index }}" > <div class="abas-produtos__lista"> {% for product in products %} {% render 'partials/components/product_block', product: product, mobile: mobile, component_configs: product_block_config %} {% endfor %} </div> </div> {% endcapture %} {% assign panels_markup = panels_markup | append: panel %} {% endif %} {% endif %}{% endfor %}
{% if has_active_tab %} <section class="abas-produtos"> <div class="container"> <nav class="abas-produtos__nav">{{ tabs_markup }}</nav> {{ panels_markup }} </div> </section>{% endif %}JavaScript
Seção intitulada “JavaScript”const AbasProdutos = { init() { const tabs = document.querySelectorAll('.abas-produtos__tab'); const panels = document.querySelectorAll('.abas-produtos__painel'); if (!tabs.length) return;
tabs.forEach(tab => { tab.addEventListener('click', () => { const index = tab.dataset.tab; tabs.forEach(t => t.classList.remove('-active')); panels.forEach(p => p.classList.remove('-active')); tab.classList.add('-active'); document.querySelector(`.abas-produtos__painel[data-painel="${index}"]`) ?.classList.add('-active'); }); }); },};
export default AbasProdutos;Formulário de newsletter
Seção intitulada “Formulário de newsletter”Formulário com captação de e-mail. Exige cadastro prévio no Painel Administrativo antes de funcionar - veja Formulários.
{ "title": "Newsletter", "description": "Captação de e-mails.", "requires_external_content": { "type": "manual", "instructions": { "title": "Cadastro do formulário", "steps": [ "1. Acesse [Forms]({{ADMIN_URL}}/config/mensagens-e-avisos/forms)", "2. Crie um formulário com assunto `newsletter`", "3. Informe o e-mail do destinatário e salve antes de publicar" ] } }, "component_config": { "settings": { "background_color": { "type": "string", "title": "Cor de fundo", "default": "#03060B" }, "text_color": { "type": "string", "title": "Cor do texto", "default": "#FFFFFF" } }, "content": { "title": { "type": "string", "title": "Título" }, "description": { "type": "string", "title": "Descrição" }, "placeholder": { "type": "string", "title": "Placeholder do campo de e-mail" }, "cta_label": { "type": "string", "title": "Texto do botão" } } }, "defaultContent": { "settings": { "background_color": "#03060B", "text_color": "#FFFFFF" }, "content": { "title": "Fique por dentro", "description": "Receba novidades e ofertas exclusivas no seu e-mail.", "placeholder": "Seu e-mail", "cta_label": "Quero receber" } }}{% assign settings = component_configs.settings %}{% assign content = component_configs.content %}
<section class="newsletter" style="--newsletter-bg: {{ settings.background_color }}; --newsletter-color: {{ settings.text_color }};"> <div class="container newsletter__inner">
<div class="newsletter__texto"> {% if content.title != blank %} <h2 class="newsletter__titulo">{{ content.title }}</h2> {% endif %} {% if content.description != blank %} <p class="newsletter__descricao">{{ content.description }}</p> {% endif %} </div>
<form data-webform="newsletter" class="newsletter__form"> <fieldset> <input type="text" name="vnda" style="display:none;" tabindex="-1" autocomplete="off" /> <input type="hidden" name="key" value="{{ current_shop.subdomain }}-newsletter" /> <input type="hidden" name="reply_to" value="" />
<div class="newsletter__campo"> <input type="email" name="email" class="input" placeholder="{{ content.placeholder | default: 'Seu e-mail' }}" required /> <button type="button" class="action button-default"> {{ content.cta_label | default: 'Enviar' }} </button> </div>
<div class="messages" data-msg-retorno> <p class="msg-received" data-msg>Processando…</p> <p class="msg-received" data-msg-success>Cadastro realizado com sucesso!</p> <p class="msg-received" data-msg-error>Erro ao enviar. Tente novamente.</p> </div> </fieldset> </form>
</div></section>Mantenha reply_to vazio no markup. O comportamento padrão do formulário preenche esse campo com o e-mail informado antes do envio.
.newsletter { background: var(--newsletter-bg, #03060b); color: var(--newsletter-color, #fff); padding: 4.8rem 0;
&__inner { display: flex; flex-direction: column; gap: 3.2rem; align-items: center; text-align: center;
@include media('md') { flex-direction: row; text-align: left; } }
&__texto { flex: 1; }
&__titulo { font-size: 2.4rem; margin-bottom: 0.8rem; }
&__descricao { font-size: 1.4rem; opacity: 0.8; }
&__form { flex: 1; width: 100%; }
&__campo { display: flex; gap: 0.8rem;
.input { flex: 1; } }}JavaScript
Seção intitulada “JavaScript”const Newsletter = { init() {},};
export default Newsletter;Tipos avançados
Seção intitulada “Tipos avançados”Os exemplos de Botões e Ícones trazem a referência-base completa para esses contratos. Antes de publicar, compare-a à referência aprovada para a loja e preserve todos os símbolos e estados que ela utilizar.