# How to customize Cloudflare Access pages

> Customize Cloudflare Access login, denial, and CLI pages with HTML and Liquid, and tell a blocked visitor what to do next.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-10-29 | Topics: [Cloudflare](https://flaviocopes.com/tags/cloudflare/) | Canonical: https://flaviocopes.com/cloudflare-access-custom-pages/

Cloudflare Access can show your own HTML on the login page, and on the pages it shows when a policy blocks someone. A separate template covers the screen where they approve a command-line login. Liquid prints the app name and the email they used. You write the sentence that tells them what to do next.

The allow or block decision stays with the policy. The HTML is the page they read while that happens.

Kenny Johnson announced this on October 27, 2026, with a [denial page](https://x.com/KennyJohnsonATX/status/2115179775899603411) that lists every check that failed. He said a real page does not have to look like that demo. The [Access custom pages docs](https://developers.cloudflare.com/cloudflare-one/reusable-components/custom-pages/access-custom-pages/) have the page types and the setup. The [changelog entry](https://developers.cloudflare.com/changelog/post/2026-10-27-access-custom-pages/) covers the same change in a few lines.

## Four pages, four templates

Each page type is its own template. Access picks one from what happened to the request.

```mermaid
flowchart TD
  accTitle: Which custom page Access shows
  accDescr: A visitor with no session gets the login page. A signed-in visitor blocked by an identity policy gets the identity denied page. A failed service authentication policy gets the forbidden page. A command-line login waiting for approval gets the interstitial page.
  A[No session] --> L[Login]
  L ~~~ B[Identity policy blocks them]
  B --> D[Identity denied]
  D ~~~ C[Service auth fails]
  C --> F[Forbidden]
  F ~~~ E[CLI login waiting]
  E --> I[Interstitial]
```

| Page | When it shows | What you leave in the HTML |
| --- | --- | --- |
| Login | The visitor has no session | One `<cf-access-authbox>` |
| Identity denied | They signed in, and an identity policy blocked them | No control of your own |
| Forbidden | A non-identity policy blocked them, such as service authentication | No control of your own |
| Interstitial | A command-line login is waiting for approval | One `<cf-access-cli-approval>` |

Access inserts its own controls into those two elements. Each one has to render once, outside any `if` or loop. Having the tag in the source is not enough if a condition skips it. Do not replace them with your own form, a link to an identity provider, or your own approve button.

## Tell a blocked person what to do

I would write the identity denied page first. The visitor has already signed in, and the policy said no. Your copy can name the app and the email Access saw, then say who to contact.

On this page the app name is `app.name`. On the login page it is `app_name`. A name that does not exist for that page type prints as empty text, so the two are not interchangeable. The Variables list in the editor is the list for the type you selected.

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>No access to {{ app.name }}</title>
  </head>
  <body>
    <main>
      <h1>No access to {{ app.name }}</h1>
      <p>You reached an app that only a few people can open.</p>
      {% if user.email %}
      <p>You are signed in as {{ user.email }}.</p>
      {% endif %}
      <p>If you need access, email the person who runs this app and include the Ray ID below.</p>
      {% if ray_id %}
      <p>Ray ID: {{ ray_id }}</p>
      {% endif %}
    </main>
  </body>
</html>
```

`user.email` and `ray_id` are optional. The `if` blocks skip them when Access does not send them. Liquid escapes what it prints, so the email stays text.

This page type can also receive `policy_evaluation`, the structured result of the checks. You choose which fields to show. The demo on X walks through each failed check. I would leave that off a page a visitor sees, because a group name or a claim is more than they need, and the docs say to skip details you do not need.

## The login page

The login template shows the app name, any sign-in error, and the Access control. This is the starter from the docs, with the one element you cannot remove:

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Sign in to {{ app_name }}</title>
  </head>
  <body>
    <main>
      <h1>Sign in to {{ app_name }}</h1>
      {% if message.text.size > 0 %}
      <p role="status">{{ message.text }}</p>
      {% endif %}
      <cf-access-authbox></cf-access-authbox>
    </main>
  </body>
</html>
```

Keep `message.text` on the page. That is how Access reports a problem with the sign-in. `idps` is display information for the identity providers. It is not a list of sign-in URLs, so do not paste one into the template.

## The command-line approval page

The interstitial template is the screen someone sees when a CLI login needs a yes or a no. It needs this element once, outside any condition:

```html
<cf-access-cli-approval></cf-access-cli-approval>
```

Access puts the approve and deny controls inside it. The template data for this type includes `requesting_tool` and an optional `user`. The approval credentials stay in the Access control, not in your HTML. Start from the starter the editor loads for this type, and add a line of context around that element.

## Create it, then assign it to one application

In Cloudflare One, go to **Reusable components**, then **Custom pages**. On the Access custom pages card, select **Manage**, then **Create custom page**. Name it, pick a type, and the editor loads a starter. Edit the HTML, read **Problems**, fix the blocking errors, and select **Publish**. If validation is unavailable, select **Retry validation** before you publish.

Publishing only saves the template. Access shows it after you assign it to an application, or after you set it as the account default.

For a self-hosted application, the docs walk through the assignment like this:

1. Go to **Access controls**, then **Applications**, and open the application.
2. Open **Additional settings**. The section is still named **Custom block pages**. The login and interstitial settings are in that section too.
3. Pick the row that matches the template. **Identity failure block page** takes an identity denied template, under **Custom page template**. **Non-identity failure block page** takes a forbidden template. **Login page** takes a login template, under **Custom page**. **Interstitial page** takes an interstitial template. Each dropdown only lists templates of that type.
4. Save, then try the flow. Give the change a moment to propagate before you test again.

To remove an assignment, choose **Cloudflare default** for that page and save. If an account default exists, Access uses it when the application has no template of its own. With neither, Access uses the built-in page.

**Set as default** and **Remove as default** are actions on the Access custom pages list. An application assignment wins over the account default for the same type. Try one application before you change the default. A default reaches every application that inherits that page type.

The Problems panel checks the template for errors. It never renders the page, and the editor has no download. A clean panel can still hide an `if` that skips a line you expected to see. Open the application while signed in as someone the policy rejects, check a long email, and check a narrow window.

Keep a copy of the HTML outside the dashboard. Publishing an edit changes every application that uses that template. A second template is how you try a change without touching the ones already assigned. The type stays fixed after you create it. Before you delete a template, point its applications somewhere else and remove it as a default.

## What the template cannot do

Templates run in a restricted Liquid environment.

- No custom JavaScript, no inline event handlers, and no authentication form of your own. Access owns the sign-in and the approval controls.
- The Liquid tags `include`, `render`, `layout`, `echo`, and `raw` are unavailable, and so is the `raw` filter.
- HTML and CSS are sanitized. Images and fonts have to be HTTPS URLs the page's Content Security Policy allows. Do not count on a third-party script to draw the page.
- Parsing, rendering, and memory have limits. Keep the template small, and skip a loop over a long list.

If a saved template is missing or cannot render safely, Access falls back to a built-in page. The allow or deny decision stays as it was.

## How I would use it

I already put Access in front of [Worker previews](https://flaviocopes.com/cloudflare-worker-previews/) and in front of [tunnels](https://flaviocopes.com/cloudflare-tunnel/) to machines at home. The built-in block page names the denial and stops, so the visitor still has to guess which app they opened and who can add them.

On one preview hostname I would use the identity denied template, and I would keep that HTML in the project's repo. Then I would open the preview while signed in with an email the policy rejects, and read the page on a phone.

The account default can wait until that hostname looks right.

The login page can stay close to the starter, with the app name and `message.text`. The sign-in buttons have to stay inside `<cf-access-authbox>`.

I would leave the failed policies off the page, because the email and the Ray ID are enough for them to ask someone.

## If the new editor is not there

The day after the announcement, a couple of people said the Access custom pages card still showed the older fields, with no **Create custom page** action. Kenny replied to one of them that he would check the account.

If that is what you see, the dashboard is still offering the [legacy block pages](https://developers.cloudflare.com/cloudflare-one/reusable-components/custom-pages/access-block-page/). Those are the previous HTML templates. The HTML and Liquid pages are the ones in the docs linked above.
