Instructions

How to Customise Euphoria

Everything you need to make this template your own, from colours and fonts to CMS content and animations.

Getting Started

Euphoria is built entirely with native Webflow elements, variables and components, so most changes happen in a few panels instead of page by page. Work through these steps before editing individual sections.

  1. Open the Variables panel and set your brand colours, fonts and spacing first.
  2. Edit the global components (Navbar, Footer and CTA) so every page updates at once.
  3. Replace the CMS items with your own blog posts, case studies and integrations.
  4. Swap the images, update each page's SEO settings, then publish.

Tip: use the links in the sidebar to jump to any topic. Everything described here can be changed in the Designer without writing code.

Colours, Sizes and Typography

Open the Variables panel in the Designer. Every colour, spacing value and text style on the site reads from a variable, so changing a variable once updates every page.

  • Color: the palettes plus semantic groups for Text Color, Background Color, Border Color and Button Color.
  • Sizes: Spacers, Radius and Container Sizes control section padding, gaps, corner rounding and maximum widths.
  • Typography: H1 to H6, Paragraph 1 to 7, Line Height, Letter Spacing and Font Weight, with Tablet, Mobile Landscape and Mobile modes bound to each breakpoint.

For everyday changes, edit the semantic groups (such as Heading Text Color or Primary Button BG). Change the palette steps only when you want a completely new hue.

Fonts

Euphoria uses Switzer for headings and Satoshi for body text. Both are free from Fontshare under the ITF Free Font License.

  1. Open Site settings > Fonts and add the font you want to use.
  2. In the Variables panel, set Heading Font Family and Body Font Family to the new font.
  3. Check that the Font Weight variables match the weights your new font includes.
  4. Remove the old font from Site settings once nothing uses it.

Only the two Font Family variables need to change. Every heading, paragraph and button reads from them.

Global Components

Shared parts of the site live in the Components panel. Edit the main component once and every instance updates across all pages.

  • Navbar comes in two variants: Base for light pages and Overlay for pages with a dark hero.
  • Footer, CTA, CTA Simple and CTA Dark close every page.
  • FAQ, Marquee, Partners Logos, Video Testimonial and Pricing Plans are reusable sections.
  • Buttons are components too: Primary, Secondary, Link, Pill and Small, each with Text and Link props.

To change one button, select the instance and edit its Text and Link in the right panel. Opening the main component changes every button of that type.

CMS Collections

Blog Posts, Case Studies and Integrations power the listing pages, the home page cards and the sliders. Add or edit items in the CMS panel and every list updates automatically.

  • Blog Posts: each post opens its own template page. Switch on Featured to choose which stories lead the Blog page.
  • Case Studies: each study has a template page and also feeds the case study sliders across the site.
  • Integrations: each integration has a template page, and its Integrations Category option drives the filter tabs on the Integrations page.

Template pages pull their content from the CMS, so editing one item updates its page, its cards and its slider slides together.

Images and SVG Icons

All raster images are WebP files under 150KB and live in the Assets panel. Icons are inline SVG code, so they stay sharp at any size.

  • To replace an image, select it, open its settings and choose a new asset with a similar aspect ratio.
  • Export new photos as WebP and keep them under 150KB so pages load quickly.
  • Icons are SVG embeds that use currentColor, so they take the text colour of their class.
  • To recolour an icon, change the colour on its class or the variable that class is bound to.

Integration and partner logos are placeholders. Replace them with brand assets you are licensed to use.

Interactions and GSAP

Scroll reveals, hover states and the Video Banner parallax are Webflow IX3 interactions, edited in the Interactions panel. A small GSAP script adds the behaviours listed below. The site-wide script lives in Site settings > Custom code > Footer code, and the Home V3 video script lives in the Home V3 Page settings > Custom code. Each feature is switched on by a class or a custom attribute, so you control it from the element's Settings panel.

  • Number counters: any element with data-counter counts up from zero to its number when it scrolls into view.
  • Marquee loops: data-marquee="left" or "right" sets the direction, data-marquee-speed sets the speed in pixels per second (default 40). Loops pause on hover.
  • Filter pills: buttons with data-filter show only the items whose data-category matches, used on Integrations, the FAQ tabs and the Contact V2 office tabs. A pill with data-filter="all" shows everything.
  • Office slider: the wrapper with data-office-slider on Contact V1 fades between its slides using the two .slider-arrow buttons.
  • Blog featured accordion: cards inside .featured-grid open on hover on desktop and stack normally on tablet and mobile.
  • Scroll sliders: a .scroll-track scrolls one card at a time with the two .slider-arrow buttons in the same section.
  • Home V3 video: the element with data-video-expand grows to full width as you scroll.
  • Also included: FAQ lists with data-faq-open="first" open their first item, the Timeline highlights the year in view, and the mobile menu locks page scroll while open.

