Changelog

Getting Started

Horizon is a no-dependency Webflow template that works 100% with native features. Any custom code listed below is optional and safe to remove.

Pages: Home, Apartments (Listing), Property Details (CMS), About, Contact, 404, Password, Licenses, Changelog, Instructions, Cookie, Privacy Policy, Terms, Style Guide.
CMS Collections: Apartments, Agents, Bedrooms, Bathrooms, Garages, Views, 3D Tours (demo items included).

Quick setup (5 steps)

  1. Open Style Guide → update global colors & typography (affects the whole site).
  2. Replace the logo in Nav & Footer Components.
  3. Fill the CMS: add 6–10 Apartments and 2–4 Agents (realistic demo text & images).
  4. Configure forms (Project Settings → Forms) and edit success/error messages.
  5. Set SEO for key pages (Page Settings): Title, Meta Description, Open Graph image.

Accessibility checklist (required)

  • Provide alt text for all meaningful images (properties, plans, logos).
  • Icon-only links/buttons must have visible text or an aria-label (e.g., “Open gallery”).
  • Use a single H1 per page and follow H2 → H3 order.
  • Keep mobile body text ≥ 15–16 px for legibility.

Performance best practices (required)

  • Hero/LCP image ≤ 300 KB (WebP/AVIF), responsive images enabled.
  • No preloader on Home.
  • Avoid third-party scripts on Home.
  • Enable Minify CSS/JS (Project Settings) and compress all images.

SEO basics (required)

  • Add a canonical URL on main pages (Page Settings → Advanced).
  • If you publish one language only, use root paths (no /en).
  • Provide OG title/description and an OG image 1200×630.

Global Styles (optional, recommended)

Horizon ships with an optional Global Styles Component that defines a fluid type scale using pure CSS (no JS, no <html> font-size hacks).

How to use

  1. Place the Global Styles Component at the top of the <body> on each page.
  2. Edit sizes via CSS tokens in the snippet below.
  3. To revert to Webflow defaults, simply remove the Component.

CSS (paste in an Embed inside the “Global Styles” Component):

<style id="global-styles">
/* ===== Fluid type — compact defaults (no JS, no html hacks) ===== */
:root{
  /* steps: tighten maxima ~20–25% vs previous scale */
  --step--2: clamp(0.8125rem, 0.79rem + 0.20vw, 0.875rem);   /* 13–14px */
  --step--1: clamp(0.9rem,    0.88rem + 0.20vw, 0.95rem);    /* 14.4–15.2px */
  --step-0:  clamp(1rem,      0.98rem + 0.20vw, 1.0625rem);  /* 16–17px (body) */
  --step-1:  clamp(1.125rem,  1.05rem + 0.35vw, 1.25rem);    /* 18–20px */
  --step-2:  clamp(1.25rem,   1.10rem + 0.60vw, 1.5rem);     /* 20–24px */
  --step-3:  clamp(1.5rem,    1.20rem + 1.00vw, 1.875rem);   /* 24–30px */
  --step-4:  clamp(1.875rem,  1.40rem + 1.40vw, 2.5rem);     /* 30–40px */
  --step-5:  clamp(2.25rem,   1.60rem + 2.00vw, 3rem);       /* 36–48px (display) */
}

html{ font-size:16px; -webkit-text-size-adjust:100%; }
body{ font-size:var(--step-0); line-height:1.6; }

/* Headings — smaller by default; use .display-* for hero titles */
h1,.h1{ font-size:var(--step-4); line-height:1.12; }
h2,.h2{ font-size:var(--step-3); line-height:1.18; }
h3,.h3{ font-size:var(--step-2); line-height:1.24; }
h4,.h4{ font-size:var(--step-1); line-height:1.3; }
h5,.h5{ font-size:var(--step-0); line-height:1.4; }
small,.text-small{ font-size:var(--step--1); }

/* Hero/marketing displays (opt-in only) */
.display-lg{ font-size:var(--step-5); line-height:1.06; }
.display-md{ font-size:var(--step-4); line-height:1.10; }

/* ===== Utilities ===== */
*,*::before,*::after{ box-sizing:border-box; }
:focus-visible{ outline:2px solid currentColor; outline-offset:2px; }
.sr-only{ position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); border:0; }

/* Text sizing helpers */
.text-xs{ font-size:var(--step--2); }
.text-sm{ font-size:var(--step--1); }
.text-md{ font-size:var(--step-0); }
.text-lg{ font-size:var(--step-1); }

/* Table/compact contexts — shrink one step without changing markup */
.is-compact{
  --step-5: var(--step-4);
  --step-4: var(--step-3);
  --step-3: var(--step-2);
  --step-2: var(--step-1);
  --step-1: var(--step-0);
  --step-0: var(--step--1);
}
.no-wrap{ white-space:nowrap; }

/* Optional: tighten headings inside tables only */
.table th, .table .th, .table .heading { font-size:var(--step-1); line-height:1.2; }
</style>


