Theme blocks
This page is for theme developers. It explains how a theme changes a storefront page without copying it: every page KahunaCart draws is split into named regions, and a theme overrides the regions it wants different and inherits the rest, including whatever a later release adds to the page.
For colors, radius and type, see Theming, which is still the cheapest way to make the shop look like your site. Reach for blocks when you need different markup, and for a template copy only when blocks can't do it.
Extend, don't copy
The plugin's templates are on Twig's path twice. Once by name, so a file in your theme with the same name replaces the plugin's. Once under the @kahunacart namespace, so that file can extend the plugin's page instead of replacing it.
A theme's checkout that adds a list of what's being bought above the totals is the whole file:
{# user/themes/my-theme/templates/kahunacart-checkout.html.twig #}
{% extends '@kahunacart/kahunacart-checkout.html.twig' %}
{% block kahunacart_checkout_summary_items %}
<ul class="my-summary-items">
{% for item in items %}<li>{{ item.title }} × {{ item.qty }}</li>{% endfor %}
</ul>
{% endblock %}
Everything else on the page is the plugin's, so the next release's checkout changes reach your store without you doing anything. A copied page stays frozen at the version you copied. The drift report (bin/plugin kahunacart templates:drift) can only tell you it has fallen behind.
Add-ons do the same under their own slug: @kahunacart-bookings, @kahunacart-affiliates, and so on.
Four ways to use a block
Replace it. Define the block in your file and your markup is drawn instead.
Wrap it. {{ parent() }} draws the plugin's version inside yours:
{% block kahunacart_processing_body %}
<div class="my-card">{{ parent() }}</div>
{% endblock %}
Wrapping is the safest change there is. The processing page's body holds four states (declined, held for review, waiting, timed out). A wrap keeps all four, plus any a later release adds.
Add a class. Every wrapper the scripts read has an empty _class block inside its class attribute. Fill it with a leading space:
{% block kahunacart_checkout_payment_step_class %} my-step my-step--pay{% endblock %}
Rearrange regions. Each big page has a _layout block holding its regions, and each checkout step has an inner block. Override one and draw the regions where you want them with {{ block('…') }}. Each call draws your version of that region if you have one, and the plugin's if not:
{% block kahunacart_checkout_payment %}
<div class="my-columns">
<div>
{{ block('kahunacart_checkout_payment_methods') }}
{{ block('kahunacart_checkout_order_fields') }}
{{ block('kahunacart_checkout_terms') }}
</div>
<aside>{{ block('kahunacart_checkout_summary') }}</aside>
</div>
{% endblock %}
A region you leave out isn't drawn. That's how you drop one on purpose, and it's also how one gets lost by accident. When you override a _layout block, check the block reference and call every region you mean to keep.
What you can't lose
A few things sit outside every block, so no theme can drop them by mistake:
- The step wrappers on checkout (
id="kahunacart-checkout-address",-shippingand-payment, each with itsdata-kc-step), which are outside the inner blocks you override. A_stepblock includes its wrapper, so override the inner block (kahunacart_checkout_address,kahunacart_checkout_payment) rather than the_steparound it. - The forms the cart's remove buttons post.
- The checkout form itself, with its nonce and
data-kc-step-strings. - The product and listing pages' structured data, and the flash messages on the checkout, cart, order, product, listing, category and search pages.
The scripts read some elements inside the regions: #kahunacart-checkout-summary, data-kc-place-order, data-kc-catalog-results and the rest. Keep them when you replace a region that holds one. For the checkout, see Payment totals in a checkout override.
Repeated parts are partials
One product card, one cart line and one receipt line are partials, not blocks. Override the partial and every instance changes, on every page that draws one, including the filter swap.
| Partial | Draws | Gets |
|---|---|---|
partials/kahunacart-product-card.html.twig |
One card in a product grid: the listing, a category, search, a filter swap | product, plus the page's own variables (theme, config, page) |
partials/kahunacart-cart-line.html.twig |
One cart line, on its own or as part of a set | row (row.item is the line, row.set its set or null), line_notes, order |
partials/kahunacart-cart-set-row.html.twig |
The heading row of a set in the cart | row (row.set), order |
partials/kahunacart-order-line.html.twig |
One line of the receipt | item, order |
partials/kahunacart-shipping-method.html.twig |
One delivery method at checkout | offer, chosen_method, kc_first, order |
partials/kahunacart-checkout-field.html.twig |
One checkout control | field |
A theme building its own row of products draws the same card the shop does:
{% for product in kahunacart_products(4) %}
{% include 'partials/kahunacart-product-card.html.twig' with {product: product} %}
{% endfor %}
Blocks in partials
Some regions are partials because the plugin also returns them on their own: the catalog results to a filter change, the shipping step to a change of address. Reach their blocks by extending the partial, at the same path in your theme:
{# user/themes/my-theme/templates/partials/kahunacart-catalog-results.html.twig #}
{% extends '@kahunacart/partials/kahunacart-catalog-results.html.twig' %}
{% block kahunacart_catalog_results_summary %}
<p class="my-count">{{ products|length }} products</p>
{% endblock %}
The swap fetches the same file, so your version is what comes back on every filter click too.
Forms that a theme wires its own script to (htmx and the like) have an empty _attrs block inside the opening tag:
{% extends '@kahunacart/partials/kahunacart-estimate.html.twig' %}
{% block kahunacart_estimate_form_attrs %} hx-post="{{ kahunacart_url('/cart') }}" hx-target="#kahunacart-estimate" hx-swap="outerHTML"{% endblock %}
Block reference
Indented names sit inside the one above them. Blocks marked empty draw nothing in the plugin. They are places for a theme to add something.
Checkout
Template: kahunacart-checkout.html.twig.
kahunacart_checkout_class: classes on the page's<section>kahunacart_checkout_head: the headingkahunacart_checkout_notices: what the store has to say about this order, and the bundle suggestionkahunacart_checkout_empty: what an empty cart sees instead of the formkahunacart_checkout_form_class: classes on the<form>kahunacart_checkout_layout: every region inside the formkahunacart_checkout_express: the express-pay zonekahunacart_checkout_address_check: the address suggestion panelkahunacart_checkout_address_step: the address step, wrapper includedkahunacart_checkout_address_step_class: classes on the step wrapperkahunacart_checkout_address: inside the stepkahunacart_checkout_fields: the contact, billing and delivery fieldsetskahunacart_checkout_address_actions: the Continue button and its "still needed" line
kahunacart_checkout_shipping: the delivery step (see the shipping step partial below)kahunacart_checkout_payment_step: the payment step, wrapper includedkahunacart_checkout_payment_step_class: classes on the step wrapperkahunacart_checkout_payment: inside the stepkahunacart_checkout_payment_methods: the payment choicekahunacart_checkout_order_fields: the order section's fields (notes and anything declared beside them)kahunacart_checkout_terms: the terms boxkahunacart_checkout_coupon_note: "Code applied"kahunacart_checkout_line_notes: line notes and backorder lineskahunacart_checkout_summary: the order summary,#kahunacart-checkout-summaryincludedkahunacart_checkout_summary_class: classes on the summarykahunacart_checkout_summary_items: empty, above the totalskahunacart_checkout_totals: the totalskahunacart_checkout_place_order: Place order and its "still needed" line
kahunacart_checkout_gift_card: store credit and gift cards, outside the form
Shipping step
Template: partials/kahunacart-checkout-shipping-step.html.twig.
kahunacart_checkout_shipping_step_class: classes on#kahunacart-checkout-shippingkahunacart_checkout_shipping_offers: the delivery optionskahunacart_checkout_shipping_actions: Continue to payment and its "still needed" line
Delivery options
Template: partials/kahunacart-shipping-offers.html.twig.
kahunacart_shipping_pickup: the collection pointskahunacart_shipping_methods: the delivery methods. One method ispartials/kahunacart-shipping-method.html.twig.
While a collection point is chosen, the delivery list is hidden disabled and an empty shipping_method_id is posted before it. An override of kahunacart_shipping_methods has to keep both, or a collected order is charged for delivery.
Cart
Template: kahunacart-cart.html.twig.
kahunacart_cart_class: classes on the page's<section>kahunacart_cart_head: the headingkahunacart_cart_payment_open: "a payment page is still open" and its cancel buttonkahunacart_cart_limit_notices: the quantity and order limit noticeskahunacart_cart_bundle: the bundle suggestionkahunacart_cart_empty: what an empty cart seeskahunacart_cart_layout: every region of a cart with something in itkahunacart_cart_lines: the update form and its tablekahunacart_cart_table_class: classes on the tablekahunacart_cart_table_head: the column headingskahunacart_cart_rows: the rows. One row is a partial (see above)kahunacart_cart_actions: Update cartkahunacart_cart_sale_tiers: where each quantity ladder standskahunacart_cart_summary: the summary columnkahunacart_cart_summary_class: classes on itkahunacart_cart_coupon: the discount code boxkahunacart_cart_totals: the totalskahunacart_cart_estimate: the shipping and tax estimatekahunacart_cart_checkout_link: the Checkout buttonkahunacart_cart_express: the express-pay zonekahunacart_cart_cross_sells: "Goes well with your order"
kahunacart_cart_account_link: the link to the account, for a signed-in customer
Product
Template: kahunacart-product.html.twig.
kahunacart_product_class: classes on the page's<article>kahunacart_product_layout: every region between the messages and the structured datakahunacart_product_breadcrumbskahunacart_product_titlekahunacart_product_media: the merchant's badge and the gallerykahunacart_product_summarykahunacart_product_descriptionkahunacart_product_samples: the preview blockkahunacart_product_up_sellskahunacart_product_lead: empty, above the buy box, for the price, stock or labelskahunacart_product_buy: the buy button and option pickerkahunacart_product_wishlist: Save for laterkahunacart_product_cross_sellskahunacart_product_attributes: the specification tablekahunacart_product_custom_fieldskahunacart_product_safety: the EU product safety sectionkahunacart_product_tagskahunacart_product_reviewskahunacart_product_related: "You might also like"kahunacart_product_back: Back to shop
Order and receipt
Template: kahunacart-order.html.twig.
kahunacart_order_class: classes on the page's<section>kahunacart_order_head: the headingkahunacart_order_layout: every region from the notices to the links. Add-ons draw license keys, bookings and the like inkahunacart_order_panels, so keep it when you rearrangekahunacart_order_noticeskahunacart_order_continue: back to the article a subscriber was readingkahunacart_order_meta: order number, payment reference, statuseskahunacart_order_offline: bank transfer instructionskahunacart_order_quote: Accept and Decline on a quotekahunacart_order_pickup: where to collect itkahunacart_order_items: the item table. One line is a partial (see above)kahunacart_order_totalskahunacart_order_tax: the customer's tax number and reverse-charge linekahunacart_order_fields: answers to the checkout's own fieldskahunacart_order_downloadskahunacart_order_refund: asking for a refundkahunacart_order_historykahunacart_order_panels: add-on panelskahunacart_order_account_offer: a guest's way into an accountkahunacart_order_links: invoice, PDF, orders, back to shop
Listing, category and search
Templates: kahunacart-products.html.twig, kahunacart-category.html.twig and kahunacart-search.html.twig.
kahunacart_products_class,kahunacart_category_class,kahunacart_search_class: classes on the page's<section>kahunacart_products_head: the heading and the shop page's own textkahunacart_products_search: the search boxkahunacart_products_layout: the sidebar and the resultskahunacart_products_nav: the sidebarkahunacart_products_results: the results region
kahunacart_category_breadcrumbskahunacart_category_head: the headingkahunacart_category_banner: the category's picturekahunacart_category_descriptionkahunacart_category_layout: the sidebar and the resultskahunacart_category_navkahunacart_category_results
kahunacart_category_back: Back to shopkahunacart_search_head: the headingkahunacart_search_form: the search boxkahunacart_search_layout: the sidebar and the resultskahunacart_search_nav
kahunacart_search_back: Back to shop
Results
Template: partials/kahunacart-catalog-results.html.twig.
kahunacart_catalog_results_class: classes on#kahunacart-catalog-resultskahunacart_catalog_results_summary: empty, under the filter chips, for a result count or a sort controlkahunacart_catalog_results_empty: what's said when nothing matcheskahunacart_catalog_results_grid: the product grid. One card is a partial (see above)
The grid itself (partials/kahunacart-product-grid.html.twig) has kahunacart_product_grid_class, for classes on its <ul>.
Account overview
Template: kahunacart-account.html.twig.
kahunacart_account_class: classes on the page's<section>kahunacart_account_head: the headingkahunacart_account_summary: empty, above the panels, for a row of figures. The page hasorder_count,download_countandcredit_balance_minor(store credit in the currency being browsed, null when credit is off)
Small pages
kahunacart_processing_class,kahunacart_processing_body: the payment return page, with one block per state inside the body:kahunacart_processing_declined,kahunacart_processing_held,kahunacart_processing_waitingandkahunacart_processing_timeoutkahunacart_closed_class,kahunacart_closed_body: the cart and checkout while the store is paused or coming soonkahunacart_denied_class,kahunacart_denied_body: a download that can't be givenkahunacart_currency_class,kahunacart_currency_body: the question before a currency switch empties the cart, withkahunacart_currency_questioninside the body around the sentence and the list of what would go (the form stays the plugin's)kahunacart_paywall_class,kahunacart_paywall_body: a paywalled article
Form attributes
kahunacart_search_form_attrs: inpartials/kahunacart-search.html.twigkahunacart_estimate_form_attrs: inpartials/kahunacart-estimate.html.twigkahunacart_bundle_swap_form_attrs,kahunacart_bundle_keep_form_attrs: inpartials/kahunacart-bundle-suggestions.html.twig
Block names are an interface
A block name is a promise to every theme that uses it. Blocks are added freely, in any release, because wrapping existing markup in a new block changes nothing for a theme that doesn't use it. A block is only renamed or removed in a major release, after a minor release that keeps the old name working beside the new one. The plugin's test suite fails if a block exists in a template without being listed on this page, or is listed here without existing.
Related
- Theming: the color, radius and type tokens, the class reference, and the notes for a checkout override
- Storefront: which template draws which route
- Extension zones: where add-ons draw on a page, as opposed to where themes do