data-marquee="left" data-marquee-speed="40"

To change a speed, select the element, open Settings > Custom attributes and edit the value. To turn off one effect, delete its attribute. To remove GSAP entirely, delete the scripts from Site settings and the Home V3 page settings; the layout stays intact and marquees, sliders and filters simply become static.


<script>
  window.Webflow ||= [];
  window.Webflow.push(function () {
    if (!window.gsap) return;
    if (window.ScrollTrigger) gsap.registerPlugin(ScrollTrigger);
    var $ = gsap.utils.toArray;
    var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

    /* ---------- FAQ: lists with data-faq-open="first" start with the first item open ---------- */
    $('.faq-list[data-faq-open="first"]').forEach(function (list) {
      var first = list.querySelector(".faq-item");
      if (first) gsap.delayedCall(0.4, function () { first.click(); });
    });

    /* ---------- Timeline: highlight the year whose card is in view (ScrollTrigger) ---------- */
    if (window.ScrollTrigger) {
      var yearLinks = $('.timeline-year[href^="#"]');
      yearLinks.forEach(function (link) {
        var card = document.getElementById(link.getAttribute("href").slice(1));
        if (!card) return;
        ScrollTrigger.create({
          trigger: card, start: "top 50%", end: "bottom 40%",
          onToggle: function (self) {
            if (self.isActive) yearLinks.forEach(function (l) { l.classList.toggle("is-active", l === link); });
          }
        });
      });
    }

    /* ---------- Mobile menu: lock page scroll while .navbar-menu.is-open ---------- */
    $(".navbar-menu").forEach(function (menu) {
      function sync() { gsap.set(document.documentElement, { overflow: menu.classList.contains("is-open") ? "hidden" : "" }); }
      $(".menu-toggle").forEach(function (toggle) {
        toggle.addEventListener("click", function () { gsap.delayedCall(0.05, sync); });
      });
      $("a[href]", menu).forEach(function (a) {
        a.addEventListener("click", function () {
          if (a.getAttribute("href") === "#") return;
          menu.classList.remove("is-open");
          sync();
        });
      });
    });

    /* ---------- Scroll sliders: .scroll-track + two .slider-arrow inside the same .section-stack ---------- */
    $(".scroll-track").forEach(function (track) {
      var wrap = track.closest(".section-stack") || track.parentElement;
      var arrows = wrap ? $(".slider-arrow", wrap) : [];
      if (arrows.length < 2) return;
      function step() {
        var first = track.firstElementChild;
        var gap = parseFloat(getComputedStyle(track).columnGap) || 0;
        return first ? first.getBoundingClientRect().width + gap : track.clientWidth;
      }
      function go(dir) {
        var max = track.scrollWidth - track.clientWidth;
        var target = gsap.utils.clamp(0, max, track.scrollLeft + dir * step());
        gsap.set(track, { scrollSnapType: "none" });
        gsap.to(track, {
          scrollLeft: target, duration: 0.6, ease: "power2.out", overwrite: true,
          onComplete: function () { gsap.set(track, { clearProps: "scrollSnapType" }); }
        });
      }
      arrows[0].addEventListener("click", function (e) { e.preventDefault(); go(-1); });
      arrows[1].addEventListener("click", function (e) { e.preventDefault(); go(1); });
    });

    /* ---------- Office slider: [data-office-slider] > slides; .slider-arrow (aria-label Previous/Next) ---------- */
    $("[data-office-slider]").forEach(function (slider) {
      var slides = $(slider.children);
      if (slides.length < 2) return;
      var index = 0, busy = false;
      gsap.set(slides.slice(1), { display: "none" });
      function go(next) {
        if (busy) return;
        next = gsap.utils.wrap(0, slides.length, next);
        if (next === index) return;
        var current = slides[index], target = slides[next];
        index = next;
        busy = true;
        gsap.timeline({ onComplete: function () { busy = false; } })
          .to(current, { autoAlpha: 0, y: -12, duration: 0.3, ease: "power2.in" })
          .set(current, { display: "none", clearProps: "opacity,visibility,transform" })
          .set(target, { display: "" })
          .fromTo(target, { autoAlpha: 0, y: 12 }, { autoAlpha: 1, y: 0, duration: 0.45, ease: "power2.out" });
      }
      $(".slider-arrow", slider).forEach(function (arrow) {
        var prev = (arrow.getAttribute("aria-label") || "").toLowerCase().indexOf("prev") > -1;
        arrow.addEventListener("click", function (e) { e.preventDefault(); go(index + (prev ? -1 : 1)); });
      });
    });

    /* ---------- Featured accordion (Blog): .featured-grid > .featured-card, one open, hover opens another ---------- */
    $(".featured-grid").forEach(function (grid) {
      var cards = $(".featured-card", grid);
      if (cards.length < 2) return;
      var desktop = window.matchMedia("(min-width: 992px)");
      var OPEN = 707, CLOSED = 263;
      function setCard(card, open, instant) {
        var panel = card.querySelector(".featured-card-panel");
        var img = card.querySelector(".featured-card-image");
        card.classList.toggle("is-open", open);
        if (!desktop.matches) {
          gsap.set(card, { clearProps: "flexGrow" });
          if (panel) gsap.set(panel, { clearProps: "opacity,visibility,transform" });
          if (img) gsap.set(img, { clearProps: "transform" });
          return;
        }
        gsap.to(card, { flexGrow: open ? OPEN : CLOSED, duration: instant ? 0 : 0.8, ease: "power3.inOut", overwrite: "auto" });
        if (panel) gsap.to(panel, { autoAlpha: open ? 1 : 0, y: open ? 0 : 16, duration: instant ? 0 : (open ? 0.5 : 0.25), delay: open && !instant ? 0.35 : 0, ease: "power2.out", overwrite: "auto" });
        if (img) gsap.to(img, { scale: open ? 1 : 1.04, duration: instant ? 0 : 0.8, ease: "power3.inOut", overwrite: "auto" });
      }
      function openCard(target, instant) { cards.forEach(function (c) { setCard(c, c === target, instant); }); }
      cards.forEach(function (card) {
        function enter() { if (desktop.matches && !card.classList.contains("is-open")) openCard(card, false); }
        card.addEventListener("mouseenter", enter);
        card.addEventListener("focusin", enter);
      });
      openCard(cards[0], true);
      desktop.addEventListener("change", function () { openCard(grid.querySelector(".featured-card.is-open") || cards[0], true); });
    });

    /* ---------- Filter pills: [data-filter] pills show/hide [data-category] items in the same section ---------- */
    function applyFilter(pill, animate) {
      var scope = pill.closest("section") || document;
      var value = pill.getAttribute("data-filter");
      $("[data-filter]", scope).forEach(function (p) {
        p.classList.toggle("is-active", p === pill);
        p.setAttribute("aria-pressed", p === pill ? "true" : "false");
      });
      $("[data-category]", scope).forEach(function (card) {
        var item = card.closest(".w-dyn-item") || card;
        var show = value === "all" || card.getAttribute("data-category") === value;
        if (!show) { gsap.set(item, { display: "none" }); return; }
        gsap.set(item, { display: "" });
        if (!animate) return;
        gsap.fromTo(item, { autoAlpha: 0, y: 16 }, { autoAlpha: 1, y: 0, duration: 0.4, ease: "power2.out", overwrite: true });
        if (item.classList.contains("faq-list") && !item.hasAttribute("data-faq-opened")) {
          item.setAttribute("data-faq-opened", "");
          var first = item.querySelector(".faq-item");
          if (first) gsap.delayedCall(0.15, function () { first.click(); });
        }
      });
    }
    $("[data-filter]").forEach(function (pill) {
      pill.addEventListener("click", function (e) { e.preventDefault(); applyFilter(pill, true); });
    });
    $("[data-filter].is-active").forEach(function (pill) {
      var list = (pill.closest("section") || document).querySelector(".faq-list[data-faq-open]");
      if (list) list.setAttribute("data-faq-opened", "");
      applyFilter(pill, false);
    });

    /* ---------- Marquee loops: data-marquee="left|right" data-marquee-speed="px per second" ---------- */
    if (!reduceMotion) {
      $("[data-marquee]").forEach(function (wrap) {
        var dir = wrap.getAttribute("data-marquee") === "right" ? 1 : -1;
        var speed = parseFloat(wrap.getAttribute("data-marquee-speed")) || 40;
        var gap = parseFloat(getComputedStyle(wrap).columnGap) || 0;
        var track = document.createElement("div");
        track.className = "marquee-loop-track";
        while (wrap.firstChild) track.appendChild(wrap.firstChild);
        wrap.appendChild(track);
        gsap.set(wrap, { display: "flex", overflow: "hidden", columnGap: gap });
        gsap.set(track, { display: "flex", alignItems: "center", flexGrow: 0, flexShrink: 0, flexBasis: "auto", columnGap: gap });

        var loop = null;
        function build() {
          var w = track.offsetWidth + gap;
          if (w <= gap) return;
          var copies = Math.ceil((wrap.clientWidth * 2) / w);
          while (wrap.children.length < copies + 1) {
            var clone = track.cloneNode(true);
            clone.setAttribute("aria-hidden", "true");
            wrap.appendChild(clone);
          }
          var progress = loop ? loop.progress() : 0;
          var timeScale = loop ? loop.timeScale() : 1;
          if (loop) loop.kill();
          loop = gsap.fromTo(wrap.children,
            { x: dir < 0 ? 0 : -w },
            { x: dir < 0 ? -w : 0, duration: w / speed, ease: "none", repeat: -1 });
          loop.progress(progress).timeScale(timeScale);
        }
        build();
        if (window.ScrollTrigger) ScrollTrigger.addEventListener("refresh", build);

        wrap.addEventListener("mouseenter", function () { if (loop) gsap.to(loop, { timeScale: 0, duration: 0.4, overwrite: true }); });
        wrap.addEventListener("mouseleave", function () { if (loop) gsap.to(loop, { timeScale: 1, duration: 0.4, overwrite: true }); });
      });
    }
  });
</script>

<!-- Lenis Smooth Scroll -->
<link rel="stylesheet" href="https://unpkg.com/lenis@1.3.1/dist/lenis.css">
<script src="https://unpkg.com/lenis@1.3.1/dist/lenis.min.js"></script>

<script>
  const lenis = new Lenis({
    smoothWheel: true,
    lerp: 0.1,
    wheelMultiplier: 1,
    infinite: false
  });

  function raf(time) {
    lenis.raf(time);
    requestAnimationFrame(raf);
  }

  requestAnimationFrame(raf);
</script>


<!-- Counter Animation -->
<script>
  window.Webflow ||= [];

  window.Webflow.push(() => {
    document.querySelectorAll("[data-counter]").forEach((counter) => {
      const textEl = counter.firstElementChild || counter;
      const original = textEl.textContent.trim();

      const match = original.match(/^([^0-9.-]*)([0-9.,-]+)(.*)$/);
      if (!match) return;

      const prefix = match[1];
      const number = match[2].replace(/,/g, "");
      const suffix = match[3];

      const target = parseFloat(number);
      if (Number.isNaN(target)) return;

      const hasDecimal = number.includes(".");
      const hasComma = original.includes(",");

      const value = { current: 0 };

      gsap.fromTo(
        value,
        { current: 0 },
        {
          current: target,
          duration: 1.8,
          ease: "power2.out",
          scrollTrigger: {
            trigger: counter,
            start: "top 88%",
            once: true
          },
          onUpdate: () => {
            let formattedValue = hasDecimal
              ? value.current.toFixed(1)
              : Math.floor(value.current);

            if (hasComma && !hasDecimal) {
              formattedValue = Number(formattedValue).toLocaleString();
            }

            textEl.textContent =
              prefix + formattedValue + suffix;
          }
        }
      );
    });
  });
</script>

Forms

The Contact page forms and the CTA form are native Webflow forms, so they work as soon as the site is published.

  • Submissions appear under Site settings > Forms, where you can forward them to any email address.
  • Edit labels and placeholders on the canvas, and field names in each input's settings.
  • The success and error messages are styled blocks you can edit directly on the canvas.

Give every field a clear name before launch; it becomes the column heading in your form submissions.

SEO and Open Graph

Every static page has its own title and description under Page settings. CMS template pages pull the item name and excerpt automatically.

  • Keep meta titles under 60 characters and descriptions between 150 and 160 characters.
  • Add a 1200 by 630 pixel Open Graph image per page so shared links look great.
  • Keep one H1 per page and use the heading levels in order.

Utility Pages

Style Guide, Licenses, Changelog and Instructions are here to help you build. They are set to noindex, so search engines ignore them.

  • Style Guide shows every text style, button and form field in one place.
  • Licenses lists the source and licence of every font and image.
  • Changelog records each update to the template.
  • Delete these pages and their footer links before launch if you prefer.

Need Help?

Email design@inks.studio and we will get back to you within one business day.