Flip a text box

What’s the best way to “flip” a text box when hovered to reveal the text box behind it?

Thanks, :slight_smile:

I know two solid ways to do this. One is simpler, one is fancier. Which level do you want?

I’m curious about the fancy one :thinking:

Will this be in a repeating group or just a single card?

Anyway, if it’s a single card, it would look like this. I put together a demo:

Screen recording 2026-01-07 5.22.43 PM

Added: Not the best gif, but you get the idea. Is this what you want?

Yes, Seneca, that’s it. Thank you. Simple is always fine for me. :slight_smile:

Sorry for the delay in getting back with you. I got busy.

I took a few minutes this morning to put together a better demo. I’m not the best at gifs.

chrome-capture-2026-01-09 (2)

It’s made for single cards or repeating groups. You can change the content in the code to be dynamic if you want. I.E. change the images, text, etc. for a single card or repeating group.

The card has:
3D Parallax movement
Physics-based tilt
Adaptive glare
Glassmorphism button
Pulse action
Smart loading…it has a skeleton loader if the image takes too long to load
Legibility engine
Dark mode fallback
Toolbox bridge
Zero glow cleanup
2 tap fallback. You’ll need the toolbox plugin to make it work with links. I added the 2-tap fallback so it wouldn’t confuse clicking to change sides to linking, since on mobile, there is no hover effect.

That’s pretty much all you need for a flip card.

You can use Bubble’s vanilla thing and try and replicate it with 2 groups and a focus group, but you don’t get the full effect.

If you want to change any of the code, you can use AI. I recommend Gemini.

I did use AI for some of the coding, but I added a few things to make it fit Bubble better.

If you have any other questions, I’ll be happy to help you.

<style>
  * {
    outline: none !important;
    -webkit-tap-highlight-color: transparent !important;
    box-sizing: border-box;
  }

  :root { 
    --r: 28px; 
    --card-width: 600px;
    --card-height: 300px;
  }

  .card-viewport {
    width: var(--card-width);
    height: var(--card-height);
    margin: 40px auto;
    perspective: 2000px;
    background: transparent !important;
  }

  .card-engine {
    position: relative;
    width: 100%;
    height: 100%;
    transition: transform 0.8s cubic-bezier(0.17, 0.67, 0.1, 1);
    transform-style: preserve-3d;
  }

  .card-engine.is-flipped { transform: rotateY(180deg) translateY(-10px); }

  .card-face {
    position: absolute;
    inset: 0;
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
    border-radius: var(--r);
    overflow: hidden;
    background: #111;
    box-shadow: 0 4px 15px rgba(0,0,0,0.15);
  }

  .face-back { transform: rotateY(180deg); }

  /* THE SMART LOADING SYSTEM */
  .bg-image {
    position: absolute;
    inset: -25%;
    width: 150%;
    height: 150%;
    object-fit: cover;
    filter: brightness(0.85) blur(20px); /* Starts blurred */
    opacity: 0; /* Starts hidden */
    transition: transform 0.1s ease-out, opacity 0.8s ease, filter 0.8s ease;
  }

  /* CLASS ADDED BY JAVASCRIPT WHEN READY */
  .bg-image.is-loaded {
    opacity: 1;
    filter: brightness(0.85) blur(0px);
  }

  .glare-layer {
    position: absolute;
    inset: 0;
    z-index: 10;
    opacity: 0;
    background: radial-gradient(circle at var(--x, 50%) var(--y, 50%), rgba(255, 255, 255, 0.35) 0%, rgba(255, 255, 255, 0) 75%);
    transition: opacity 0.3s ease;
    pointer-events: none;
  }
  .card-viewport:hover .glare-layer { opacity: 1; }

  .content-overlay {
    position: absolute;
    inset: 0;
    padding: 40px;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    color: #fff;
    font-family: 'Inter', system-ui, sans-serif;
    z-index: 5;
  }

  .tint-front { background: linear-gradient(to top, rgba(0,0,0,0.7) 0%, transparent 60%); }
  .tint-back { background: linear-gradient(135deg, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.3) 100%); }

  .title { 
    font-size: 42px; font-weight: 900; margin: 0; letter-spacing: -2px; line-height: 1; 
    text-transform: uppercase; cursor: pointer; text-shadow: 0 4px 20px rgba(0,0,0,0.9);
  }

  .btn-trigger {
    margin-top: 20px; padding: 14px 32px; background: rgba(255, 255, 255, 0.15);
    backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); color: #fff;
    border: 1px solid rgba(255, 255, 255, 0.25); border-radius: 100px;
    font-weight: 800; font-size: 14px; width: fit-content; text-transform: uppercase;
    cursor: pointer; box-shadow: 0 8px 32px rgba(0,0,0,0.3); transition: all 0.3s ease;
    animation: pulseGlow 3s infinite;
  }

  @keyframes pulseGlow {
    0% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.15); }
    70% { box-shadow: 0 0 0 15px rgba(255, 255, 255, 0); }
    100% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); }
  }

  .btn-trigger:hover {
    background: rgba(255, 255, 255, 0.25); transform: translateY(-2px);
    animation: none; box-shadow: 0 0 25px rgba(255, 255, 255, 0.4);
  }

  @media (max-width: 650px) {
    .card-viewport { width: 92vw; height: 52vw; }
    .title { font-size: 26px; }
  }
