Qaid
ARTICLE

Custom CSS: Six Button Effects

Six feedback button treatments built with the css config option, from frosted glass to 8-bit, with the CSS for each.

Qaid Team

The presets cover most sites. When they don't, the css option takes a raw stylesheet and drops it into the shadow root, and from there the button is yours. Six worked examples follow, each with a live preview and the CSS behind it.

What you are actually styling

The embed renders in a shadow DOM. Your page's stylesheet cannot reach in and the embed's cannot leak out, which is why there is a css option at all: the string you pass gets written into a <style> inside that shadow root.

The button underneath is this:

<button class="qaid-btn-structural your-custom-class qaid-btn-up">
  <svg class="qaid-icon">...</svg>
</button>

Three classes, in this order. qaid-btn-structural handles layout and centering. Your own class from buttonClass comes next, which is where everything below happens. Then qaid-btn-up or qaid-btn-down tells you which button you are on, so you can colour the two differently from one rule set.

Pass the stylesheet and the class name together:

new QaidFeedback({
  endpoint: '/api/feedback',
  buttonClass: 'my-custom-button',
  css: `
    .my-custom-button {
      background: #1a1a2e;
      border: 2px solid #e94560;
      border-radius: 12px;
      color: white;
    }
  `
});

No !important, no long selector chains. Inside the shadow root you are the only stylesheet competing.

Frosted glass

A translucent fill over a blurred backdrop, which needs something behind it to be worth doing:

Glassmorphism

Frosted glass effect with backdrop blur

Loading embed...
.qaid-glass {
  background: rgba(255, 255, 255, 0.15);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 16px;
  padding: 14px;
  color: white;
  transition: all 0.3s ease;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}

.qaid-glass:hover {
  background: rgba(255, 255, 255, 0.25);
  border-color: rgba(255, 255, 255, 0.4);
  transform: translateY(-2px);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
}

.qaid-glass.qaid-btn-up:hover {
  box-shadow: 0 12px 40px rgba(74, 222, 128, 0.3);
}

.qaid-glass.qaid-btn-down:hover {
  box-shadow: 0 12px 40px rgba(248, 113, 113, 0.3);
}

backdrop-filter: blur() does the work. The 15% white fill on top is what keeps the icon readable once everything behind it has gone soft.

Soft extruded buttons

Two shadows, one dark and one light, on the same background colour as the page:

Neumorphism

Soft, extruded button effect

Loading embed...
.qaid-neumorph {
  background: #e0e5ec;
  border: none;
  border-radius: 20px;
  padding: 16px;
  color: #6b7280;
  transition: all 0.2s ease;
  box-shadow:
    8px 8px 16px #b8bcc2,
    -8px -8px 16px #ffffff;
}

.qaid-neumorph:hover {
  box-shadow:
    4px 4px 8px #b8bcc2,
    -4px -4px 8px #ffffff;
}

.qaid-neumorph:active {
  box-shadow:
    inset 4px 4px 8px #b8bcc2,
    inset -4px -4px 8px #ffffff;
}

.qaid-neumorph.qaid-btn-up {
  color: #10b981;
}

.qaid-neumorph.qaid-btn-down {
  color: #ef4444;
}

The shadows shrink on hover and swap sides on :active, so the button reads as pressed into the surface. It only works when the button and the page are the same colour.

A gradient border that moves

CSS has no animatable gradient border, so this fakes one with two pseudo-elements:

Animated Gradient Border

Rainbow border animation on hover

Loading embed...
.qaid-gradient-border {
  position: relative;
  background: #1a1a1a;
  border: none;
  border-radius: 12px;
  padding: 14px;
  color: white;
  z-index: 1;
  transition: all 0.3s ease;
}

