Theming
This page is for Twig and CSS themers. It covers the --kahunacart-* custom properties, the classes the shipped stylesheet targets, and how to choose between a variable, your own CSS, and a template override.
For the routes, templates and Twig functions behind the markup, see Storefront.
What ships
KahunaCart enqueues two files, on its own storefront routes and nowhere else:
| File | What it does |
|---|---|
assets/kahunacart.css |
One plain CSS file. No Tailwind, no preprocessor, no external fonts, no images. |
assets/kahunacart-options.js |
One small script for the product-option picker. No framework, no build step, no dependencies, no network calls. |
Both are cache-busted by file mtime, so a plugin update takes effect without a hard refresh. Webhook and download responses exit before a page exists and never receive either file.
Every rule is scoped to .kahunacart, the wrapper class the shipped templates carry, so the stylesheet cannot repaint the rest of your site. Component rules wrap their outer selector in :where(), which contributes nothing to specificity, so a plain class selector in your own CSS beats most of what the plugin sets. Nothing in the file uses !important.
The stylesheet also handles :focus-visible rings on every control, tabular figures on money columns, prefers-reduced-motion, and cart tables that scroll inside themselves on a phone.
Note
The storefront is server-rendered HTML and works with scripting off, the option picker included. See The buy button and the option picker.
How token resolution works
The stylesheet uses internal --kc-* tokens. You never set those. Each token takes the first value that is defined, down this chain:
--kahunacart-* your override, site or theme
↓
--grav-* the theme's own design tokens, if it publishes them
↓
--theme-accent a generic accent hook, for the two accent tokens only
↓
neutral fallback derived from --kc-fg, or an absolute colour
If your theme already defines --grav-fg, --grav-border, --grav-bg-elevated and --grav-radius-md, the storefront picks them up and you write no CSS at all.
Borders, muted text and faint fills are all derived from --kahunacart-fg with color-mix(): borders at 13% and 24%, muted text at 68%, subtle text at 60%, faint fills at 3.5%. Retarget --kahunacart-fg and the whole neutral layer moves with it.
Tokens are declared on both :root and .kahunacart, so either selector works. Use .kahunacart to restyle the storefront without touching anything else on the site.
Custom property reference
Every hook, what it paints, and what it resolves to when you leave it alone.
| Property | Paints | Falls back to |
|---|---|---|
--kahunacart-fg |
Text, and the anchor the neutral ramp is derived from | --grav-fg → CanvasText |
--kahunacart-bg |
The page ground; used where a control must be opaque, such as a <select> option list |
--grav-bg → Canvas |
--kahunacart-border |
Card, table, fieldset and chip borders | --grav-border → 13% of fg |
--kahunacart-border-strong |
Input borders, the rule above the order total, button outlines | --grav-border-strong → 24% of fg |
--kahunacart-muted |
Secondary text: product summaries, field labels, totals labels | --grav-fg-muted → 68% of fg |
--kahunacart-subtle |
Tertiary text: table column heads, the cart remove control at rest | --grav-fg-subtle → 60% of fg |
--kahunacart-accent |
Ink and borders: hover states, the outline of a chosen shipping or payment row | --grav-purple-bright → --theme-accent → light-dark(#0b57d0, #7cacf8) |
--kahunacart-accent-strong |
The only token that fills a box behind a label: active category chip, Checkout and Place order buttons | --grav-purple → --theme-accent → light-dark(#0b57d0, #2563eb) |
--kahunacart-accent-ink |
Prices and product-title hover, where the accent is text on the page ground | --grav-purple → --kahunacart-accent |
--kahunacart-on-accent-strong |
The label colour on an accent-filled box | Computed from the fill's luma, black or white; #fff on older browsers |
--kahunacart-surface |
Faint panels: flash messages, download rows, the order meta block | --grav-bg-elevated → 3.5% of fg |
--kahunacart-surface-2 |
The deeper panel, used for the add-to-cart block | --grav-bg-subtle → --kahunacart-surface |
--kahunacart-card |
Product card fill; transparent by default so a patterned page background is not covered in near-invisible grey rectangles | --grav-bg-elevated → transparent |
--kahunacart-card-ratio |
The aspect ratio of a product card's image | 4 / 3 |
--kahunacart-input |
Form control fill | --grav-bg-subtle → transparent |
--kahunacart-swatch |
The fill of a colour swatch that has no colour stored on it | 12% of fg |
--kahunacart-radius |
Buttons, inputs, small panels | --grav-radius-md → 8px |
--kahunacart-radius-lg |
Cards, fieldsets, the buy block | --grav-radius-lg → 12px |
--kahunacart-font-mono |
Coupon codes and category counts | --grav-font-mono → ui-monospace, SFMono-Regular, Menlo, monospace |
--kahunacart-ease |
Every transition's easing | --grav-ease → ease |
--kahunacart-duration |
Every transition's duration | --grav-duration-2 → 160ms |
--kahunacart-success |
Applied coupon, discount lines on the receipt | --grav-success → #1d9d61 |
--kahunacart-warning |
Offline payment instructions on the confirmation page | --grav-warning → #b8860b |
--kahunacart-danger |
Error messages, hovering the cart's remove control, the download denial notice | --grav-danger → #c0392b |
Override the storefront colours
Six properties usually cover the whole job.
-
Open your theme's CSS file.
-
Add the properties you want to change:
CSS:root { --kahunacart-accent: #2f7d6f; /* hover ink, chosen-option outlines */ --kahunacart-accent-strong: #1d5e53; /* filled: Checkout, Place order, active chip */ --kahunacart-accent-ink: #1d5e53; /* prices */ --kahunacart-radius: 4px; --kahunacart-radius-lg: 6px; --kahunacart-card: #fff; /* product cards get a real fill */ } -
Reload a storefront page.
The storefront now renders in your colours and your corner geometry. Everything you did not name still follows the page.
Support your theme's dark mode
Redefine only what changes.
-
Add a block under your theme's dark-mode selector:
CSShtml[data-theme="dark"] { --kahunacart-fg: #e6e8ee; --kahunacart-bg: #14161c; --kahunacart-card: #1b1e26; --kahunacart-accent: #7cc9b8; } -
Reload a storefront page in dark mode.
The neutral ramp follows the new --kahunacart-fg. You do not have to restate borders, muted text or faint fills.
Tip
If your theme flips dark mode without setting color-scheme, the stylesheet detects the common conventions (html[data-theme="dark"], html[data-bs-theme="dark"], html.dark, body[data-theme="dark"], body.dark) and sets color-scheme on the storefront. This only matters if you set no tokens of your own.
Turn off the built-in stylesheet
- Set
builtin_csstofalse, or use the matching toggle on the General tab of the plugin's admin settings. - Reload a storefront page.
No stylesheet is enqueued and the kahunacart-* classes are yours to style from scratch. The switch does not turn off the option picker's script, which a theme still needs to resolve a variant.
Important
Turn the stylesheet off only when your theme wants total control of storefront styling, or when you have replaced every shipped template and the classes are no longer rendered. Otherwise leave it on and override what you need, so you keep the focus states, tabular figures, reduced-motion handling and small-screen table behaviour.
The buy button and the option picker
partials/kahunacart-buy-button.html.twig is the partial themes include most. It always renders both of these:
- The option widgets, when the product defines axes. Each axis gets a dropdown, a row of button pills, or colour swatches, plus a
<script type="application/json">carrying the picker's data. They shiphidden. - The plain
<select name="variant_id">, which is the field the form posts.
assets/kahunacart-options.js reveals the widgets, visually hides the select, writes the resolved variant into it, and fires a change event on it. Three consequences follow:
- The form works with JavaScript off. A browser that never loads the script posts exactly what it always did.
- The POST contract is unchanged:
variant_idandqty. - A theme listening for
changeon the select keeps working, whichever control the customer used.
The template variables are product, variants, options and options_payload. kahunacart_product('slug') sets all four.
Important
If you build the variable yourself, pass options (the presented axes) and options_payload (the encoded JSON). Without them the widgets do not render and the plain select stands.
Colour swatches read their colour from an inline --kc-swatch custom property, set on .kahunacart-option-choice and painted by .kahunacart-option-dot. Restyling a swatch is a normal CSS rule.
Render the buy button outside the storefront routes
A quick-view modal or a product card with an inline buy form sits on a route where the script has not been enqueued for you.
-
Add the script in your template:
TWIG{% do assets.addJs('plugin://kahunacart/assets/kahunacart-options.js', {loading: 'defer'}) %} -
If you inject the buy form after page load, call
window.kahunacartOptions.start(container).
The picker is now active on that page.
Style the catalog sidebar, tree and tags
The catalog sidebar resolves through the same token chain as the rest of the storefront, so a theme that has set --kahunacart-accent and --kahunacart-fg gets the tree, the facet panel, the breadcrumb and the subcategory cards in its own colours.
The two-column layout only exists when there is a sidebar. The wrapper is not emitted on a store with no categories and no tags, so on a flat catalog these rules never apply. See Storefront. Below 40rem the layout collapses to one column with the sidebar above the listing.
The tree is <details> and <summary> with the browser's own disclosure marker, and the summary is display: list-item. ::marker is coloured from --kc-subtle. The indent is padding on the nested list rather than a margin on the item, so hover and current-page backgrounds run the full width of the rail at every depth.
Tags are not styled as category chips: a category chip goes to a place in the catalog, a tag goes to a filtered listing. If you restyle one, keep the two distinguishable.
Choose between a variable, extra CSS, and a template override
Three levels, cheapest first.
| Level | Use it when | Update cost |
|---|---|---|
| Set a custom property | You are changing colour, radius, typeface or motion | None |
| Write your own CSS rules | You want different spacing, a different card layout, or a component that looks structurally unlike what ships | None |
| Override a template | The markup is wrong for you: a missing product image, an extra checkout field, a different element order for screen readers, or your own class names | The copy is frozen at the version you took it from |
Plugin rules sit at low specificity, so a plain .kahunacart-product-card { … } in your theme wins. If you need !important, report it as a bug.
To override a template, copy the file from the plugin's templates/ into user/themes/<your-theme>/templates/ at the same relative path and edit it. The plugin appends its own directory to Twig's search path, so your theme wins. See Storefront for the full template list.
Warning
An overridden template does not receive later KahunaCart changes to that file, including added fields and accessibility fixes. Override markup when you need markup, not when you need a colour.
Class reference
What the stylesheet targets, grouped by page. Every class here is rendered by the shipped templates.
| Area | Classes |
|---|---|
| Wrapper | kahunacart on every page, plus one of kahunacart-products, kahunacart-category, kahunacart-product, kahunacart-cart, kahunacart-checkout, kahunacart-order, kahunacart-processing, kahunacart-denied, kahunacart-account, kahunacart-search-results, kahunacart-invoice |
| Listing | kahunacart-product-grid, kahunacart-product-card, kahunacart-card-link, kahunacart-card-media, kahunacart-price, kahunacart-listing-more |
| Categories | kahunacart-categories, kahunacart-category-chip (.is-active), kahunacart-category-count, kahunacart-category-description, kahunacart-category-media |
| Catalog layout | kahunacart-catalog-layout, kahunacart-catalog-main, plus kahunacart-has-nav on the page wrapper when there is a sidebar |
| Category tree | kahunacart-catalog-nav, kahunacart-nav-title, kahunacart-cat-tree, kahunacart-cat-list, kahunacart-cat-item, kahunacart-cat-branch, kahunacart-cat-summary, kahunacart-cat-link, kahunacart-cat-count, plus the state classes is-current and is-ancestor |
| Filter panel | kahunacart-filters, kahunacart-filter-group, kahunacart-filter-options, kahunacart-filter-option, kahunacart-filter-count, kahunacart-filter-price, kahunacart-filter-range, kahunacart-filter-field, kahunacart-filter-dash, kahunacart-filter-actions, kahunacart-filter-clear |
| Search | kahunacart-search, kahunacart-search-field, kahunacart-search-summary, kahunacart-search-more |
| Breadcrumb | kahunacart-breadcrumbs, kahunacart-breadcrumb-list, kahunacart-breadcrumb-item |
| Subcategory cards | kahunacart-subcategory-grid, kahunacart-subcategory-card (.has-image), kahunacart-subcategory-link, kahunacart-subcategory-media, kahunacart-subcategory-title, kahunacart-subcategory-count |
| Tags | kahunacart-tags, kahunacart-tags-title, kahunacart-tag-list, kahunacart-tag |
| Product | kahunacart-summary, kahunacart-description, kahunacart-buy, kahunacart-qty, kahunacart-compare-price |
| Product gallery | kahunacart-gallery, kahunacart-gallery-main, kahunacart-gallery-slide, kahunacart-gallery-thumbs |
| Option picker | kahunacart-options, kahunacart-option, kahunacart-option-name, kahunacart-option-select, kahunacart-option-choices, kahunacart-option-choice with kahunacart-option-pill or kahunacart-option-swatch, kahunacart-option-dot, kahunacart-option-label, kahunacart-option-status, kahunacart-variant-picker, plus the state classes is-selected, is-unavailable and is-sold-out |
| Cart | kahunacart-cart-table, kahunacart-remove, kahunacart-checkout-link, kahunacart-cart-account |
| Coupon | kahunacart-coupon, kahunacart-coupon-code, kahunacart-coupon-remove, kahunacart-coupon-active |
| Totals | kahunacart-totals, kahunacart-adjustment with --discount, --shipping or --tax, kahunacart-total |
| Checkout | kahunacart-checkout-form, kahunacart-checkbox, kahunacart-provider, kahunacart-shipping-methods, kahunacart-shipping-method, kahunacart-shipping-hint, kahunacart-shipping-none, kahunacart-place-order |
| Order | kahunacart-order-meta, kahunacart-order-items, kahunacart-order-links, kahunacart-offline-instructions |
| Order status badges | kahunacart-status with kahunacart-status--payment, kahunacart-status--order or kahunacart-status--fulfillment, plus kahunacart-status--{value} for the status value itself |
| Downloads | kahunacart-downloads, kahunacart-download-list, kahunacart-download, kahunacart-download-product, kahunacart-download-meta |
| Customer account | kahunacart-account-nav, kahunacart-account-nav-item (--current, --logout), kahunacart-account-panel, kahunacart-account-identity, kahunacart-account-orders, kahunacart-account-order-actions, kahunacart-account-order-history, kahunacart-account-addresses, kahunacart-account-downloads, kahunacart-account-empty |
| Addresses | kahunacart-address, kahunacart-address-form, kahunacart-address-grid, kahunacart-address-list, kahunacart-address-card (--default), kahunacart-address-flags, kahunacart-address-flag, kahunacart-address-actions, kahunacart-address-edit, kahunacart-shipping-address |
| Address check | kahunacart-address-check, kahunacart-address-check-title, kahunacart-address-check-lead, kahunacart-address-check-warning, kahunacart-address-check-options, kahunacart-address-check-option, kahunacart-address-check-label, kahunacart-address-check-suggested, kahunacart-address-check-accept, kahunacart-address-check-keep |
| Invoice | kahunacart-invoice-toolbar, kahunacart-invoice-header, kahunacart-invoice-store, kahunacart-invoice-parties, kahunacart-invoice-party, kahunacart-invoice-meta, kahunacart-invoice-items, kahunacart-invoice-tax-id, kahunacart-invoice-email, kahunacart-invoice-footer |
| Denial | kahunacart-denied-message, plus kahunacart-denied--{reason} where reason is not_found, revoked, expired, exhausted, order_refunded or unreadable |
| Messages | kahunacart-messages, kahunacart-message with --error, --warning, --info or --success |
| Utility | kahunacart-hint, kahunacart-link-button (--danger), kahunacart-visually-hidden, kahunacart-no-print |
The --{modifier} variants on adjustments, denials, statuses and messages are all rendered whether or not the shipped stylesheet paints them, so you can branch on any of them from your own CSS. The stylesheet leaves most denial reasons looking identical.
Related
- Storefront — routes, templates and Twig functions
- Products and catalog — what the catalog pages render
- Multilanguage — what a theme needs when the store runs more than one language