Optional enhancements (opt-in)

These features are not required for the template to work. Remove if not needed.

A) Tabs auto-rotation (optional, no dependencies)

No-code option: swap the Tabs section for a Webflow Slider with Auto-rotate.

Light JS option (page-scoped): add this snippet only on pages that use Tabs. It auto-detects .w-tabs, respects prefers-reduced-motion, pauses on user interaction, and pauses if the mobile menu is open.

Where to place: Page Settings → Before </body> (this page only), or in an Embed right after the Tabs component.

Script:

<!-- Horizon – Tabs Auto-Rotate (Optional, page-scoped, no dependencies)
     Attribute-free variant: auto-detects any .w-tabs on this page.
     - No data attributes required (defaults below)
     - Respects prefers-reduced-motion
     - Pauses on user interaction and when the mobile menu is open
     - You can still override per-instance via data-rotate-ms / data-initial-delay if desired
-->
<script>
window.Webflow = window.Webflow || [];
window.Webflow.push(function () {
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  const DEFAULT_ROTATE_MS = 3000;
  const DEFAULT_INITIAL_DELAY = 1000;
  const wrappers = Array.from(document.querySelectorAll('.w-tabs'));
  if (!wrappers.length) return;
  wrappers.forEach((root) => {
    const menu  = root.querySelector('.tabs-menu, .w-tab-menu'); if (!menu) return;
    const links = Array.from(menu.querySelectorAll('.tab-link, .w-tab-link')); if (links.length < 2) return;
    const ROTATE_MS     = Number(root.getAttribute('data-rotate-ms')     || DEFAULT_ROTATE_MS);
    const INITIAL_DELAY = Number(root.getAttribute('data-initial-delay') || DEFAULT_INITIAL_DELAY);
    let i = links.findIndex(l => l.classList.contains('w--current')); if (i < 0) i = 0;
    let timer = null, paused = false;
    const isMenuOpen = () => !!document.querySelector('.menu-button.w--open');
    function step(){ if (paused || isMenuOpen()) return; i = (i + 1) % links.length;
      links[i].dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); }
    function start(){ stop(); timer = setInterval(step, ROTATE_MS); }
    function stop(){ if (timer) clearInterval(timer); }
    ['mouseenter','focusin','pointerdown','touchstart'].forEach(evt =>
      root.addEventListener(evt, () => { paused = true; stop(); }, { passive: true }));
    ['mouseleave','focusout'].forEach(evt =>
      root.addEventListener(evt, () => { paused = false; start(); }, { passive: true }));
    links.forEach(l => l.addEventListener('click', () => { paused = false; start(); }, { passive: true }));
    setTimeout(step, INITIAL_DELAY); start();
  });
});
</script>

Notes: On pages with multiple Tab groups, all will auto-rotate. To target a single Tabs block, you can add data-rotate-ms/data-initial-delay on that wrapper (optional).

B) 3D Virtual Tour (optional, lazy-loaded)

Load the 3D embed only on click to keep performance and accessibility high.

Markup + script (place in the CMS Property Template or a dedicated 3D Tours page):

<div class="vr-placeholder" data-embed="https://theviewer.co/embed?galleryId=YOUR_GALLERY_ID&fullScreen=true&thumbnail=false">
  <button class="btn vr-load" aria-label="Launch 3D property tour">Launch 3D Tour</button>
</div>
<script>
document.querySelectorAll('.vr-placeholder .vr-load').forEach(b=>b.addEventListener('click',()=>{
  const wrap=b.closest('.vr-placeholder'); const src=wrap.dataset.embed;
  const ifr=document.createElement('iframe'); ifr.src=src; ifr.loading='lazy';
  ifr.setAttribute('title','3D property tour');
  ifr.setAttribute('allow','xr-spatial-tracking; gyroscope; accelerometer; fullscreen');
  ifr.style.width='100%'; ifr.style.height='60vh'; wrap.innerHTML=''; wrap.appendChild(ifr);
},{passive:true}));
</script>

Production note: your 3D provider may require an account. If you don’t need 3D, remove the placeholder block entirely.

Page-by-page notes

  • Home: hero image is the LCP — keep it lightweight; update CTA targets.
  • Apartments (Listing): bound to Apartments collection; filters by city/type/status.
  • Property Details (CMS): gallery (multi-image), specs, map, agent reference, “Schedule a tour” form.
  • About / Contact: edit copy and contact details; confirm form recipients.
  • Licenses: list sources and licenses for all images/icons.
  • Changelog: document updates (keep noindex).
  • Style Guide: manage tokens (type scale, buttons, links, spacing).

Removing optional code

  • The template is fully functional without any custom scripts.
  • To disable Tabs auto-rotation, remove its script block from the page.
  • To remove the 3D tour, delete the placeholder block or the script above.
  • To revert typography, remove the Global Styles Component.

Support

For questions or help customizing Horizon, contact: your-email@domain.com.