• 18-19 College Green, Dublin 2
  • 01 685 9088
  • info@cunninghamwebsolutions.com
  • cunninghamwebsolutions
    Cunningham Web Solutions
    • Home
    • About Us
    • Our Services
      • Web Design
      • Digital Marketing
      • SEO Services
      • E-commerce Websites
      • Website Redevelopment
      • Social Media Services
    • Digital Marketing
      • Adwords
      • Social Media Services
      • Email Marketing
      • Display Advertising
      • Remarketing
    • Portfolio
    • FAQ’s
    • Blog
    • Contact Us
    MENU CLOSE back  

    Animating React Components With GreenSock

    You are here:
    1. Home
    2. Web Design
    3. Animating React Components With GreenSock
    Thumbnail for 25601

    During the early days of the World Wide Web, things were rather static and boring. Webpages were mostly based on graphic design and layouts from the print world until animations were introduced. Animation can engage and hold people’s attention longer than a static web page and communicates an idea or concept more clearly and effectively.

    However, when not done right, animations can hamper user interactions with your product and negatively impact traction. The GreenSock Animation Platform AKA (GSAP) is a powerful JavaScript library that enables front-end developers, animators and designers to create performant timeline based animations. It allows animation lovers take precise control of their animation sequences rather than the sometimes constraining keyframe and animation properties that CSS offers.

    In this article, I’ll introduce you to some features of GSAP such as scrollTriggers, Timelines, Easing etc, at the end we’ll build an intuitive user interface by animating a React app with this featuresπŸ‘Œ. Check out the finished project on codesandbox.

    This article will be useful to you if:

    • You have been building animations on web applications with HTML, CSS, and JavaScript.
    • You are already building animated webpages in a React apps with packages like animate.css, React-motion, Framer-motion, and React-Spring, plus you want to check out alternatives.
    • You are a React enthusiast, and you’d like to build complex animations on React-based web applications.

    We will look at how to build a variety of animations from an existing web project. Let’s get to it!

    Note: This article assumes you are comfortable with HTML, CSS, JavaScript, and React.js.

    What Is GSAP?

    GreenSock Animation Platform also known as GSAP is an Ultra high-performance, professional-grade animation for the modern web that allows developers to animate their apps in a modular, declarative, and re-usable fashion. It is framework-agnostic and can be used across any JavaScript based project, it has a very minimal bundle size and will not bloat your app.

    GSAP can perform canvas animations, used to create WebGL experiences, and create dynamic SVG animations and as great browser support.

    Why Use GSAP?

    Maybe you’re not quite ready to betray other frameworks yet, or you haven’t been convinced to embrace the goodies that come with GSAP. Allow me to give you a few reason why you may want to consider GSAP.

    You Can Build Complex Animations

    GSAP JavaScript library makes it possible for developers to build simple to very complex physics-based animations like in the case of these sites, it allows developers and designers sequence motion and controls the animation dynamically. It has lots of plugins such as DrawSVGPlugin, MorphSVGPlugin, and more, which makes creating SVG based animations and 2D/3D animations a reality. Asides integrating GSAP on DOM elements, you can use them within WebGL/Canvas/ Three.js context-based animations.

    Furthermore, the easing capacity of GSAP is quite sophisticated, hence making it possible to create advance effects with multiple beziers as compared to the regular CSS animation.

    Performance

    GSAP has an impressive high performance across different browsers.

    According to GSAP’s team, in their website, β€œGSAP is 20x faster than jQuery, plus GSAP is the fastest full-featured scripted animation tool on the planet. It’s even faster than CSS3 animations and transitions in many cases.” Confirm speed comparison for yourself.

    Furthermore, the GSAP animations perform effortlessly on both desktop computers, tablets, and smartphones. It is not needed to add a long list of prefixes, this is all being taken care of under the hood by GSAP.

    You can check out more benefits on GSAP or see what Sarah Drasner as to say about it here.

    Cons Of GSAP

    Are you saying I should always use GSAP for every project? Of course not! I feel like, there’s only one reason you might not want to use GSAP. Let’s find out!

    • GSAP is solely a JavaScript-based animation library, hence it requires some knowledge of JavaScript and DOM manipulation to effectively utilize its methods and APIs. This learning curve downside leaves even more room for complications for a beginner starting out with JavaScript.
    • GSAP doesn’t cater to CSS based animations, hence if you are looking for a library for such, you might as well use keyframes in CSS animation.

    If you’ve got any other reason, feel free to share it in the comment section.

    Alright, now that your doubts are cleared, let’s jump over to some nitty-gritty in GSAP.

    GSAP Basics

    Before we create our animation using React, let’s get familiar with some methods and building blocks of GSAP.

    If you already know the fundamentals of GSAP, you can skip this section and jump straight to the project section, where we’ll make a landing page skew while scrolling.

    Tween

    A tween is a single movement in an animation. In GSAP, a tween has the following syntax:

    TweenMax.method(element, duration, vars)

    Let’s take a look at what this syntax represents;

    1. method refers to the GSAP method you’ll like to tween with.
    2. element is the element you want to animate. If you want to create tweens for multiple elements at the same time, you can pass in an array of elements to element.
    3. duration is the duration of your tween. It is an integer in seconds (without the s suffix!).
    4. vars is an object of the properties you want to animate. More on this later.

    GSAP methods

    GSAP provides numerous methods to create animations. In this article, we’d mention only a few such as gsap.to, gsap.from, gsap.fromTo. You can check out other cool methods in their documentation. The methods discussed in this section will be used in building our project later in this tutorial.

    • gsap.to() the values to which an object should be animated i.e the end property values of an animated object β€” as shown below:
      gsap.to('.ball', {x:250, duration: 5})

    To demonstrate the to method the codepen demo below shows that an element with a class of ball 250px will move across the x-axis in five seconds when the components mounts. If a duration isn’t given, a default of 500 milliseconds would be used.

    See the Pen GSAP REACT DEMO1 by Blessing Krofegha.

    Note: x and y-axis represent the horizontal and vertical axis respectively, also in CSS transform properties such as translateX and translateY they are represented as x and y for pixel-measured transforms and xPercent and yPercent for percentage-based transforms.

    To view the complete snippet of the code check the codepen playground.

    • gsap.from() β€” Defines the values an object should be animated from β€” i.e., the start values of an animation:
      gsap.from('.square', {duration:3, scale: 4})

    The codepen demo show how an element with a class of square is resized from a scale of 4 in 3seconds when the components mounts. Check for the complete code snippet on this codepen.

    See the Pen GSAP REACT DEMO2 by Blessing Krofegha.

    • gsap.fromTo() β€” lets you define the starting and ending values for an animation. It is a combination of both the from() and to() method.

    Here’s how it looks;

    gsap.fromTo('.ball',{opacity:0 }, {opacity: 1 , x: 200 , duration: 3 });
    gsap.fromTo('.square', {opacity:0, x:200}, { opacity:1, x: 1 , duration: 3 });

    This code would animates the element with a class of ball from an opacity of 0 to an opacity of 1 across the x-axis in 3 seconds and the square class is animated the from an opacity of 0 to 1 in 3 seconds across the x-axis only when the component mounts. To see how the fromTo method works and the complete code snippet, check the demo on CodePen below.

    See the Pen React GSAP FromTo demo by Blessing Krofegha.

    Note: Whenever we’re animating positional properties, such as left and top, we must ensure that the elements concerned must have a CSS position property of either relative, absolute, or fixed.

    Easing

    GSAP official documentation defined easing as the primary way to change the timing of your Tweens. It determines how an object changes position at different points. Ease controls the rate of change of animation in GSAP and is used to set the style of an object’s animation.

    GSAP provides different types of eases and options to give you more control over how your animation should behave. It also provides an Ease Visualizer to help you choose your preferred ease settings.

    There are three types of eases, and they vary in their operations.

    1. in() β€” Motion starts slowly, then picks up the pace toward the end of the animation.
    2. out() β€” The animation starts fast then slows down at the end of the animation.
    3. inOut() β€” The animation begins slow, picks up the pace halfway through, and ends slowly.

    See the Pen React GSAP Easing demo by Blessing Krofegha.

    In these easing example, we chained the tweens that displayed the three types of eases bounce.in, bounce.out and bounce.inOut, and set a delay of the number of seconds it takes the animation to complete before starting the next one only when the component is mounts. This pattern is repetitive, in the next next section we would see how we could use a timeline to do this better.

    Timelines

    A Timeline acts as a container for multiple tweens. It animates tweens in sequential order, and it is not dependent on the duration of the previous tween. Timeline makes it simple to control tweens as a whole and precisely manage their timing.

    Timelines can be written by creating an instance of a timeline like so:

    gsap.timeline();

    You can also chain multiple tweens to a timeline in two different ways, in the code below:

    ##Method 1
    const tl = gsap.timeline(); // create an instance and assign it a variable
    tl.add(); // add tween to timeline 
    tl.to('element', {});
    tl.from('element', {});
    
    ##Method 2
    gsap.timeline()
        .add() // add tween to timeline 
        .to('element', {})
        .from('element', {})

    Let’s recreate the previous example with a timeline:

    const { useRef, useEffect } = React;
    
    const Balls = () => {
        useEffect(() => {
    const tl = gsap.timeline(); tl.to('#ball1', {x:1000, ease:"bounce.in", duration: 3}) tl.to('#ball2', {x:1000, ease:"bounce.out", duration: 3, delay:3 }) tl.to('#ball3', {x:1000, ease:"bounce.inOut", duration: 3, delay:6 }) }, []); } ReactDOM.render(, document.getElementById('app'));

    Inside a useEffect hook, we created a variable(tl) that holds an instance of a timeline, next we used the tl variable to animate our tween in sequential without depending on the previous tween to animate, passing the same properties as it were in the previous example. For the complete code snippet of this demo check the codepen playground below.

    See the Pen React GSAP (Easing with Timeline) demo by Blessing Krofegha.

    Now that we have gotten a feel of some the basic building blocks of GSAP, let’s see how we could build a complete animation in a typical React app in the next section.
    Let’s begin the flight! πŸš€

    Building an Animated Landing Page with React and GSAP

    Let’s get to animate a React App. Ensure you clone the repo before you begin and run npm install, to install the dependencies.

    What are we building?

    Currently, our landing page contains a few texts a white background, a menu that doesn’t drop down, with really no animation. The following are what we’ll be adding to the landing page;

    • Animate the text and the logo on the homepage, so it eases out when the component is mounted.
    • Animate the menu, so it drops down when the menu is clicked.
    • Make the images in the gallery page skew 20deg when the page scrolls.

    Animated page.

    Check out the demo on codesandbox.

    We’ll break the process of our landing page into components, so it will be easy to grasp. Here’s the process;

    • Define the animation methods,
    • Animate text and logo,
    • Toggle menu,
    • Make images skew 20deg on page scroll.

    components

    • Animate.js β€” Defined all animation methods,
    • Image.js β€” import galley images,
    • Menu.js β€” Contains the menu toggle functionality,
    • Header.js β€” Contains navigation links.

    Define animation methods

    Create a component folder inside the src directory, and create an animate.js file. Copy and paste the following code into it.

    import gsap from "gsap"
    import { ScrollTrigger } from "gsap/ScrollTrigger";
    //Animate text 
    export const textIntro = elem => {
      gsap.from(elem, {
        xPercent: -20,
        opacity: 0,
        stagger: 0.2,
        duration: 2,
        scale: -1,
        ease: "back",
      });
    };
    

    Here, we imported gsap . We wrote an exported arrow function that animates the text on the landing page. Remember that gsap.from() method defines the values an object should be animated from. The function has an elem parameter that represents the class which needs to be animated. It takes a few properties and assigns values such as xPercent: -20 (transforms the object by -20%), gives the object no opacity, makes the object scale by -1, makes the object ease back in 2sec.

    To see if this works, head over to App.js and include the following code.

    ...
    //import textIntro
    import {textIntro} from "./components/Animate"
    
    ...
    //using useRef hook to access the textIntro DOM
     let intro = useRef(null)
      useEffect(() => {
        textIntro(intro)
      }, [])
    
    function Home() {
      return (
        <div className='container'>
          <div className='wrapper'>
            <h5 className="intro" ref={(el) => (intro = el)}></h5>
              The <b>SHOPPER</b>, is a worldclass, innovative, global online ecommerce platform,
              that meets your everyday daily needs.
            </h5>
          </div>
        </div>
      );
    }

    Here, we import the textIntro method from the Aminate component. To access the DOM we used to useRef Hook. We created a variable intro whose value is set to null. Next, inside the useEffect hook, we called the textIntro method and the intro variable. Inside our home component, in the h5 tag, we defined the ref prop and passed in the intro variable.

    Animated text.

    Next, we have got a menu, but it isn’t dropping down when it’s clicked. Let’s make it work! Inside the Header.js Component, add the code below.

    import React, { useState, useEffect, useRef } from "react";
    import { withRouter, Link, useHistory } from "react-router-dom";
    import Menu from "./Menu";
    const Header = () => {
      const history = useHistory()
      let logo = useRef(null);
      //State of our Menu
      const [state, setState] = useState({
        initial: false,
        clicked: null,
        menuName: "Menu",
      });
      // State of our button
      const [disabled, setDisabled] = useState(false);
      //When the component mounts
      useEffect(() => {
        textIntro(logo);
        //Listening for page changes.
        history.listen(() => {
          setState({ clicked: false, menuName: "Menu" });
        });
      }, [history]);
      //toggle menu
      const toggleMenu = () => {
        disableMenu();
        if (state.initial === false) {
          setState({
            initial: null,
            clicked: true,
            menuName: "Close",
          });
        } else if (state.clicked === true) {
          setState({
            clicked: !state.clicked,
            menuName: "Menu",
          });
        } else if (state.clicked === false) {
          setState({
            clicked: !state.clicked,
            menuName: "Close",
          });
        }
      };
      // check if out button is disabled
      const disableMenu = () => {
        setDisabled(!disabled);
        setTimeout(() => {
          setDisabled(false);
        }, 1200);
      };
      return (
        <header>
          <div className="container">
            <div className="wrapper">
              <div className="inner-header">
                <div className="logo" ref={(el) => (logo = el)}>
                  <Link to="/">SHOPPER.</Link>
                </div>
                <div className="menu">
                  <button disabled={disabled} onClick={toggleMenu}>
                    {state.menuName}
                  </button>
                </div>
              </div>
            </div>
          </div>
          <Menu state={state} />
        </header>
      );
    };
    export default withRouter(Header);

    In this component, we defined our menu and button state, inside the useEffect hook, we listened for page changes using useHistory hook, if the page changes we set the clicked and menuName state values to false and Menu respectively.

    To handle our menu, we checked if the value of our initial state is false, if true, we change the value of initial , clicked and menuName to null, true and Close. Else we check if the button is clicked, if true we’d change the menuName to Menu. Next, we have a disabledMenu function that disables our button for 1sec when it’s clicked.

    Lastly, in our button, we assigned disabled to disabled which is a boolean value that will disable the button when its value is true. And the onClick handler of the button is tied to the toggleMenu function. All we did here was toggle our menu text and passed the state to a Menu component, which we would create soonest. Let’s write the methods that will make our menu dropdown before creating the actual Menu component. Head over to Animate.js and paste this code into it.

    ....
    //Open menu
    export const menuShow = (elem1, elem2) => {
      gsap.from([elem1, elem2], {
        duration: 0.7,
        height: 0,
        transformOrigin: "right top",
        skewY: 2,
        ease: "power4.inOut",
        stagger: {
          amount: 0.2,
        },
      });
    };
    //Close menu
    export const menuHide = (elem1, elem2) => {
      gsap.to([elem1, elem2], {
        duration: 0.8,
        height: 0,
        ease: "power4.inOut",
        stagger: {
          amount: 0.07,
        },
      });
    };
    

    Here, we have a function called menuShow, which skews the menu horizontally by 2degrees, eases the menu, offset’s the animation using the stagger property, and transforms the menu from right to top in 0.7sec, the same properties go for the menuHide function. To use these functions, create Menu.js file inside the components and paste this code into it.

    import React, {useEffect, useRef} from 'react'
    import { gsap } from "gsap"
    import { Link } from "react-router-dom"
    import {
      menuShow,
      menuHide,
      textIntro,
    } from './Animate'
    const Menu = ({ state }) => {
       //create refs for our DOM elements
    
      let menuWrapper = useRef(null)
      let show1 = useRef(null)
      let show2 = useRef(null)
      let info = useRef(null)
      useEffect(() => {
        // If the menu is open and we click the menu button to close it.
        if (state.clicked === false) {
          // If menu is closed and we want to open it.
          menuHide(show2, show1);
          // Set menu to display none
          gsap.to(menuWrapper, { duration: 1, css: { display: "none" } });
        } else if (
          state.clicked === true ||
          (state.clicked === true && state.initial === null)
        ) {
          // Set menu to display block
          gsap.to(menuWrapper, { duration: 0, css: { display: "block" } });
          //Allow menu to have height of 100%
          gsap.to([show1, show2], {
            duration: 0,
            opacity: 1,
            height: "100%"
          });
          menuShow(show1, show2);
          textIntro(info);
    
        }
      }, [state])
    
      return (
        <div ref={(el) => (menuWrapper = el)} className="hamburger-menu">
          <div
            ref={(el) => (show1 = el)}
            className="menu-secondary-background-color"
          ></div>
          <div ref={(el) => (show2 = el)} className="menu-layer">
            <div className="container">
              <div className="wrapper">
                <div className="menu-links">
                  <nav>
                    <ul>
                      <li>
                        <Link
                          ref={(el) => (line1 = el)}
                          to="/about-us"
                        >
                          About
                        </Link>
                      </li>
                      <li>
                        <Link
                          ref={(el) => (line2 = el)}
                          to="/gallery"
                        >
                          Gallery
                        </Link>
                      </li>
                      <li>
                        <Link
                          ref={(el) => (line3 = el)}
                          to="/contact-us"
                        >
                          Contact us
                        </Link>
                      </li>
    
                    </ul>
                  </nav>
                  <div ref={(el) => (info = el)} className="info">
                    <h3>Our Vision</h3>
                    <p>
                      Lorem ipsum dolor sit amet consectetur adipisicing elit....
                    </p>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      );
    }
    export default Menu

    What we did in the Menu component was to import the animated functions, which are menuShow, menuHide, and textIntro. Next, we assigned variables for each created refs for our DOM elements using the useRef hook and passed null as their values. Inside the useEffect hook, we check for the state of the menu, if clicked is false, we call the menuHide function, otherwise, if the clicked state is true we call the menuShow function. Lastly, we ensured that the DOM elements concerned are passed their specific refs which are menuWrapper, show1, show2. With that, we’ve got our menu animated.

    Let’s see how it looks.

    Animated Menu.

    The last animation we would implement is make our images in our gallery skew when it scrolls. Let’s see the state of our gallery now.

    Gallery without animation.

    To implement the skew animation on our gallery, let’s head over to Animate.js and add a few codes to it.

    ....
    //Skew gallery Images
    export const skewGallery = elem1 => {
      //register ScrollTrigger
      gsap.registerPlugin(ScrollTrigger);
      // make the right edge "stick" to the scroll bar. force3D: true improves performance
        gsap.set(elem1, { transformOrigin: "right center", force3D: true });
        let clamp = gsap.utils.clamp(-20, 20) // don't let the skew go beyond 20 degrees. 
        ScrollTrigger.create({
          trigger: elem1,
          onUpdate: (self) => {
            const velocity = clamp(Math.round(self.getVelocity() / 300));
            gsap.to(elem1, {
              skew: 0,
              skewY: velocity,
              ease: "power3",
              duration: 0.8,
            });
          },
        });
    }

    We created a function called skewGallery, passed elem1 as a param, and registered ScrollTrigger.

    ScrollTrigger is a plugin in GSAP that enables us to trigger scroll-based animations, like in this case of skewing the images while the page scrolls.

    To make the right edge stick to the scroll bar we passed right center value to the transformOrigin property, we also set the force3D property to true in other to improve the performance.

    We declared a clamp variable that calculates our skew and ensures it doesn’t exceed 20degs. Inside the ScrollTrigger object, we assigned the trigger property to the elem1 param, which would be the element that needs to be triggered when we call this function. We have an onUpdate callback function, inside it is a velocity variable that calculates the current velocity and divides it by 300.

    Lastly, we animate the element from their current values by setting other values. We set skew to initially be at 0 and skewY to be the velocity variable at 0.8.

    Next, we’ve got to call this function in our App.js file.

    ....
    import { skewGallery } from "./components/Animate"
    function Gallery() {
      let skewImage = useRef(null);
      useEffect(() => {
        skewGallery(skewImage)
      }, []);
      return (
        <div ref={(el) => (skewImage = el)}>
          <Image/>
        </div>
      )
    }
    
    ....

    Here, we imported skewGalley from ./components/Animate, created a skewImage ref that targets the image element. Inside the useEffect hook, we called the skewGallery function and passed the skewImage ref as a param. Lastly, we passed the skewImage to the ref to attribute.

    You’d agree with me it was such a pretty cool journey thus far. Here’s the preview on CodeSanbox πŸ‘‡

    The supporting repo for this article is available on Github.

    Conclusion

    We’ve explored the potency of GSAP in a React project, we only scratched the surface in this article, there’s no limit to what you can do with GSAP as it concerns animation.
    GSAP’s official website offers additional tips to help you gain a thorough understanding of methods and plugins. There’s a lot of demos that would blow your mind away with what people have done with GSAP. I’d love to hear your experience with GSAP in the comment section.

    Resources

    1. GSAP Documentation β€” GSAP
    2. The Beginner’s Guide to the GreenSock Animation Platform β€” Freecodecamp
    3. An introduction to animations with Greensock Animation API (GSAP) β€” Zellwk

    From our sponsors: Animating React Components With GreenSock

    Posted on 14th September 2020Web Design
    FacebookshareTwittertweetGoogle+share

    Related posts

    Archived
    22nd March 2023
    Archived
    18th March 2023
    Archived
    20th January 2023
    Thumbnail for 25788
    Handling Continuous Integration And Delivery With GitHub Actions
    19th October 2020
    Thumbnail for 25778
    A Monthly Update With New Guides And Community Resources
    19th October 2020
    Thumbnail for 25781
    Supercharge Testing React Applications With Wallaby.js
    19th October 2020
    Latest News
    • Archived
      22nd March 2023
    • Archived
      18th March 2023
    • Archived
      20th January 2023
    • 20201019 ML Brief
      19th October 2020
    • Thumbnail for 25788
      Handling Continuous Integration And Delivery With GitHub Actions
      19th October 2020
    • Thumbnail for 25786
      The Future of CX with Larry Ellison
      19th October 2020
    News Categories
    • Digital Marketing
    • Web Design

    Our services

    Website Design
    Website Design

    A website is an important part of any business. Professional website development is an essential element of a successful online business.

    We provide website design services for every type of website imaginable. We supply brochure websites, E-commerce websites, bespoke website design, custom website development and a range of website applications. We love developing websites, come and talk to us about your project and we will tailor make a solution to match your requirements.

    You can contact us by phone, email or send us a request through our online form and we can give you a call back.

    More Information

    Digital Marketing
    Digital Marketing

    Our digital marketeers have years of experience in developing and excuting digital marketing strategies. We can help you promote your business online with the most effective methods to achieve the greatest return for your marketing budget. We offer a full service with includes the following:

    1. Social Media Marketing

    2. Email & Newsletter Advertising

    3. PPC - Pay Per Click

    4. A range of other methods are available

    More Information

    SEO
    SEO Services

    SEO is an essential part of owning an online property. The higher up the search engines that your website appears, the more visitors you will have and therefore the greater the potential for more business and increased profits.

    We offer a range of SEO services and packages. Our packages are very popular due to the expanse of on-page and off-page SEO services that they cover. Contact us to discuss your website and the SEO services that would best suit to increase your websites ranking.

    More Information

    E-commerce
    E-commerce Websites

    E-commerce is a rapidly growing area with sales online increasing year on year. A professional E-commerce store online is essential to increase sales and is a reflection of your business to potential customers. We provide professional E-commerce websites custom built to meet our clients requirements.

    Starting to sell online can be a daunting task and we are here to make that journey as smooth as possible. When you work with Cunningham Web Solutions on your E-commerce website, you will benefit from the experience of our team and every detail from the website design to stock management is carefully planned and designed with you in mind.

    More Information

    Social Media Services
    Social Media Services

    Social Media is becoming an increasingly effective method of marketing online. The opportunities that social media marketing can offer are endless and when managed correctly can bring great benefits to every business.

    Social Media Marketing is a low cost form of advertising that continues to bring a very good ROI for our clients. In conjuction with excellent website development and SEO, social media marketing should be an essential part of every digital marketing strategy.

    We offer Social Media Management packages and we also offer Social Media Training to individuals and to companies. Contact us to find out more.

    More Information

    Cunningham Web Solutions
    © Copyright 2025 | Cunningham Web Solutions
    • Home
    • Our Services
    • FAQ's
    • Account Services
    • Privacy Policy
    • Contact Us