# Customizing the Pryk bar: reference for AI agents

Use this to make the Pryk cookie bar match a website's design. It lists everything that can be changed, what must not be changed, and how to check the result.

## Procedure

1. **Find the install.** Search the project for `pryk.click/k/`. If it isn't there, look for a Pryk tag in Google Tag Manager, or ask the owner for their key. The tag looks like this and must stay first in `<head>`:
   ```html
   <script src="https://pryk.click/k/YOUR_KEY.js"></script>
   ```
2. **Read the site's design.** Prefer source over guessing: CSS variables, Tailwind or theme config, design tokens. Otherwise read computed styles from the page body, a card and the primary button. Collect the colors in [Theme tokens](#theme-tokens), the font stack, corner radii, how dark mode works, and anything fixed to the bottom of the screen (chat widget, sticky footer, mobile nav).
3. **Choose the dark mode approach** from [Dark mode](#dark-mode). Skip this and the bar can look wrong to visitors whose OS setting differs from the site.
4. **Write the config** at the lowest level that works:
   - Level 1: JSON ([Config keys](#config-keys), [Theme tokens](#theme-tokens), [Text keys](#text-keys)).
   - Level 2: the site's CSS, with tokens and [parts](#parts).
   - Level 3: the `css` key, only for elements that have no part.
5. **Check the [hard rules](#hard-rules).**
6. **[Verify](#verify)** in the opt-in, Italian and US versions, on desktop and at 375px wide, in light and dark.
7. **Report back:** what you changed, an [explorer preview link](#preview-without-a-browser-session), and anything that needs the Pryk operator ([limits](#what-the-site-cannot-change)).

## How the bar is built

- It is a `<pryk-consent>` custom element with an open shadow root, appended to `<html>`, not `<body>`.
- The host has `all: initial`, so it inherits no fonts or colors from the page. CSS custom properties are the exception: they do inherit into it.
- All values go through `--pryk-*` tokens declared on the host.
- Copy is rendered as text. HTML in a text value appears literally.
- Priority, highest first: `!important` on the host, `theme` JSON (written as inline styles on the host), the site's CSS, the bar's defaults.
- There is no API to change the theme after load. Changing tokens with CSS takes effect live. Changing JSON needs a page reload.

## Where config goes

Config set on the site applies on the next page load, with no Pryk deploy.

| Install | How to set config | Notes |
| --- | --- | --- |
| Script tag, normal | Inline script directly before the Pryk script | Must run before the Pryk script |
| Script tag, strict CSP | `data-config` on the Pryk script | No inline script needed |
| Google Tag Manager | The Pryk tag's "Full JSON" field | That field replaces the tag's simple fields |
| Any install | The site's stylesheet, for tokens and parts | Works regardless of load order |

```html
<script>window.PrykConfig = { "layout": "box", "theme": { "buttonBg": "#0b57d0" } };</script>
<script src="https://pryk.click/k/YOUR_KEY.js"></script>
```

```html
<script src="https://pryk.click/k/YOUR_KEY.js" data-config='{"layout":"box","theme":{"buttonBg":"#0b57d0"}}'></script>
```

- If both are present, `data-config` keys replace `window.PrykConfig` keys whole. A `theme` in `data-config` replaces the entire `theme` in `window.PrykConfig`. Use one or the other.
- Invalid JSON in `data-config` is ignored, with a console warning.
- An apostrophe in a `data-config` value ends the attribute. Write it as `&#39;`.
- Keep the Pryk script parser-blocking and before other scripts. Don't add `async`, `defer` or `type="module"`, and don't move it into a framework script loader that runs after hydration. Pryk can only hold back scripts that come after it, and with `type="module"` it can't read `data-config`.
- The site's CSP must allow `https://pryk.click` in `script-src`, and the logo's origin in `img-src` if you set `logo`.
- The Pryk operator can also set appearance in the key's `config_json`. Site config overrides it key by key: `theme`, `reopener` and each language in `text` are merged, `css` is appended, and the rest is replaced.

## Hard rules

These exist because regulators treat the opposite as a dark pattern, and because they protect the site owner. Don't break them even if asked to match a design. If a request conflicts, explain why and offer the closest compliant version.

Must not:

- Make Reject weaker than Accept. Keep the same background, text color, font size and height, at opacity 0.8 or higher. Change the two together, or use `theme.buttonBg` and `theme.buttonFg`, which style both.
- Hide or shrink out of view any of these parts: `reject`, `customize`, `opt-out`, `close`, `links`, `gpc-notice`, or anything else that lets a visitor refuse or get information.
- Reorder the buttons or use `order` to push Reject out of first place.
- Remove the reopen button, unless every page has a visible `data-pryk-open` link instead. Withdrawing must stay as easy as agreeing. US visitors need that link to read "Do Not Sell or Share My Personal Information".
- Rewrite copy so that it misstates what the site does. Don't soften `messageUsSale`, `doNotSell` or `gpcHonored`: their wording follows US law. Treat wording changes to `message*` keys as something the owner should have reviewed.
- Pre-tick anything, auto-accept, or call `Pryk.acceptAll()` without a visitor's action.
- Replace the bar with custom UI, or edit or self-host the key file.

Must:

- Keep text contrast at 4.5:1 or higher for `buttonFg` on `buttonBg`, `muted` on `bg` and `fg` on `bg`.
- Keep `accent` visible against `bg`, at 3:1 or higher. It colors the focus ring and the enabled switch.
- Set `policyUrl` if the key doesn't already have one. The Italian regulator expects a cookie policy link in the banner.
- Keep button text able to wrap. The US label is long. Don't use `white-space: nowrap` on buttons.

## Config keys

Validate against the [JSON Schema](https://pryk.click/pryk-config.schema.json). Unknown keys are ignored. Keys that change legal behaviour have no effect from the page.

| Key | Type | Default | Effect |
| --- | --- | --- | --- |
| `layout` | `bar`, `box`, `modal` | `bar` | Banner shape |
| `position` | `bottom`, `top`, `bottom-left`, `bottom-right` | `bottom` | See below |
| `theme` | object | | See [Theme tokens](#theme-tokens) |
| `logo` | URL | none | Image beside the title, 22px high. Part: `logo` |
| `text` | object | language packs | Wording, by language. See [Text keys](#text-keys) |
| `css` | string | none | Injected into the shadow root, after the defaults |
| `reopener.show` | boolean | `true` | Button that reopens preferences after a choice. See [hard rules](#hard-rules) |
| `reopener.position` | `bottom-left`, `bottom-right` | `bottom-left` | Its corner |
| `reopener.style` | `icon`, `label` | `icon`, or `label` in the US | Shield icon only, or icon and text |
| `showCookieTable` | boolean | `true` | Per-cookie list in preferences |
| `policyUrl` | URL | from the key | Cookie policy link. Must start with `http:`, `https:`, `/` or `#`, or it is dropped |
| `privacyUrl` | URL | from the key | Privacy policy link. Same rule |
| `language` | two-letter code | `<html lang>` | The site's language. The visitor's browser language still comes first |

Where each combination of `layout` and `position` lands:

| | `bottom` | `top` | `bottom-left` | `bottom-right` |
| --- | --- | --- | --- | --- |
| `bar` | Bottom, centered | Top, centered | Same as `bottom` | Same as `bottom` |
| `box` (420px) | Bottom left | Top right | Bottom left | Bottom right |
| `modal` (560px, dims the page) | Center | Don't use: renders partly off-screen | Center | Center |

Leave `position` out when using `modal`. Prefer `bar` or `box` unless the owner asks for a modal, because it covers the page until a choice is made.

## Theme tokens

Set a token with `theme.<key>` in JSON or `--pryk-<name>` on `pryk-consent` in CSS. Values can be any CSS valid for that use, including `var()`, `hsl()` and `color-mix()`. Everything is sized in `em`, so `fontSize` scales the whole bar.

| JSON key | CSS token | Light default | Dark default | Take it from the site's |
| --- | --- | --- | --- | --- |
| `theme.bg` | `--pryk-bg` | `#fff` | `#17181a` | Card or surface background |
| `theme.fg` | `--pryk-fg` | `#17181a` | `#f3f3f1` | Body text and link color |
| `theme.muted` | `--pryk-muted` | `#5b5f66` | `#a6a9ad` | Secondary text. Used for the message |
| `theme.border` | `--pryk-border` | `#d9dbde` | `#35373b` | Border or divider, and ghost button outline |
| `theme.accent` | `--pryk-accent` | `#17181a` | `#f3f3f1` | Brand color. Focus ring, enabled switch |
| `theme.accentFg` | `--pryk-accent-fg` | `#fff` | `#17181a` | Color that reads on the brand color. Switch thumb |
| `theme.buttonBg` | `--pryk-button-bg` | `#17181a` | `#f3f3f1` | Primary button background. Styles Accept, Reject and Opt out |
| `theme.buttonFg` | `--pryk-button-fg` | `#fff` | `#17181a` | Primary button text |
| `theme.radius` | `--pryk-radius` | `14px` | same | Card radius. Banner and preferences |
| `theme.buttonRadius` | `--pryk-button-radius` | `9px` | same | Button radius |
| `theme.font` | `--pryk-font` | `system-ui,-apple-system,"Segoe UI",Roboto,sans-serif` | same | Full `font-family` stack, by name |
| `theme.fontSize` | `--pryk-font-size` | `14px` | same | Base size. Usually 14px to 16px |
| `theme.shadow` | `--pryk-shadow` | `0 12px 40px rgba(0,0,0,.18)` | `0 12px 40px rgba(0,0,0,.5)` | Elevated surface shadow |
| `theme.maxWidth` | `--pryk-max-width` | `1120px` | same | Content width. `bar` layout only |
| `theme.overlay` | `--pryk-overlay` | `rgba(10,11,13,.5)` | same | Dim behind `modal` and preferences |
| `theme.zIndex` | `--pryk-z-index` | `2147483600` | same | Lower only if the site needs something above the bar |
| `theme.scheme` | none | `auto` | | `auto`, `light` or `dark`. Which default set applies |

`--pryk-pos` is internal. Leave it alone.

## Dark mode

`theme.scheme` defaults to `auto`, so the bar follows the visitor's OS setting even when the site doesn't. Colors set in `theme` apply in both schemes, and they beat the site's CSS. Pick one row:

| The site | Do this |
| --- | --- |
| Light only | `"scheme": "light"`, plus your colors in `theme` |
| Dark only | `"scheme": "dark"`, plus your colors in `theme` |
| Has tokens on `:root` or `html` that swap with the theme | `theme` values using `var()`. Nothing else needed. [Recipe](#site-with-css-variables) |
| Follows the OS, with no shared tokens | `scheme` left as `auto`, colors in the site's CSS inside `@media (prefers-color-scheme: dark)` |
| Toggles with a class or attribute, with no shared tokens | `"scheme": "light"`, dark colors in the site's CSS under that selector. Leave colors out of `theme`. [Recipe](#class-based-dark-mode) |

## Traps

Each of these fails silently. All were confirmed in Chromium.

| Don't | What happens | Do |
| --- | --- | --- |
| `"font": "inherit"` | Falls back to Times New Roman | Give the stack by name: `"font": "Inter, system-ui, sans-serif"`, or `var(--font-sans, system-ui, sans-serif)` |
| `@font-face` in the `css` key | Not applied inside a shadow root | Load the font on the page. Page fonts can be used in the bar by name |
| `var(--x)` where `--x` is set on `body` or a wrapper | Resolves to nothing: transparent button | The host's parent is `<html>`. Only variables on `:root` or `html` reach it |
| `var(--brand)` with no fallback | Same if the variable is missing | `var(--brand, #0b57d0)` |
| `body.dark pryk-consent { ... }` | Never matches, since the host isn't inside `<body>` | `html.dark pryk-consent`, or `:root:has(body.dark) pryk-consent` |
| `html { --pryk-bg: #111 }` | Overridden, since the host declares its own tokens | Target `pryk-consent` |
| Token in the site's CSS while `theme` sets it too | `theme` wins | Set it in one place |
| Only `buttonBg` on a light-only site | OS dark visitors get a dark bar with your button | Set `scheme`, or the whole color set |
| Only `bg` | Text stays the default color: dark on dark | Set `bg`, `fg`, `muted`, `border`, `accent`, `accentFg`, `buttonBg` and `buttonFg` together |
| `.banner { left: 0 }` in the `css` key | Loses to `.banner[data-layout=bar]` | `pryk-consent::part(banner) { left: 0 }`, which beats all inner rules |
| `text.sv` or another language with no pack | Never used | Supported: `en`, `it`, `de`, `fr`, `es`, `pt`, `nl`, `ko`, `ja`. Others get English |
| `text["*"]` on a multilingual site | Forces that wording on every language | Set `text.en`, `text.de` and so on |

## Parts

Style from the site's CSS with `pryk-consent::part(name)`. These rules beat the bar's own. `::part()` accepts pseudo-classes like `:hover`, but no descendant selectors after it.

| Where | Parts |
| --- | --- |
| Banner | `banner`, `body`, `head`, `logo`, `title`, `message`, `links`, `link`, `policy-link`, `privacy-link`, `actions`, `overlay` |
| Buttons | `button` (all), `accept`, `reject`, `customize`, `opt-out`, `acknowledge`, `save`, `close` |
| Preferences | `prefs`, `prefs-head`, `prefs-title`, `prefs-intro`, `prefs-close`, `prefs-body`, `prefs-foot` |
| Categories | `category`, `category-necessary`, `category-functional`, `category-analytics`, `category-marketing`, `category-name`, `category-desc`, `always-on`, `switch`, `switch-track` |
| Cookie list | `cookie-details`, `cookie-summary`, `cookie-table` |
| Status | `reopener`, `gpc-notice`, `toast` |

The `accept` and `reject` parts appear in both the banner and preferences.

Which buttons show depends on the visitor's law, so style all of them:

| Visitor | Regime ids | Banner buttons | Preview |
| --- | --- | --- | --- |
| Opt-in | `eu`, `uk`, `ch`, `br`, `ca-qc`, `kr`, `row` | `reject`, `accept`, `customize` | `?pryk_geo=DE` |
| Italy | `it` | `close`, `reject`, `accept`, `customize` | `?pryk_geo=IT` |
| Opt-out | `us`, `ca`, `au`, `jp` | `opt-out`, `customize`, `acknowledge` | `?pryk_geo=US` |

`accept`, `reject` and `opt-out` are solid. `customize`, `acknowledge` and `save` are ghost style, using `fg` and `border`. With a Global Privacy Control signal, `opt-out` is removed, and in the US `gpc-notice` appears.

### Level 3: the `css` key

For elements with no part. These internal classes can change, so prefer tokens and parts:

`.root`, `.banner`, `.body`, `.head`, `.title`, `.message`, `.links`, `.actions`, `.btn`, `.btn.ghost`, `.close`, `.notice`, `.overlay`, `.prefs`, `.prefs-head`, `.prefs-body`, `.prefs-foot`, `.category`, `.cat-row`, `.cat-name`, `.cat-desc`, `.always`, `.switch`, `.track`, `.table-wrap`, `.reopener`, `.toast`, and the `details`, `summary`, `table`, `th` and `td` elements.

Only the `css` key can target these attributes:

- `.banner[data-layout=bar|box|modal]`
- `.banner[data-position=...]`
- `.banner[data-regime=eu|it|uk|ch|us|br|ca-qc|ca|au|kr|jp|row]`
- `.reopener[data-style=icon|label]`

## Text keys

Set them by language, for example `{"text":{"en":{"accept":"Allow all"}}}`. A language you don't set keeps its translated default, so override in every language the site uses. The bar picks the visitor's browser language if it has a pack, then the site's language, then English. `*` applies to all languages, and a specific language beats it. Values are plain text.

| Keys | Where |
| --- | --- |
| `title` | Banner heading |
| `message` | Banner text, opt-in visitors |
| `messageOptOut` | Banner text, opt-out visitors outside the US |
| `messageUsSale` | Banner text, US. Legal wording |
| `accept`, `reject`, `customize` | Banner and preferences buttons |
| `ok` | The `acknowledge` button |
| `optOut` | The `opt-out` button outside the US |
| `doNotSell` | The `opt-out` button and reopener in the US. Legal wording |
| `optOutDone` | Toast after opting out |
| `gpcHonored` | Global Privacy Control confirmation. Legal wording |
| `close` | Accessible label of the close buttons |
| `cookiePolicy`, `privacyPolicy` | Policy links |
| `prefsTitle`, `prefsIntro`, `save`, `alwaysOn` | Preferences |
| `necessaryName`, `necessaryDesc` | Category name and description |
| `functionalName`, `functionalDesc` | Category name and description |
| `analyticsName`, `analyticsDesc` | Category name and description |
| `marketingName`, `marketingDesc` | Category name and description |
| `showCookies` | Cookie list toggle |
| `thName`, `thProvider`, `thPurpose`, `thDuration`, `thDestination` | Cookie list columns |
| `reopen` | Reopen button label outside the US |
| `blockedContent`, `blockedAction` | Placeholder on a blocked embed |

## Recipes

### Light-only site

```json
{
  "layout": "bar",
  "policyUrl": "/cookies",
  "theme": {
    "scheme": "light",
    "bg": "#ffffff",
    "fg": "#1a1a2e",
    "muted": "#55556a",
    "border": "#e3e3ec",
    "accent": "#4f46e5",
    "accentFg": "#ffffff",
    "buttonBg": "#4f46e5",
    "buttonFg": "#ffffff",
    "radius": "12px",
    "buttonRadius": "8px",
    "font": "Inter, system-ui, sans-serif",
    "fontSize": "15px"
  }
}
```

### Site with CSS variables

The best case: the bar follows every theme change on its own. The variables must be on `:root` or `html`. If they hold raw channels, wrap them, as in `hsl(var(--primary))`.

```json
{
  "theme": {
    "scheme": "light",
    "bg": "var(--color-surface, #fff)",
    "fg": "var(--color-text, #17181a)",
    "muted": "var(--color-text-muted, #5b5f66)",
    "border": "var(--color-border, #d9dbde)",
    "accent": "var(--color-primary, #17181a)",
    "accentFg": "var(--color-on-primary, #fff)",
    "buttonBg": "var(--color-primary, #17181a)",
    "buttonFg": "var(--color-on-primary, #fff)",
    "radius": "var(--radius-lg, 14px)",
    "buttonRadius": "var(--radius-md, 9px)",
    "font": "var(--font-sans, system-ui, sans-serif)"
  }
}
```

### Class-based dark mode

Set `{"theme":{"scheme":"light"}}` in JSON and put the colors in the site's CSS:

```css
pryk-consent {
  --pryk-bg: #ffffff; --pryk-fg: #1a1a2e; --pryk-muted: #55556a; --pryk-border: #e3e3ec;
  --pryk-accent: #4f46e5; --pryk-accent-fg: #ffffff;
  --pryk-button-bg: #4f46e5; --pryk-button-fg: #ffffff;
}
html.dark pryk-consent {
  --pryk-bg: #14141f; --pryk-fg: #eeeef5; --pryk-muted: #a5a5b8; --pryk-border: #2c2c3d;
  --pryk-accent: #a5b4fc; --pryk-accent-fg: #14141f;
  --pryk-button-bg: #a5b4fc; --pryk-button-fg: #14141f;
}
```

For a class on `<body>`, use `:root:has(body.dark) pryk-consent`.

### Clearing a chat widget or mobile nav

The bar and reopener sit 16px from the bottom. Move them above anything fixed there, or move the reopener to the other corner with `{"reopener":{"position":"bottom-right"}}`.

```css
pryk-consent::part(banner), pryk-consent::part(reopener) { bottom: 88px; }
```

### Outlined or uppercase buttons

Always change Accept and Reject together.

```css
pryk-consent::part(accept), pryk-consent::part(reject) {
  background: transparent;
  color: #1a1a2e;
  border-color: #1a1a2e;
}
pryk-consent::part(button) { text-transform: uppercase; letter-spacing: .04em; }
```

### Wording for a brand voice

```json
{
  "text": {
    "en": { "title": "Cookies at Acme", "accept": "Allow all", "reject": "Refuse all" },
    "de": { "title": "Cookies bei Acme", "accept": "Alle erlauben", "reject": "Alle ablehnen" }
  }
}
```

### Elements with no part

The switch thumb, table header and an Italy-only rule:

```json
{
  "css": ".track::after { box-shadow: none } th { text-transform: uppercase } .banner[data-regime=it] .title { font-style: italic }"
}
```

### Footer link

Any element with `data-pryk-open` opens preferences. It is a good addition on any site, and required if you set `reopener.show` to `false`.

```html
<a href="#" data-pryk-open>Privacy choices</a>
```

### Blocked embeds

An embed held back until consent is replaced by a `[data-pryk-placeholder]` element in the page itself, not the shadow root. Its look is set inline, so restyle it from the site's CSS with `!important`.

```css
[data-pryk-placeholder] { background: #f4f4f8 !important; color: #1a1a2e !important; border-radius: 12px; }
[data-pryk-placeholder] button { font: inherit; }
```

## Verify

### Preview without a browser session

The explorer takes its whole state from the URL, and nothing is sent to a server. Build a link for yourself or the owner:

```js
const state = {
  json: JSON.stringify(config), // your config object
  css: siteCss,                 // the site CSS you wrote, or ''
  regime: 'eu',                 // or it, uk, ch, us, br, ca-qc, ca, au, kr, jp, row
  lang: 'en',
  view: 'banner',               // or prefs, reopener
  device: 'desktop',            // or mobile
  page: 'light',                // or dark
  gpc: false,
};
const url = 'https://pryk.click/explorer#s=' + Buffer.from(JSON.stringify(state)).toString('base64');
```

If you can drive a browser, open that link and read the checks: each `.chk` element has `data-state="ok"` or `"warn"`, and its text says what failed. It measures Reject against Accept, button and message contrast, and the policy link. The explorer can't load the site's fonts or variables, so it may show a fallback for those.

### On the real site

The key only runs on its licensed domain, its subdomains and the hosts in `extra_hosts`. Anywhere else, including `localhost`, there is no bar, `window.Pryk` is undefined and the console says `[pryk] This key is licensed for ...`. To test there, ask the Pryk operator to add the host, or use the explorer.

- `?pryk_geo=IT` previews another region. Try `DE`, `IT`, `US` and `JP`.
- `Pryk.reset()` clears the stored choice and reloads, so the banner shows again.
- `Pryk.showBanner()` and `Pryk.open()` show the banner and preferences.
- Automation tools can pierce the open shadow root, for example `pryk-consent [part~="accept"]` in Playwright.

Run this in the console to compare Accept and Reject:

```js
const root = document.querySelector('pryk-consent').shadowRoot;
const read = (name) => {
  const el = root.querySelector(`[part~="${name}"]`);
  const s = getComputedStyle(el);
  return { bg: s.backgroundColor, color: s.color, fontSize: s.fontSize, height: el.offsetHeight, opacity: s.opacity };
};
console.table({ accept: read('accept'), reject: read('reject') });
```

Check all of these:

- [ ] Accept and Reject match, in the banner and in preferences.
- [ ] Text is readable in every version, including ghost buttons and the US message.
- [ ] Nothing overlaps at 375px wide, including the long US button label.
- [ ] Correct in light and dark, with the OS set to each.
- [ ] Preferences, the switches and the focus ring (Tab through) match the design.
- [ ] After a choice, the reopen button is visible and clear of other fixed elements.
- [ ] The font is the site's font, not Times New Roman or a system fallback.

## What the site cannot change

The page can only change looks and wording. Only the Pryk operator can change these, in the key's `config_json` or the customer sheet, and a change takes a Pryk deploy to publish:

- Regional rules: `regimes`, `forceRegime`, `ukStatisticalException`.
- Blocking and reloads: `autoBlock`, `reloadOnWithdraw`.
- Google Consent Mode: `gcm`.
- The cookie list, blocked hosts and allowed hosts: `extra_hosts`.

Tell the owner when a request needs one of these.

## Related API

For wiring the bar into a site, not for styling it.

- `Pryk.open()`, `Pryk.showBanner()`, `Pryk.getConsent()`, `Pryk.onConsent(fn)`, `Pryk.acceptAll()`, `Pryk.rejectAll()`, `Pryk.reset()`.
- `pryk:consent` event on `window`, and `pryk_consent` in `dataLayer`.
- Manual tagging: `<script type="text/plain" data-pryk="analytics">` and `<iframe data-pryk="marketing" data-src="...">`.
