---
title: Embedding dynamic challenges in pages
summary: null
url: >-
  https://www.fastly.com/documentation/guides/security/client-challenges/embedding-dynamic-challenges-in-pages
---

When you embed a dynamic challenge within a page of your web application, you can verify visitors while keeping your application's branding consistent. The challenge will run inline on your webpage instead of redirecting to an [interstitial page](https://www.fastly.com/documentation/guides/security/client-challenges/serving-challenges-with-interstitial-pages). We recommend this approach for React and single-page applications.

## Prerequisites

### Bot Management (pre-cache inspection)

Before setting up client challenges for Bot Management with pre-cache inspection (ContentGuard), you must:

- [purchase](https://docs.fastly.com/products/bot-management) and [deploy](https://www.fastly.com/documentation/guides/security/bot-management/about-bot-management/#deploying-bot-management) Bot Management with pre-cache inspection (ContentGuard) on each CDN service where you intend to use client challenges. Compute services are not supported.
- [configure client challenges](https://www.fastly.com/documentation/guides/security/client-challenges/getting-started-with-client-challenges/#quick-start) for the bot categories or identities you want to challenge.

### Bot Management (post-cache inspection)

Before setting up client challenges for Bot Management with post-cache inspection, you must [purchase](https://docs.fastly.com/products/bot-management) and [deploy](https://www.fastly.com/documentation/guides/security/bot-management/about-bot-management/#deploying-bot-management) Bot Management with post-cache inspection on each CDN service where you intend to use client challenges. Compute services are not supported.

### DDoS

Before setting up client challenges for Fastly DDoS Protection, you must:

- purchase [Fastly DDoS Protection](https://docs.fastly.com/products/fastly-ddos-protection) for your account.
- configure Fastly DDoS Protection to [challenge attack traffic](https://www.fastly.com/documentation/guides/security/client-challenges/getting-started-with-client-challenges/#quick-start).

## Adding embedded challenges

On the page where you want the challenge to appear, complete the following steps:

1. To the page's `<head>`, add the following JS:

   ```plain
   <script src="/_fs-ch-1T1wmsGaOgGaSxcX/challenge.js" defer></script>
   ```

   > **HINT:** As the script waits for the DOM to be ready before loading the challenge, you can use `async` or `async defer` instead of `defer`. For information about these attributes, check out [Mozilla's documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#attributes).

2. In the location of the page where you want the challenge to appear, add the following div:

   ```plain
   <div class="fastly-challenge"></div>
   ```

3. _(Optional)_ [Use CSS](https://www.fastly.com/documentation/guides/security/client-challenges/embedding-dynamic-challenges-in-pages#making-all-challenge-types-visible-on-the-page) to make all challenge types visible. By default, embedded challenges don't display on the page, unless Fastly detects suspicious activity and chooses an interactive challenge (e.g., CAPTCHA challenge).

4. _(Optional)_ If you've deployed Bot Management with post-cache inspection, [set up rules](https://www.fastly.com/documentation/guides/security/client-challenges/blocking-requests-with-invalid-challenge-tokens) to block requests from clients that failed a challenge.

## Making all challenge types visible on the page

By default, embedded challenges don't display on the page unless Fastly detects suspicious activity and chooses an interactive challenge (e.g., CAPTCHA challenge). To make all challenge types visible, use the following CSS directives:

```plain
fastly-challenge[data-challenge-status="started"]
fastly-challenge[data-challenge-status="processing"]
fastly-challenge[data-challenge-status="complete"]
fastly-challenge[data-challenge-status="error"]
fastly-challenge[data-challenge-status="captcha_prompted"]
```

### Sample CSS

Below is a sample CSS that makes all challenge types visible on the page.

```plain truncate
/* Base container styling - Pre-allocate space to prevent layout shift */
.fastly-challenge {
content:"Powered by Fastly";
   display: inline-flex;
   align-items: center;
   min-height: 32px;
   min-width: 140px;
   position: relative;
   contain: layout;
}

/* Initialize with a placeholder to prevent empty state */
.fastly-challenge:empty::before,
.fastly-challenge:not([data-challenge-status])::before {
   content: "";
   width: 32px;
   height: 32px;
   display: inline-flex;
   border-radius: 50%;
   background: #f3f4f6;
   border: 2px solid #e5e7eb;
   opacity: 0.5;
   animation: gentlePulse 2s ease-in-out infinite;
}

/* Challenge indicator base - Force immediate rendering */
.fastly-challenge[data-challenge-status]::before {
   content: "";
   width: 32px;
   height: 32px;
   display: inline-flex;
   align-items: center;
   justify-content: center;
   border-radius: 50%;
   position: relative;
   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
   /* Force immediate paint in Firefox */
   transform: translateZ(0) scale(1);
   will-change: transform, background;
   /* Ensure transitions work in Firefox */
   transition-property: transform, box-shadow, background;
   transition-duration: 0.3s;
   transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

/* Status text label - Ensure Firefox renders this */
.fastly-challenge[data-challenge-status]::after {
   content: attr(data-status-text);
   margin-left: 12px;
   font-size: 14px;
   font-weight: 500;
   letter-spacing: 0.025em;
   opacity: 0;
   transform: translateX(-10px) translateZ(0);
   /* Delay animation to ensure it's visible */
   animation: fadeInSlide 0.5s ease forwards;
   animation-delay: 0.1s;
   /* Force text rendering in Firefox */
   -moz-osx-font-smoothing: grayscale;
   text-rendering: optimizeLegibility;
}

/* COMPLETE STATE */
.fastly-challenge[data-challenge-status="complete"]::before {
   background: linear-gradient(135deg, #10b981 0%, #059669 100%);
   /* Ensure animation plays even on quick transitions */
   animation: successPulse 0.8s ease both;
   animation-fill-mode: both;
}

.fastly-challenge[data-challenge-status="complete"]::after {
   content: "Verified";
   color: #059669;
   animation-delay: 0.3s; /* Delay text to sync with icon */
}

/* Checkmark icon for complete */
.fastly-challenge[data-challenge-status="complete"]::before {
   background-image:
         linear-gradient(135deg, #10b981 0%, #059669 100%),
         url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
   background-size: 100%, 18px 18px;
   background-position: center, center;
   background-repeat: no-repeat, no-repeat;
}

/* ERROR STATE */
.fastly-challenge[data-challenge-status="error"]::before {
   background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
   animation: errorShake 0.6s ease both;
}

.fastly-challenge[data-challenge-status="error"]::after {
   content: "Error";
   color: #dc2626;
}

.fastly-challenge[data-challenge-status="error"]::before {
   background-image:
         linear-gradient(135deg, #ef4444 0%, #dc2626 100%),
         url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");
   background-size: 100%, 18px 18px;
   background-position: center, center;
   background-repeat: no-repeat, no-repeat;
}

/* STARTED STATE */
.fastly-challenge[data-challenge-status="started"]::before {
   background: #ffffff;
   border: 3px solid #f59e0b;
   box-shadow: 0 2px 8px rgba(245, 158, 11, 0.3);
   /* Ensure animation starts immediately */
   animation: gentlePulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
   animation-play-state: running;
}

.fastly-challenge[data-challenge-status="started"]::after {
   content: "Initializing...";
   color: #d97706;
}

/* PROCESSING STATE - Firefox-safe spinner */
.fastly-challenge[data-challenge-status="processing"]::before {
   background: #ffffff;
   border: 3px solid #e5e7eb;
   border-top-color: #3b82f6;
   border-right-color: #3b82f6;
   box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
   /* Use Firefox-compatible animation */
   animation: spinSimple 1s linear infinite;
   animation-play-state: running;
   /* Force animation in Firefox */
   -moz-animation: spinSimple 1s linear infinite;
}

.fastly-challenge[data-challenge-status="processing"]::after {
   content: "Processing...";
   color: #2563eb;
   /* Ensure text shows immediately */
   animation: fadeInSlide 0.3s ease forwards, textPulse 1.5s ease infinite;
   animation-delay: 0s, 0.3s;
}

/* ===================================
   FIREFOX-SPECIFIC OVERRIDES
=================================== */

@-moz-document url-prefix() {
   /* Firefox-specific optimizations */
   .fastly-challenge[data-challenge-status]::before {
         /* Force repaint in Firefox */
         backface-visibility: hidden;
         perspective: 1000px;
         transform-style: preserve-3d;
   }

   /* Ensure processing animation works in Firefox */
   .fastly-challenge[data-challenge-status="processing"]::before {
         animation-name: spinSimple !important;
         animation-duration: 1s !important;
         animation-timing-function: linear !important;
         animation-iteration-count: infinite !important;
   }

   /* Force text to render immediately in Firefox */
   .fastly-challenge[data-challenge-status]::after {
         will-change: opacity, transform;
   }
}

/* ===================================
   ANIMATIONS (Firefox-optimized)
=================================== */

@keyframes gentlePulse {
   0%, 100% {
         opacity: 1;
         transform: scale(1) translateZ(0);
   }
   50% {
         opacity: 0.7;
         transform: scale(0.95) translateZ(0);
   }
}

/* Firefox-compatible simple spin */
@keyframes spinSimple {
   0% {
         transform: rotate(0deg) translateZ(0);
   }
   100% {
         transform: rotate(360deg) translateZ(0);
   }
}

/* Firefox prefix for spin animation */
@-moz-keyframes spinSimple {
   0% {
         transform: rotate(0deg);
   }
   100% {
         transform: rotate(360deg);
   }
}

@keyframes fadeInSlide {
   0% {
         opacity: 0;
         transform: translateX(-10px) translateZ(0);
   }
   100% {
         opacity: 1;
         transform: translateX(0) translateZ(0);
   }
}

@keyframes successPulse {
   0% {
         transform: scale(0.9) translateZ(0);
         opacity: 0.8;
         box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
   }
   50% {
         transform: scale(1.05) translateZ(0);
         opacity: 1;
         box-shadow: 0 0 0 10px rgba(16, 185, 129, 0);
   }
   100% {
         transform: scale(1) translateZ(0);
         opacity: 1;
         box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
   }
}

@keyframes errorShake {
   0%, 100% {
         transform: translateX(0) translateZ(0);
   }
   10%, 30%, 50%, 70%, 90% {
         transform: translateX(-2px) translateZ(0);
   }
   20%, 40%, 60%, 80% {
         transform: translateX(2px) translateZ(0);
   }
}

@keyframes textPulse {
   0%, 100% {
         opacity: 1;
   }
   50% {
         opacity: 0.6;
   }
}

@keyframes captchaBounce {
   0% {
         transform: scale(0.85) translateZ(0);
         opacity: 0.8;
   }
   40% {
         transform: scale(1.1) translateZ(0);
         opacity: 1;
   }
   100% {
         transform: scale(1) translateZ(0);
         opacity: 1;
   }
}

/* ===================================
   HOVER EFFECTS (Optimized)
=================================== */

.fastly-challenge[data-challenge-status="complete"]:hover::before,
.fastly-challenge[data-challenge-status="error"]:hover::before {
   transform: scale(1.1) translateZ(0);
   transition: transform 0.2s ease;
}

/* ===================================
   RESPONSIVE ADJUSTMENTS
=================================== */

@media (max-width: 480px) {
   .fastly-challenge[data-challenge-status]::after {
         font-size: 13px;
   }

   .fastly-challenge[data-challenge-status]::before {
         width: 28px;
         height: 28px;
   }
}

/* ===================================
   DARK MODE SUPPORT (optional)
=================================== */

@media (prefers-color-scheme: dark) {
   .fastly-challenge[data-challenge-status]::before {
         box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
   }
}
```

## Related content

### Bot Management (pre-cache inspection)

- [Getting started with client challenges](https://www.fastly.com/documentation/guides/security/client-challenges/getting-started-with-client-challenges)
- [Managing bot traffic](https://www.fastly.com/documentation/guides/security/bot-management/contentguard/managing-bot-traffic)
- [Monitoring bot traffic](https://www.fastly.com/documentation/guides/security/bot-management/contentguard/monitoring-bot-traffic)

### Bot Management (post-cache inspection)

- [Getting started with client challenges](https://www.fastly.com/documentation/guides/security/client-challenges/getting-started-with-client-challenges)
- [Blocking requests with invalid challenge tokens](https://www.fastly.com/documentation/guides/security/client-challenges/blocking-requests-with-invalid-challenge-tokens)

### DDoS

- [Getting started with client challenges](https://www.fastly.com/documentation/guides/security/client-challenges/getting-started-with-client-challenges)
- [About the DDoS protection controls](https://www.fastly.com/documentation/guides/security/ddos-protection/about-the-ddos-protection-controls)


