Overlays overlaysfeedbackfloatingwidgetsurvey

Floating Feedback Widget

Fixed bottom-right collapsible feedback trigger with emoji satisfaction ratings and text submission textarea.

Preview

Usage

Copy the full block below to use this component with its imports.

astro
---
import { FloatingFeedbackWidget } from 'astro-component-kit';
---

<FloatingFeedbackWidget placeholder="How can we improve this experience?" />
--- import { FloatingFeedbackWidget } from 'astro-component-kit'; --- <FloatingFeedbackWidget placeholder="How can we improve this experience?" />

Manual Installation

If you are not using the npm package, create a new file src/components/lib/FloatingFeedbackWidget.astro and paste the following code:

astro
---
/**
 * FloatingFeedbackWidget — Fixed corner floating pill that expands into an interactive feedback survey card.
 * 
 * @param {string} title - Widget title. Default is 'Was this helpful?'.
 * @param {string} position - Screen anchor ('bottom-right' | 'bottom-left'). Default is 'bottom-right'.
 */
interface Props {
  title?: string;
  position?: 'bottom-right' | 'bottom-left';
}

const {
  title = 'Send Feedback',
  position = 'bottom-right',
} = Astro.props;
---

<div class:list={['fb-widget', `fb-widget--${position}`]} data-feedback-widget>
  <button type="button" class="fb-widget__trigger" aria-label="Open feedback modal" data-fb-trigger>
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
    </svg>
    <span>Feedback</span>
  </button>

  <div class="fb-widget__card" data-fb-card hidden>
    <div class="fb-widget__head">
      <h5 class="fb-widget__title">{title}</h5>
      <button type="button" class="fb-widget__close" aria-label="Close" data-fb-close>×</button>
    </div>

    <div class="fb-widget__emojis">
      <button type="button" class="emoji-btn" aria-label="Sad">🙁</button>
      <button type="button" class="emoji-btn" aria-label="Neutral">😐</button>
      <button type="button" class="emoji-btn emoji-btn--selected" aria-label="Happy">😊</button>
      <button type="button" class="emoji-btn" aria-label="Love it">🤩</button>
    </div>

    <textarea
      placeholder="Any details to share? (optional)"
      class="fb-widget__textarea"
      rows="2"
    ></textarea>

    <button type="button" class="fb-widget__send" data-fb-send>
      Send Feedback
    </button>
  </div>
</div>

