Developer docs  /  Themes

Snippets reference

attribute-tile.fluid

Renders: one feature tile — icon, name, optional space count — linking to/t/spaces?attributes={{ Attribute.Id }}. Used exclusively bysections/attributes.fluid.

Data: expects Attribute (from the caller's loop: Id, Name,SpaceCount) and the caller's Section (it scans Section.Blocks for anicon block whose Settings.attributeId == Attribute.Id, and readsSection.Settings.showCounts). Icon precedence: block image → block iconkey (via | attr_icon_key | raw) → automatic keyword match on the name (via| attr_icon | raw).

Preserve: the variable names Attribute and Section; the?attributes={id} link (feeds the spaces listing's validated server-sideattribute filter); attr_icon / attr_icon_key must be piped through| raw (they return SVG markup). Both filters read icon maps loaded fromschema/attribute-icons.json — ship that file (keys icons, keywords,default) or every tile falls back to the built-in check SVG.

category-card.fluid

Renders: a simple card with the category's emoji/icon, name and"From $X/{duration}". Used by sections/category-tabs.fluid.

Data: expects item (a category: Icon, Name, Price,DurationType); uses the dollar filter.

Preserve: variable name item; the card / card-body classes.

google-review-card.fluid

Renders: the body of one Google review card — initials avatar,name, time-ago, the Google "G" logo, 1–5 star row, quote. Shared by all threestyles of sections/google-reviews.fluid (the section supplies the outer.testimonial-card.gr-card... wrapper div).

Data: expects item (a review block): item.Settings.name,item.Settings.quote, item.Settings.rating (default 5),item.Settings.timeAgo.

Preserve: variable name item; it renders card contents only — thecaller owns the wrapper (that wrapper's .testimonial-card class is what thecarousel JS measures). gr-stars / gr-star--empty star classes and thearia-label="{{ rating }} out of 5 stars" accessibility line.

location-card.fluid

Renders: one location card — image (or map-pin placeholder), name,address, optional phone, "View {Spaces} →" — the whole card is a link to/t/locations/{{item.Name | url_encode}}/spaces. Used bysections/locations-strip.fluid and templates/locations.fluid.

Data: expects item (a location: Name, Address, Image,PhoneNumber); Organisation.SpaceName for the CTA.

Preserve: variable name item; the URL-encoded name-based link (thebackend resolves location slugs by name, spaces↔dashes tolerated). Phonerenders only when set — admins hide it by clearing the location's phonefield, no template change.

section-style.fluid

Renders: an attribute, not an element — it emitsstyle="…" for the section wrapper <div> when any of the six sharedstyle-override settings are set on the section. This is the application halfof ThemeV2SectionStyleOverrides (the schema half lives inAdminThemeV2Service.GetSectionSchemasAsync).

Data: Section.Settings.overrideTextScale, overrideTextColor,overrideHeadingColor, overrideBackgroundColor, overrideButtonColor,overrideButtonTextColor.

How it works: overrides re-scope the theme's global CSS variables on thewrapper, so everything inside the section that reads --text-color,--heading-color, --bg-color / --surface-color, --button-bg /--button-hover-bg, --button-text follows automatically — no per-sectionCSS:

{%- if so.overrideTextColor and so.overrideTextColor != "" -%}--text-color:{{ so.overrideTextColor }};color:{{ so.overrideTextColor }};{%- endif -%}
{%- if so.overrideBackgroundColor and so.overrideBackgroundColor != "" -%}--bg-color:{{ so.overrideBackgroundColor }};--surface-color:{{ so.overrideBackgroundColor }};background:{{ so.overrideBackgroundColor }};{%- endif -%}

Preserve: must be included inside the wrapper div's tag(<div data-alyta-section=…{% include 'section-style' %}>) on everysection-rendering template. It's written with whitespace-trimming tags({%- -%}) so it emits either a single clean style="…" or nothing. Acustom theme's components must read the CSS variables above (rather thanhard-coding colours) for the overrides to have any effect. Empty string means"inherit the global theme setting".

space-card.fluid

Renders: the standard space listing card — badge (Available/Occupied),image or placeholder, category + area / dimensions, attribute tags, priceblock (with full promotion strikethrough support), location line, and theCTA stack (View + Get a free quote). Used by the space-listing snippet (allspaces pages + embed), sections/space-grid.fluid andsections/featured-space.fluid.

Data: expects item (a ThemeV2SpaceData); also reads Settings(HidePricing, ShowBookOnline, ShowGetQuote), Organisation.SpaceName,Request.IsEmbed and the caller-set EmbedInlineDetail flag.

Preserve — this is the most contract-heavy file in the theme:

  1. Filter/sort data attributes on the root <article class="space-card"> — read by applyFilters, applySort, monthlyPrice and initSpaceGridSections in theme.js:

<article class="space-card"
data-location="{{item.LocationName}}"
data-category="{{item.CategoryName}}"
data-price="{{item.Price}}"
data-duration="{{item.DurationType}}"
data-width="{{item.Width}}"
data-length="{{item.Length}}"
data-attributes="{% for attr in item.Attributes %}{{ attr }}|{% endfor %}">

data-attributes is pipe-delimited (trailing | is fine — JS trims and filters empties). data-price + data-duration feed the monthly-equivalent price conversion used by the price bands and sorting; data-width/data-length feed size sorting. The JS also writes data-match="0|1" onto cards — don't use that name yourself.

  1. CTA hooks: - Detail CTA: on embed pages that set EmbedInlineDetail (only templates/embed-spaces.fluid does) render <button class="space-card__cta js-view-space">; everywhere else render a link to the UId: <a href="/t/spaces/{{item.UId}}" class="space-card__cta">. js-view-space triggers showSpaceDetailInline() which clones .space-card__visual, .space-card__body and .space-card__price-block from the clicked card — keep those class names or the inline detail view renders empty. - Quote CTA: class="js-quote-btn" with data-space-id, data-name, data-category, data-price (omit when Settings.HidePricing), data-durationopenQuoteModal() reads all five from dataset (delegated document-level click handler, so the button works inside cloned/inline content too). - Both CTAs are gated by Settings.ShowBookOnline / Settings.ShowGetQuote.
  2. Pricing rules: wrap all price markup in {% unless Settings.HidePricing %}; render the promotion strikethrough layout only when item.PromotionPrice exists and item.PromotionRuleType == "MinimumRequirement"; always render the plain price line otherwise (the dollar filter falls back to $0.00 — a priceless space must not collapse the block). PromotionPrice == 0 renders FREE<sup>*</sup>.
  3. Area is width × length ({{ item.Width | times: item.Length | round: 0 }}m²), matching the detail page.

space-listing.fluid

Renders: the complete shared spaces listing — filter sidebar (Type select,price bands, dynamic Features checkboxes, active-filter pills, reset), mobilefilter toggle, results bar (live count + sort select), the card grid,client-side pagination and both empty states. Included bytemplates/spaces.fluid (full page) and templates/embed-spaces.fluid(chrome-less embed) so both share one implementation.

Data: Spaces, Categories, Organisation, Request(FilterCategoryId, FilterLocationName). Iterates Spaces with loopvariable item into {% include 'space-card' %}.

Preserve — ids and hooks (single instance per page; everything isid-based):

  • #filterSidebar + filter-sidebar--open classConsumer in theme.js: toggleMobileFilters()
  • .js-toggle-mobile-filters (close ✕ + mobile toggle button)Consumer in theme.js: delegated click
  • #resetAllFilters, .js-reset-filtersConsumer in theme.js: resetAllFilters() — also clears the URL-driven category/location/attributes params and reloads
  • #filterPillsConsumer in theme.js: rebuilt by updateFilterPills() on every filter change
  • #categorySelectConsumer in theme.js: change → ?category={id} URL navigation (server re-projects)
  • input.js-filter-cb with data-filter="price" and value="min-max"Consumer in theme.js: price bands; values 0-100, 100-200, 200-500, 500-99999 are compared against the monthly-equivalent price
  • #attributeFilterGroup (starts display:none) + #attributeFiltersConsumer in theme.js: initSpaceFilters() builds Feature checkboxes here from card data-attributes, un-hides the group when any exist
  • #filterMobileToggle + #filterCountConsumer in theme.js: active-filter badge
  • #visibleCountConsumer in theme.js: live results count
  • #sort-select with option values priceAsc/priceDesc/sizeAsc/sizeDescConsumer in theme.js: applySort(); initial value from ?sort=, persisted via history.replaceState
  • #spaceListConsumer in theme.js: the card container all filter/sort/paging code queries
  • #spacePagination (starts display:none)Consumer in theme.js: client-side pager (9 per page, SPACE_PAGE_SIZE); its mere presence enables paging
  • #noFilterResults (starts display:none)Consumer in theme.js: zero-results empty state

Also preserve: the emptiness test is {% if Spaces.size > 0 %}an emptylist is truthy in Fluid, so {% if Spaces %} makes the no-spaces stateunreachable. The Type/category select is server-driven (URL navigation);price/feature filters are client-side. The no-spaces empty statedifferentiates on Request.FilterLocationName (location-scoped message + a"View All" link back to /t/spaces).