</style>

<div class="card-viewport" id="cardViewport">
  <div class="card-engine" id="cardEngine">
    <div class="card-face face-front">
      <div class="glare-layer"></div>
      <img class="bg-image" id="imgFront" src="https://images.pexels.com/photos/110854/pexels-photo-110854.jpeg?auto=compress&cs=tinysrgb&w=1260">
      <div class="content-overlay tint-front">
        <h2 class="title">Dream it<br>First.</h2>
      </div>
    </div>
    <div class="card-face face-back">
      <div class="glare-layer"></div>
      <img class="bg-image" id="imgBack" src="https://images.pexels.com/photos/1169754/pexels-photo-1169754.jpeg?auto=compress&cs=tinysrgb&w=1260">
      <div class="content-overlay tint-back">
        <h2 class="title">Ship it<br>Faster.</h2>
        <div class="btn-trigger">Explore More</div>
      </div>
    </div>
  </div>
</div>

<script>
  (function(){
    const viewport = document.getElementById("cardViewport");
    const engine = document.getElementById("cardEngine");
    const imgs = document.querySelectorAll(".bg-image");
    const glares = document.querySelectorAll(".glare-layer");
    let flipped = false;

    // Loading Logic: Reveal when ready
    imgs.forEach(img => {
      if (img.complete) {
        img.classList.add('is-loaded');
      } else {
        img.addEventListener('load', () => img.classList.add('is-loaded'));
      }
    });

    viewport.addEventListener("mousemove", (e) => {
      if (!window.matchMedia("(pointer: coarse)").matches) {
        const rect = viewport.getBoundingClientRect();
        const x = (e.clientX - rect.left) / rect.width;
        const y = (e.clientY - rect.top) / rect.height;
        glares.forEach(g => {
          g.style.setProperty('--x', `${x * 100}%`);
          g.style.setProperty('--y', `${y * 100}%`);
        });
        const moveX = (x - 0.5) * -50;
        const moveY = (y - 0.5) * -50;
        imgs.forEach(img => {
          img.style.transform = `translate(${moveX}px, ${moveY}px)`;
        });
      }
    });

    viewport.addEventListener("mouseenter", () => {
      flipped = true;
      engine.classList.add("is-flipped");
    });
    viewport.addEventListener("mouseleave", () => {
      flipped = false;
      engine.classList.remove("is-flipped");
      imgs.forEach(img => img.style.transform = "translate(0,0)");
    });

    viewport.addEventListener("click", (e) => {
      const isAction = e.target.closest('.title') || e.target.closest('.btn-trigger');
      if (!flipped) {
        flipped = true;
        engine.classList.add("is-flipped");
      } else {
        if (isAction) {
          if (typeof bubble_fn_cardClick === 'function') bubble_fn_cardClick();
        } else {
          flipped = false;
          engine.classList.remove("is-flipped");
        }
      }
    });
  })();
</script>

Tbh with bubble builtin functionality you can’t do that, it’ll require custom coding means you’ve to put html element on page and you can ask any ai about specific requirements you want recommened chatgpt…

Hi Seneca. Thanks so much, but I’m a no-coder so that’s not something I would be able to implement. No plugin that does that?.
Thank you,

Dana :slight_smile:

1 Like

No problem.

I did make it so you can just copy the code and put it in an HTML element wherever you want the card on the page. You can change the size, change the images, and change the text.

But I understand not everyone wants to mess with code.

I would guess there’s a plugin you could use, although I’ve never used one or searched for one that does this.

You could go to plugins and search for card flip or something like that.

If I had more time this morning, I would look.

Maybe someone else can steer you in the direction of a good plugin.

Best of luck with your project. Sorry, I couldn’t help more.

Thank you Seneca. Yeah–I’ve got up to 6 of these on one page so that would be a bit much for me.:slight_smile: