Guide
Cookbook
Four complete sections. Each one was dropped into a fresh copy of the starter
theme and checked with validate (zero errors, zero warnings); the output is in
_verified.txt. Copy the files, add the section type to
templates/index.sneferu, and run dev.
<hk-sections />
<hk-schema>{"sections":[
{"type":"promo-hero"},
{"type":"product-grid"},
{"type":"pricing"},
{"type":"bento"}
]}</hk-schema>All four use the base component styles from
chapter 5. Save them as
assets/base.css:
.sn-btn { display: inline-flex; align-items: center; justify-content: center; min-height: 44px;
padding-inline: 20px; border-radius: 10px; font-weight: 600; text-decoration: none;
border: 2px solid var(--hk-color-accent); }
.sn-btn-solid { background: var(--hk-color-accent); color: var(--hk-color-background); }
.sn-btn-outline { background: transparent; color: var(--hk-color-accent); }
.sn-btn-ghost { background: transparent; border-color: transparent; color: inherit; }
.sn-badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 13px; font-weight: 600; }
.sn-badge-accent { background: var(--hk-color-accent); color: var(--hk-color-background); }
.sn-badge-neutral { background: color-mix(in srgb, var(--hk-color-text) 10%, transparent); }
.sn-product-card { display: flex; flex-direction: column; gap: 8px; color: inherit; text-decoration: none; }
.sn-product-card img { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; border-radius: 12px; }
.sn-was { opacity: .6; margin-inline-start: 6px; }(a) Hero with a token background and a call to action
A full-width hero whose background is a colour setting defaulting to the
primary token, an optional eyebrow badge, a title that scales down on tablet
and mobile, and two buttons that stack on mobile.
sections/promo-hero.sneferu:
<hk-section background="{{ section.settings.background }}" padding-y="120" tablet-padding-y="88" mobile-padding-y="56" class="promo-hero">
<hk-stack gap="20" mobile-gap="12" align="start">
{% if section.settings.eyebrow %}<hk-badge text="{{ section.settings.eyebrow }}" tone="neutral" />{% endif %}
<hk-heading text="{{ section.settings.title }}" level="h1" size="60" tablet-size="44" mobile-size="32" color="theme:background" />
<hk-text text="{{ section.settings.subtitle }}" size="20" mobile-size="16" color="theme:background" />
<hk-stack direction="row" mobile-direction="column" gap="12">
<hk-button label="{{ section.settings.cta_label }}" href="{{ section.settings.cta_link }}" size="lg" />
<hk-button label="{{ t('browse_all') }}" href="/collections/all" variant="outline" size="lg" />
</hk-stack>
</hk-stack>
</hk-section>
<hk-schema>{
"name": { "ar": "واجهة ترويجية", "en": "Promo hero" },
"version": 1,
"settings": {
"eyebrow": { "type": "text", "label": { "ar": "سطر صغير فوق العنوان", "en": "Eyebrow" }, "default": "عرض الأسبوع", "maxLength": 40 },
"title": { "type": "text", "label": { "ar": "العنوان", "en": "Title" }, "default": "كل اللي محتاجه في مكان واحد", "maxLength": 80 },
"subtitle": { "type": "textarea", "label": { "ar": "الوصف", "en": "Subtitle" }, "default": "توصيل لكل مصر والدفع عند الاستلام", "maxLength": 200 },
"cta_label": { "type": "text", "label": { "ar": "نص الزر", "en": "Button label" }, "default": "اطلب دلوقتي", "maxLength": 30 },
"cta_link": { "type": "link", "label": { "ar": "رابط الزر", "en": "Button link" }, "default": "/products" },
"background": { "type": "color", "label": { "ar": "الخلفية", "en": "Background" }, "default": "theme:primary" }
}
}</hk-schema>Add to locales/ar.json and locales/en.json:
{
"browse_all": "تصفح كل المنتجات"
}{
"browse_all": "Browse all products"
}Things to notice:
"default": "theme:primary"on acolorsetting: the merchant can keep the token or pick any hex colour;background="{{ section.settings.background }}"takes both.color="theme:background"keeps the text readable whatever the token values are.{% if section.settings.eyebrow %}: an empty eyebrow is falsy, so the badge disappears instead of rendering empty.cta_linkis alinksetting; an unsafe value (javascript:…) renders as#.
(b) Pricing tiers: three cards, a "best value" badge, 3 → 1 columns
Three cards side by side on desktop, one per row on tablet and mobile. The
merchant picks which tier is the best value; that card gets a badge and an
accent outline. Prices are numbers in piastres and printed with money, so
29900 shows as 299.00 ج.م / EGP 299.00.
sections/pricing.sneferu:
<hk-section>
<hk-stack gap="32">
<hk-heading text="{{ section.settings.heading }}" level="h2" size="36" mobile-size="28" align="center" />
<hk-grid columns="3" tablet-columns="1" gap="24" class="tiers">
{% render 'tier-card', name: section.settings.tier1_name, price: section.settings.tier1_price, features: section.settings.tier1_features, link: '/pages/plans', best: section.settings.best == '1', variant: 'outline' %}
{% render 'tier-card', name: section.settings.tier2_name, price: section.settings.tier2_price, features: section.settings.tier2_features, link: '/pages/plans', best: section.settings.best == '2', variant: 'solid' %}
{% render 'tier-card', name: section.settings.tier3_name, price: section.settings.tier3_price, features: section.settings.tier3_features, link: '/pages/plans', best: section.settings.best == '3', variant: 'outline' %}
</hk-grid>
</hk-stack>
</hk-section>
<hk-schema>{
"name": { "ar": "باقات الأسعار", "en": "Pricing tiers" },
"version": 1,
"settings": {
"heading": { "type": "text", "label": { "ar": "العنوان", "en": "Heading" }, "default": "اختار الباقة المناسبة" },
"tier1_name": { "type": "text", "label": { "ar": "اسم الباقة ١", "en": "Tier 1 name" }, "default": "أساسي" },
"tier1_price": { "type": "number", "label": { "ar": "سعر الباقة ١ (قرش)", "en": "Tier 1 price (piastres)" }, "default": 9900, "min": 0 },
"tier1_features": { "type": "textarea", "label": { "ar": "مميزات الباقة ١", "en": "Tier 1 features" }, "default": "متجر واحد · ١٠٠ منتج" },
"tier2_name": { "type": "text", "label": { "ar": "اسم الباقة ٢", "en": "Tier 2 name" }, "default": "بيزنس" },
"tier2_price": { "type": "number", "label": { "ar": "سعر الباقة ٢ (قرش)", "en": "Tier 2 price (piastres)" }, "default": 29900, "min": 0 },
"tier2_features": { "type": "textarea", "label": { "ar": "مميزات الباقة ٢", "en": "Tier 2 features" }, "default": "منتجات بلا حدود · دومين خاص" },
"tier3_name": { "type": "text", "label": { "ar": "اسم الباقة ٣", "en": "Tier 3 name" }, "default": "متقدم" },
"tier3_price": { "type": "number", "label": { "ar": "سعر الباقة ٣ (قرش)", "en": "Tier 3 price (piastres)" }, "default": 59900, "min": 0 },
"tier3_features": { "type": "textarea", "label": { "ar": "مميزات الباقة ٣", "en": "Tier 3 features" }, "default": "كل حاجة في بيزنس · دعم أولوية" },
"best": { "type": "select", "label": { "ar": "الباقة الأفضل قيمة", "en": "Best value tier" }, "options": ["1", "2", "3"], "default": "2" }
}
}</hk-schema>snippets/tier-card.sneferu:
<hk-card class="tier" data-best="{{ best }}" padding="28" mobile-padding="20" radius="16" border="true" shadow="sm">
<hk-stack gap="12">
{% if best %}<hk-badge text="{{ t('best_value') }}" tone="accent" />{% endif %}
<hk-heading text="{{ name }}" level="h3" size="24" />
<p class="tier-price"><strong>{{ price | money }}</strong> <small>{{ t('per_month') }}</small></p>
<hk-text text="{{ features }}" size="15" />
<hk-button label="{{ t('choose_plan') }}" href="{{ link }}" variant="{{ variant }}" />
</hk-stack>
</hk-card>assets/pricing.css:
.tier[data-best="true"] { outline: 2px solid var(--hk-color-accent); outline-offset: -2px; }
.tier-price { margin: 0; font-size: 28px; }
.tier-price small { font-size: 14px; opacity: .7; }Add to locales/ar.json and locales/en.json:
{
"best_value": "أفضل قيمة",
"per_month": "/ شهريًا",
"choose_plan": "اختار الباقة"
}{
"best_value": "Best value",
"per_month": "/ month",
"choose_plan": "Choose plan"
}Things to notice:
- The card markup lives once in a snippet; the section renders it three times with different arguments.
best: section.settings.best == '1'passes a boolean. The snippet uses it in{% if best %}and asdata-best="{{ best }}"("true"/"false"), and the CSS styles[data-best="true"]. A dynamicstyleattribute is not allowed, sodata-*+ CSS is the way to style by state.columns="3" tablet-columns="1"onhk-gridis the whole responsive layout.- A version where the merchant adds and removes tiers would make each tier a block (see chapter 3).
(c) Product grid from collection('featured') with a heading
A heading with a "view all" link on the same row, then up to N products from the
featured collection, with an empty state.
sections/product-grid.sneferu:
<hk-section padding-y="72" mobile-padding-y="40">
<hk-stack gap="24">
<hk-stack direction="row" justify="between" align="center">
<hk-heading text="{{ section.settings.heading }}" level="h2" size="32" mobile-size="24" />
{% if section.settings.show_link %}<hk-button label="{{ t('view_all') }}" href="/collections/featured" variant="ghost" size="sm" />{% endif %}
</hk-stack>
{% if collection('featured').products %}
<hk-product-grid products="{{ collection('featured').products }}" limit="{{ section.settings.count }}" columns="4" tablet-columns="3" mobile-columns="2" />
{% else %}
<hk-text text="{{ t('collection_empty') }}" align="center" />
{% endif %}
</hk-stack>
</hk-section>
<hk-schema>{
"name": { "ar": "شبكة منتجات", "en": "Product grid" },
"version": 1,
"settings": {
"heading": { "type": "text", "label": { "ar": "العنوان", "en": "Heading" }, "default": "الأكثر مبيعًا" },
"count": { "type": "range", "label": { "ar": "عدد المنتجات", "en": "Products to show" }, "default": 8, "min": 2, "max": 24, "step": 1 },
"show_link": { "type": "boolean", "label": { "ar": "إظهار «عرض الكل»", "en": "Show “View all”" }, "default": true }
}
}</hk-schema>Add to locales/ar.json and locales/en.json:
{
"view_all": "عرض الكل",
"collection_empty": "مفيش منتجات هنا لسه"
}{
"view_all": "View all",
"collection_empty": "No products here yet"
}Things to notice:
products="{{ collection('featured').products }}"passes the product array itself, not text: an attribute that is exactly one{{ }}keeps the value's type.limit="{{ section.settings.count }}"comes from arangesetting clamped to 2-24;hk-product-griditself capslimitat 48.{% if collection('featured').products %}is false for an empty or missing collection (empty arrays are falsy), so the empty state shows.- The handle
featuredis written in the template: lookups take literal arguments only. Use one section per collection, or ask the store to fill thefeaturedcollection.
(d) Bento grid with mixed spans
A four-column bento: one large tile spanning 2 × 2, a wide tile, two small tiles, then four product tiles where the first is wide. Two columns on tablet, one on mobile.
sections/bento.sneferu:
<hk-section>
<hk-grid columns="4" tablet-columns="2" mobile-columns="1" gap="16" class="bento">
<hk-card class="bento-wide bento-tall" background="theme:primary" padding="32" mobile-padding="20" radius="20">
<hk-stack gap="12">
<hk-heading text="{{ section.settings.lead_title }}" level="h2" size="40" mobile-size="28" color="theme:background" />
<hk-text text="{{ section.settings.lead_text }}" color="theme:background" />
<hk-button label="{{ t('shop_now') }}" href="/products" />
</hk-stack>
</hk-card>
<hk-card class="bento-wide" background="theme:accent" radius="20">
<hk-heading text="{{ t('bento_delivery') }}" level="h3" size="22" color="theme:background" />
</hk-card>
<hk-card border="true" radius="20">
<hk-heading text="{{ t('bento_cod') }}" level="h3" size="20" />
</hk-card>
<hk-card border="true" radius="20">
<hk-heading text="{{ t('bento_returns') }}" level="h3" size="20" />
</hk-card>
{% for p in collection('featured').products limit: 4 %}
<hk-card class="bento-product" data-first="{{ forloop.first }}" border="true" radius="20" padding="12">
<hk-product-card product="{{ p }}" />
</hk-card>
{% endfor %}
</hk-grid>
</hk-section>
<hk-schema>{
"name": { "ar": "شبكة بينتو", "en": "Bento grid" },
"version": 1,
"settings": {
"lead_title": { "type": "text", "label": { "ar": "العنوان الرئيسي", "en": "Lead title" }, "default": "جديد الموسم وصل" },
"lead_text": { "type": "textarea", "label": { "ar": "النص الرئيسي", "en": "Lead text" }, "default": "تشكيلة جديدة كل أسبوع" }
}
}</hk-schema>assets/bento.css:
.bento { grid-auto-rows: minmax(160px, auto); grid-auto-flow: dense; }
.bento-wide { grid-column: span 2; }
.bento-tall { grid-row: span 2; }
@media (max-width: 639px) {
.bento-wide, .bento-tall { grid-column: auto; grid-row: auto; }
}
.bento-product[data-first="true"] { grid-column: span 2; }
@media (max-width: 639px) { .bento-product[data-first="true"] { grid-column: auto; } }Add to locales/ar.json and locales/en.json:
{
"shop_now": "تسوق دلوقتي",
"bento_delivery": "توصيل لكل المحافظات",
"bento_cod": "الدفع عند الاستلام",
"bento_returns": "استرجاع خلال ١٤ يوم"
}{
"shop_now": "Shop now",
"bento_delivery": "Delivery to every governorate",
"bento_cod": "Cash on delivery",
"bento_returns": "14-day returns"
}Things to notice:
hk-gridsets the columns; spans come from your CSS through theclasspassthrough (bento-wide,bento-tall).grid-auto-flow: densefills the gaps the large tiles leave.- Inside the loop,
data-first="{{ forloop.first }}"marks the first product so CSS can widen it.{% %}directives cannot go inside an attribute value; use{{ }}anddata-*instead. limit: 4keeps the loop small and predictable.- On mobile every span resets to one cell, so the tiles stack in source order.