<style>
  .fb-widget {
    position: fixed;
    z-index: 90;
    font-family: var(--font-sans, system-ui, sans-serif);
  }

  .fb-widget--bottom-right { bottom: 1.5rem; right: 1.5rem; }
  .fb-widget--bottom-left  { bottom: 1.5rem; left: 1.5rem; }

  .fb-widget__trigger {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 0.6rem 1.1rem;
    background: rgba(14, 14, 18, 0.9);
    backdrop-filter: blur(16px);
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: var(--r-full, 9999px);
    color: var(--c-text-1, #ffffff);
    font-size: 0.82rem;
    font-weight: 700;
    cursor: pointer;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
    transition: transform 0.2s ease, border-color 0.2s ease;
  }

  .fb-widget__trigger:hover {
    transform: translateY(-2px);
    border-color: rgba(184, 134, 11, 0.4);
    color: var(--c-primary-light, #d4a017);
  }

  .fb-widget__card {
    position: absolute;
    bottom: calc(100% + 12px);
    right: 0;
    width: 280px;
    padding: 1.2rem;
    background: rgba(12, 12, 16, 0.95);
    backdrop-filter: blur(24px);
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: var(--r-xl, 20px);
    box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6);
    display: flex;
    flex-direction: column;
    gap: 12px;
  }

  .fb-widget__head {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }

  .fb-widget__title {
    font-size: 0.88rem;
    font-weight: 800;
    color: var(--c-text-1, #ffffff);
    margin: 0;
  }

  .fb-widget__close {
    background: transparent;
    border: none;
    color: var(--c-text-3, #737373);
    font-size: 1.2rem;
    line-height: 1;
    cursor: pointer;
  }

  .fb-widget__emojis {
    display: flex;
    justify-content: space-between;
    gap: 6px;
  }

  .emoji-btn {
    flex: 1;
    font-size: 1.4rem;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: var(--r-md, 10px);
    padding: 6px 0;
    cursor: pointer;
    transition: transform 0.2s ease, background 0.2s ease;
  }

  .emoji-btn:hover,
  .emoji-btn--selected {
    transform: scale(1.15);
    background: rgba(184, 134, 11, 0.2);
    border-color: rgba(184, 134, 11, 0.4);
  }

  .fb-widget__textarea {
    width: 100%;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: var(--r-sm, 8px);
    color: var(--c-text-1, #ffffff);
    font-size: 0.8rem;
    padding: 8px;
    resize: none;
    outline: none;
    font-family: inherit;
  }

  .fb-widget__send {
    padding: 0.6rem;
    background: linear-gradient(135deg, #d4a017, #b8860b);
    border: none;
    border-radius: var(--r-sm, 8px);
    color: #050505;
    font-size: 0.8rem;
    font-weight: 800;
    cursor: pointer;
    transition: opacity 0.2s;
  }

  .fb-widget__send:hover {
    opacity: 0.9;
  }
</style>

<script>
  function setupFeedbackWidgets() {
    document.querySelectorAll('[data-feedback-widget]').forEach((widget) => {
      const trigger = widget.querySelector('[data-fb-trigger]');
      const card = widget.querySelector('[data-fb-card]') as HTMLElement;
      const close = widget.querySelector('[data-fb-close]');
      const send = widget.querySelector('[data-fb-send]');
      if (!trigger || !card) return;

      trigger.addEventListener('click', () => {
        card.hidden = !card.hidden;
      });

      close?.addEventListener('click', () => {
        card.hidden = true;
      });

      send?.addEventListener('click', () => {
        card.innerHTML = `<p style="text-align:center;font-size:0.85rem;color:#10b981;font-weight:700;margin:1rem 0;">✓ Thank you for your feedback!</p>`;
        setTimeout(() => { card.hidden = true; }, 2000);
      });
    });
  }

  setupFeedbackWidgets();
  document.addEventListener('astro:page-load', setupFeedbackWidgets);
</script>
--- /** * FloatingFeedbackWidget — Fixed corner floating pill that expands into an interactive feedback survey card. * * @param {string} title - Widget title. Default is 'Was this helpful?'. * @param {string} position - Screen anchor ('bottom-right' | 'bottom-left'). Default is 'bottom-right'. */ interface Props { title?: string; position?: 'bottom-right' | 'bottom-left'; } const { title = 'Send Feedback', position = 'bottom-right', } = Astro.props; --- <div class:list={['fb-widget', `fb-widget--${position}`]} data-feedback-widget> <button type="button" class="fb-widget__trigger" aria-label="Open feedback modal" data-fb-trigger> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/> </svg> <span>Feedback</span> </button> <div class="fb-widget__card" data-fb-card hidden> <div class="fb-widget__head"> <h5 class="fb-widget__title">{title}</h5> <button type="button" class="fb-widget__close" aria-label="Close" data-fb-close>×</button> </div> <div class="fb-widget__emojis"> <button type="button" class="emoji-btn" aria-label="Sad">🙁</button> <button type="button" class="emoji-btn" aria-label="Neutral">😐</button> <button type="button" class="emoji-btn emoji-btn--selected" aria-label="Happy">😊</button> <button type="button" class="emoji-btn" aria-label="Love it">🤩</button> </div> <textarea placeholder="Any details to share? (optional)" class="fb-widget__textarea" rows="2" ></textarea> <button type="button" class="fb-widget__send" data-fb-send> Send Feedback </button> </div> </div> <style> .fb-widget { position: fixed; z-index: 90; font-family: var(--font-sans, system-ui, sans-serif); } .fb-widget--bottom-right { bottom: 1.5rem; right: 1.5rem; } .fb-widget--bottom-left { bottom: 1.5rem; left: 1.5rem; } .fb-widget__trigger { display: inline-flex; align-items: center; gap: 8px; padding: 0.6rem 1.1rem; background: rgba(14, 14, 18, 0.9); backdrop-filter: blur(16px); border: 1px solid rgba(255, 255, 255, 0.12); border-radius: var(--r-full, 9999px); color: var(--c-text-1, #ffffff); font-size: 0.82rem; font-weight: 700; cursor: pointer; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); transition: transform 0.2s ease, border-color 0.2s ease; } .fb-widget__trigger:hover { transform: translateY(-2px); border-color: rgba(184, 134, 11, 0.4); color: var(--c-primary-light, #d4a017); } .fb-widget__card { position: absolute; bottom: calc(100% + 12px); right: 0; width: 280px; padding: 1.2rem; background: rgba(12, 12, 16, 0.95); backdrop-filter: blur(24px); border: 1px solid rgba(255, 255, 255, 0.12); border-radius: var(--r-xl, 20px); box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6); display: flex; flex-direction: column; gap: 12px; } .fb-widget__head { display: flex; justify-content: space-between; align-items: center; } .fb-widget__title { font-size: 0.88rem; font-weight: 800; color: var(--c-text-1, #ffffff); margin: 0; } .fb-widget__close { background: transparent; border: none; color: var(--c-text-3, #737373); font-size: 1.2rem; line-height: 1; cursor: pointer; } .fb-widget__emojis { display: flex; justify-content: space-between; gap: 6px; } .emoji-btn { flex: 1; font-size: 1.4rem; background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: var(--r-md, 10px); padding: 6px 0; cursor: pointer; transition: transform 0.2s ease, background 0.2s ease; } .emoji-btn:hover, .emoji-btn--selected { transform: scale(1.15); background: rgba(184, 134, 11, 0.2); border-color: rgba(184, 134, 11, 0.4); } .fb-widget__textarea { width: 100%; background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: var(--r-sm, 8px); color: var(--c-text-1, #ffffff); font-size: 0.8rem; padding: 8px; resize: none; outline: none; font-family: inherit; } .fb-widget__send { padding: 0.6rem; background: linear-gradient(135deg, #d4a017, #b8860b); border: none; border-radius: var(--r-sm, 8px); color: #050505; font-size: 0.8rem; font-weight: 800; cursor: pointer; transition: opacity 0.2s; } .fb-widget__send:hover { opacity: 0.9; } </style> <script> function setupFeedbackWidgets() { document.querySelectorAll('[data-feedback-widget]').forEach((widget) => { const trigger = widget.querySelector('[data-fb-trigger]'); const card = widget.querySelector('[data-fb-card]') as HTMLElement; const close = widget.querySelector('[data-fb-close]'); const send = widget.querySelector('[data-fb-send]'); if (!trigger || !card) return; trigger.addEventListener('click', () => { card.hidden = !card.hidden; }); close?.addEventListener('click', () => { card.hidden = true; }); send?.addEventListener('click', () => { card.innerHTML = `<p style="text-align:center;font-size:0.85rem;color:#10b981;font-weight:700;margin:1rem 0;">✓ Thank you for your feedback!</p>`; setTimeout(() => { card.hidden = true; }, 2000); }); }); } setupFeedbackWidgets(); document.addEventListener('astro:page-load', setupFeedbackWidgets); </script>

Quick Info

Category
Overlays
Filename
FloatingFeedbackWidget.astro
Dependencies
None — pure Astro + CSS
Tags
overlaysfeedbackfloatingwidgetsurvey