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, 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, 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:
<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:
<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
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
{
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:
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 |
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
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.