.qaid-gradient-border::before {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: 14px;
  background: linear-gradient(45deg, #ff0080, #ff8c00, #40e0d0, #ff0080);
  background-size: 300% 300%;
  z-index: -1;
  opacity: 0;
  transition: opacity 0.3s ease;
}

.qaid-gradient-border::after {
  content: '';
  position: absolute;
  inset: 0;
  background: #1a1a1a;
  border-radius: 12px;
  z-index: -1;
}

.qaid-gradient-border:hover::before {
  opacity: 1;
  animation: gradient-rotate 2s linear infinite;
}

@keyframes gradient-rotate {
  0% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}

::before paints the full gradient rectangle. ::after covers the middle of it back up, leaving a rim. Animating background-position slides the gradient under that rim.

Expanding rings

Two rings leaving the button on a stagger:

Pulse Ripple Effect

Expanding ring animation on interaction

Loading embed...
.qaid-ripple {
  position: relative;
  background: #2a2a4a;
  border: 2px solid #4a4a6a;
  border-radius: 50%;
  padding: 14px;
  color: white;
  overflow: visible;
  transition: all 0.3s ease;
}

.qaid-ripple::before,
.qaid-ripple::after {
  content: '';
  position: absolute;
  inset: -4px;
  border: 2px solid currentColor;
  border-radius: 50%;
  opacity: 0;
}

.qaid-ripple:hover::before {
  animation: ripple-out 1s ease-out infinite;
}

.qaid-ripple:hover::after {
  animation: ripple-out 1s ease-out 0.3s infinite;
}

.qaid-ripple.qaid-btn-up {
  color: #00ff88;
  border-color: #00ff8844;
}

.qaid-ripple.qaid-btn-down {
  color: #ff4466;
  border-color: #ff446644;
}

@keyframes ripple-out {
  0% {
    transform: scale(1);
    opacity: 0.8;
  }
  100% {
    transform: scale(1.8);
    opacity: 0;
  }
}

Both pseudo-elements run the same animation. The half-second delay on the second is the entire effect.

8-bit

No curves, no gradients, and shadows quantised to whole pixels:

Retro Pixel Art

8-bit gaming aesthetic

Loading embed...
.qaid-pixel {
  background: #4ade80;
  border: none;
  padding: 12px 16px;
  color: #000;
  image-rendering: pixelated;
  box-shadow:
    4px 0 0 #000,
    -4px 0 0 #000,
    0 4px 0 #000,
    0 -4px 0 #000,
    4px 4px 0 #000,
    -4px 4px 0 #000,
    4px -4px 0 #000,
    -4px -4px 0 #000,
    0 8px 0 #166534;
  transition: all 0.1s ease;
}

.qaid-pixel:hover {
  transform: translateY(-2px);
  box-shadow:
    4px 0 0 #000, -4px 0 0 #000,
    0 4px 0 #000, 0 -4px 0 #000,
    4px 4px 0 #000, -4px 4px 0 #000,
    4px -4px 0 #000, -4px -4px 0 #000,
    0 10px 0 #166534;
}

.qaid-pixel:active {
  transform: translateY(2px);
  box-shadow:
    4px 0 0 #000, -4px 0 0 #000,
    0 4px 0 #000, 0 -4px 0 #000,
    4px 4px 0 #000, -4px 4px 0 #000,
    4px -4px 0 #000, -4px -4px 0 #000,
    0 4px 0 #166534;
}

.qaid-pixel.qaid-btn-down {
  background: #f87171;
  box-shadow:
    4px 0 0 #000, -4px 0 0 #000,
    0 4px 0 #000, 0 -4px 0 #000,
    4px 4px 0 #000, -4px 4px 0 #000,
    4px -4px 0 #000, -4px -4px 0 #000,
    0 8px 0 #991b1b;
}

Stacked box-shadows at whole-pixel offsets do what a border-radius can't. The darker one along the bottom is the only depth cue.

Icons that move on hover

Scale and rotate the SVG rather than the button:

Icon Morph Effect

Icons scale and rotate on hover

Loading embed...
.qaid-morph {
  background: linear-gradient(135deg, #3b3b3b 0%, #2a2a2a 100%);
  border: 1px solid #444;
  border-radius: 12px;
  padding: 14px;
  color: #888;
  transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}

.qaid-morph .qaid-icon {
  transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}

.qaid-morph:hover {
  border-color: currentColor;
}

.qaid-morph:hover .qaid-icon {
  transform: scale(1.2) rotate(10deg);
}

.qaid-morph.qaid-btn-up {
  color: #4ade80;
}

.qaid-morph.qaid-btn-up:hover {
  background: linear-gradient(135deg, #166534 0%, #14532d 100%);
  box-shadow: 0 0 20px rgba(74, 222, 128, 0.3);
}

.qaid-morph.qaid-btn-down {
  color: #f87171;
}

.qaid-morph.qaid-btn-down:hover {
  background: linear-gradient(135deg, #991b1b 0%, #7f1d1d 100%);
  box-shadow: 0 0 20px rgba(248, 113, 113, 0.3);
}

Keep the rotation under about ten degrees. Past that it reads as a glitch rather than a response.

What these animations cost

Animate transform and opacity. Both run on the GPU and neither triggers layout. Animating box-shadow repaints on every frame, which is why the pixel example above only sets its shadows once instead of transitioning them. Reach for will-change only on an element that genuinely animates, since it costs memory on every element that has it. Then open the page on a mid-range phone, where all of this actually gets tested.

.qaid-optimized {
  will-change: transform;
  transform: translateZ(0); /* Force GPU acceleration */
}

All of it at once

Glass fill, moving gradient rim, and a glow on the icon:

Combined Techniques

Glass + gradient + animation

Loading embed...
.qaid-ultimate {
  position: relative;
  background: rgba(30, 30, 30, 0.8);
  backdrop-filter: blur(8px);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 16px;
  padding: 16px;
  color: white;
  overflow: hidden;
  transition: all 0.4s ease;
}

.qaid-ultimate::before {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: 18px;
  background: conic-gradient(from 0deg, #ff0080, #ff8c00, #40e0d0, #8b5cf6, #ff0080);
  z-index: -2;
  opacity: 0;
  transition: opacity 0.4s ease;
  animation: spin 3s linear infinite;
}

.qaid-ultimate::after {
  content: '';
  position: absolute;
  inset: 1px;
  background: rgba(20, 20, 20, 0.95);
  border-radius: 15px;
  z-index: -1;
}

.qaid-ultimate:hover::before {
  opacity: 1;
}

.qaid-ultimate:hover {
  transform: translateY(-2px);
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
}

.qaid-ultimate .qaid-icon {
  filter: drop-shadow(0 0 8px currentColor);
  transition: filter 0.3s ease;
}

.qaid-ultimate:hover .qaid-icon {
  filter: drop-shadow(0 0 12px currentColor);
}

.qaid-ultimate.qaid-btn-up { color: #4ade80; }
.qaid-ultimate.qaid-btn-down { color: #f87171; }

@keyframes spin {
  to { transform: rotate(360deg); }
}

Three effects is about the ceiling. A fourth stops being noticed and starts costing frames.

Take whichever of these fits your site and delete the rest. A feedback button that draws attention away from the page it sits on is a worse feedback button.

Back to all articles