---
title: "Inner Circle for Shopify theme developers"
description: "Show different markup to a member, a guest or a locked-out visitor, with no flash of the wrong version and no JavaScript to write. On every plan, including Free."
source: https://innercircle.monochrome.digital/developers/theme/
---

[Home](https://innercircle.monochrome.digital/) [Developers](https://innercircle.monochrome.digital/developers/) Inner Circle for Shopify theme developers

Contract

# Inner Circle for theme developers

Show different markup to a member, a guest or a locked-out visitor, with no flash of the wrong version and no JavaScript to write. On every plan, including Free.

Last updated 20 August 2026 · Version 1 · Every plan

## On this page

1. [Before anything else: this is presentation, not security](#before-anything-else-this-is-presentation-not-security)
2. [Requirements](#requirements)
3. [The quickest thing that works](#the-quickest-thing-that-works)
4. [Reading the state from JavaScript](#reading-the-state-from-javascript)
5. [The DOM event](#the-dom-event)
6. [Classes on `<html>`](#classes-on)
7. [Opening Inner Circle's own panels](#opening-inner-circle-s-own-panels)
8. [Dynamic content](#dynamic-content)
9. [Version, and what a bump means](#version-and-what-a-bump-means)
10. [If Inner Circle is uninstalled](#if-inner-circle-is-uninstalled)
11. [What you cannot get, and why](#what-you-cannot-get-and-why)

How to make a theme behave differently for a visitor who is inside the circle.

Everything on this page is a published contract, currently at **version 1**. It will not change under you without the version going up. Anything about Inner Circle that is *not* on this page — other classes, other globals, the shape of the app proxy's responses — is internal and changes without notice.

## Before anything else: this is presentation, not security

The gate is decided on Inner Circle's servers. This contract publishes that decision to your theme so you can lay the page out around it. It does not enforce anything, and it cannot.

Specifically: **if Inner Circle cannot be reached, everything here settles as unlocked and reveals.** That is deliberate — an outage of ours must not take a merchant's storefront down with it — but it means a determined visitor with developer tools can see anything you put behind these attributes.

So: use this to change what the page *looks like*. Do not use it to hold anything that would matter if it leaked. Purchases are stopped server-side by Inner Circle's [checkout validation](https://innercircle.monochrome.digital/features/#not-features), which none of this affects.

## Requirements

The **Inner Circle** app embed must be enabled in the theme editor. It is what loads the runtime, and without it every attribute below is inert and your markup simply shows.

Nothing here is behind a plan. It works on every plan the merchant might be on, including Free. The [read-only API](https://innercircle.monochrome.digital/developers/api/), which is a different thing for a different job, is the one that needs Growth.

## The quickest thing that works

Put `data-ic-show` on any element in any Liquid file:

```liquid
<div data-ic-show="unlocked">
  <a href="/collections/members">Members' collection</a>
</div>

<div data-ic-show="locked">
  <p>This collection is for members. <a href="#inner-circle-code">Enter your code</a></p>
</div>
```

The element is hidden until Inner Circle answers, then shown or hidden to match. There is no flash of the wrong branch.

### The conditions

| Condition | True when |
| --- | --- |
| `unlocked` | This page is not gated for this visitor |
| `locked` | This page is gated for this visitor |
| `member` | This visitor holds access, whether or not this page is gated |
| `guest` | This visitor holds no access |
| `tier:<name>` | This visitor holds the tier with that name |

`data-ic-hide` takes the same values and means the opposite:

```liquid
<div data-ic-hide="locked">Free shipping on every order</div>
```

If an element carries both, `data-ic-hide` wins.

### `unlocked` and `member` are not the same thing

This trips people up, so it is worth being explicit.

`unlocked` is about **the page**: a page with no lock on it is unlocked for everybody, including a first-time visitor.

`member` is about **the person**: they have redeemed a code, followed an invite, or are signed in as a customer the merchant has let in.

A member standing on an ungated page is `unlocked` *and* `member`. A stranger on that same page is `unlocked` and `guest`. If you want "show this to people who are in", you want `member`. If you want "show this instead of the price on a gated page", you want `locked`.

### Tiers match by name, and there is no ordering

`tier:Gold` is true for somebody holding the tier called Gold, matched exactly, ignoring case. There is deliberately no "Gold or above".

Ranking is a rule, and the rules live on Inner Circle's servers. A theme that compared tier ranks would be a second implementation of the access rules, and the two would eventually disagree — which is the failure that would be hardest to explain to a merchant, because both halves would look correct on their own. The server has already applied the ordering by the time it tells you whether the page is `locked`.

If you need "Gold or above" for something the server does not gate, ask the merchant to add a lock and use `locked` / `unlocked` instead. That keeps one answer in one place.

## Reading the state from JavaScript

```js
window.InnerCircle.onReady(function (state) {
  if (state.locked) {
    document.querySelector('.my-banner').textContent = 'Members only';
  }
});
```

`onReady` fires as soon as the gate has settled, or immediately if it already has. It is safe to call from a script that loads before or after Inner Circle's own — the app embed installs it in `<head>`, ahead of everything.

`window.InnerCircle.state()` returns the same object synchronously. Before the gate settles it reports `ready: false` and every other field is meaningless.

### The state object

```js
{
  ready:      true,   // the gate has settled; until then, ignore the rest
  reachedApp: true,   // false if Inner Circle could not be reached — see below
  locked:     false,  // this page is gated for this visitor
  unlocked:   true,   // the inverse of locked, for readability
  hasAccess:  false,  // this visitor holds access (the `member` condition)
  signedIn:   false,  // a Shopify customer is signed in
  tier:       null,   // the name of the tier they hold, or null
  unlocksAt:  null    // ISO timestamp this page stops being gated, or null
}
```

`state()` hands back a copy. Editing it changes nothing.

**`reachedApp: false`** means the gate could not be asked — the app was unreachable, or the app embed is misconfigured. Everything else has fallen open, and this is the field that tells you the difference between "this visitor is allowed in" and "we never found out". If your theme does something expensive or irreversible on an unlocked page, check it.

**`unlocksAt`** is a scheduled drop's end: the moment this page opens to everybody. It is an absolute ISO timestamp so it survives caching, and it is null on plans without scheduling and on locks with no end. Do not count down against the device clock alone if precision matters — Inner Circle sends its own clock for exactly this reason, and its countdowns are drawn against the difference.

## The DOM event

If you would rather listen than register a callback:

```js
document.addEventListener('inner-circle:ready', function (event) {
  console.log(event.detail.state);
});
```

`event.detail.state` is the same object. It fires exactly once per page load, including when Inner Circle could not be reached. **Other properties on `event.detail` are internal** — they are there for Inner Circle's own blocks and will change.

Note the race: if your script runs after the gate has already settled, the event has already fired and you will never see it. `onReady` has no such problem, and is what you should reach for.

## Classes on `<html>`

Useful when CSS alone will do it.

| Class | Meaning |
| --- | --- |
| `ic-pending` | The gate has not answered yet |
| `ic-locked` | This page is gated for this visitor |
| `ic-open` | This page is not gated for this visitor |
| `ic-has-access` | This visitor holds access |

```css
html.ic-locked .my-add-to-cart { display: none; }
```

`ic-pending` is removed either when the answer arrives or when the reveal timeout in the app embed's settings expires, whichever is first. Nothing stays hidden because of an outage.

## Opening Inner Circle's own panels

Any link in your theme can open them — useful for a hero button or a menu item, where an app block cannot be placed.

| Href | Opens |
| --- | --- |
| `#inner-circle-code` | Enter an access code |
| `#inner-circle-request` | Request access |
| `#inner-circle-invite` | Invite a friend (members, where the merchant has invites on) |

Or from JavaScript: `window.InnerCircle.openCodeModal()`, `openRequestModal()`, `openInviteModal()`.

## Dynamic content

Attributes are re-applied when the theme swaps DOM — quick views, filtered grids, AJAX carts, variant changes. Markup you inject after the page has loaded is picked up automatically; you do not need to tell Inner Circle about it.

## Version, and what a bump means

```js
window.InnerCircle.contractVersion   // 1
```

New conditions and new state fields can appear without a bump, so **never assume an unknown condition is an error**. A condition this version does not recognise leaves the element visible, which means a theme written against a later contract degrades to showing more rather than to a blank section.

A bump means something documented here changed meaning or went away. It will not happen quietly.

`window.InnerCircle.version` is a different thing: the build stamp of the runtime, for telling which copy a browser has actually cached. Do not branch on it.

## If Inner Circle is uninstalled

Inner Circle never edits a theme. Everything it puts on a storefront lives in its own theme app extension, which Shopify removes with the app.

The attributes above are the exception, because you typed them into the theme rather than the app putting them there. Nothing removes them, and nothing needs to: with the app gone there is no runtime to hide anything, so every `data-ic-show` and `data-ic-hide` element simply shows. A theme that has had Inner Circle removed is a theme with some unused attributes in it and no visible difference.

That is the same fail-open direction as everything else here, and it is the second reason not to put anything sensitive behind one of these. If you want the markup gone as well as inert, it is a find-and-replace on the attribute name.

## What you cannot get, and why

No lock ids, no lock names, no list of what is gated, no tier rankings, no merchant settings.

Inner Circle's access rules are evaluated in one place on the server, and a theme able to evaluate them too would be a second implementation of them. The two would drift, and the version that lost would be whichever one the merchant was not looking at. So this contract publishes what was decided and never how.

If you have hit a wall this leaves you at, that is worth telling the Inner Circle team about — it is more likely to become a new condition here than a reason to open up the rules.

## Also for developers

[The Inner Circle API](https://innercircle.monochrome.digital/developers/api/) [Ask us something](https://innercircle.monochrome.digital/contact/)
