@codecademy/gamut
72.5.072.5.1-alpha.ba3184.0
agent-tools/skills/gamut-zindex/SKILL.md+
agent-tools/skills/gamut-zindex/SKILL.mdNew file+134
Index: package/agent-tools/skills/gamut-zindex/SKILL.md
===================================================================
--- package/agent-tools/skills/gamut-zindex/SKILL.md
+++ package/agent-tools/skills/gamut-zindex/SKILL.md
@@ -0,0 +1,134 @@
+---
+name: gamut-zindex
+description: Use this skill when setting a `zIndex` on a Gamut component or styled component, choosing a stacking layer for an overlay/portal/sticky element, typing a `zIndex` prop with `ZIndexType`, or fixing a raw numeric z-index flagged by `gamut/no-raw-z-index` — not for other system props (see gamut-system-props) or general css()/variant()/states() authoring (see gamut-style-utilities).
+---
+
+# Gamut Z-Index
+
+Source: `@codecademy/gamut-styles` — `packages/gamut-styles/src/variables/zIndexes.ts` (scale + `ZIndexType`), `packages/gamut-styles/src/variance/config.ts` (`zIndex` system prop config). Lint rule: `packages/eslint-plugin-gamut/src/no-raw-z-index.ts`.
+
+See also: [`gamut-system-props`](../gamut-system-props/SKILL.md) (`system.positioning`, the rest of the `zIndex` prop group). [`gamut-style-utilities`](../gamut-style-utilities/SKILL.md) (`css()`, `variant()`, `states()`). Storybook: [Foundations / Z-Index](https://gamut.codecademy.com/?path=/docs-foundations-z-index--page), [Meta / ESLint rules](https://gamut.codecademy.com/?path=/docs-meta-eslint-rules--page).
+
+## Overview
+
+Gamut coordinates stacking order through one semantic scale, `zIndexes`, exported from `@codecademy/gamut-styles`. Every `zIndex` in Gamut should reference a token from this scale rather than a magic number — a repo-wide eslint rule, `gamut/no-raw-z-index`, enforces it.
+
+```tsx
+import { zIndexes } from '@codecademy/gamut-styles';
+
+<Box zIndex="modal">…</Box> // token name — preferred
+<Box zIndex={zIndexes.modal}>…</Box> // equivalent numeric value
+```
+
+## The scale
+
+Tokens are spaced by 100, low to high, leaving room for in-between escape-hatch numbers:
+
+| Token | Value | Use for |
+| ------------ | ----- | ------------------------------------------------------------------------------------------------------- |
+| `underlay` | -100 | Decorative layer behind content (underlines, backdrops, shadows) |
+| `base` | 0 | Ground layer — local stacking context without lifting above siblings |
+| `foreground` | 100 | Raised in-flow layer above `underlay`/siblings, below all portal overlays; also sticky headers |
+| `floating` | 200 | Portal floor — `BodyPortal` default; persistent floating page furniture (AI chat launcher, help bubble) |
+| `appBar` | 300 | Global app header/nav bar (aliased by the legacy `elements.headerZ` constant) |
+| `flyout` | 400 | Portaled side panel (`Flyout` = `Drawer` inside `Overlay`) |
+| `modal` | 500 | `Overlay`, `Modal`, `Dialog` (share one portal primitive) |
+| `popover` | 600 | Portal-mode `Popover` and the portaled `SelectDropdown` menu — above modal |
+| `topmost` | 700 | Top-most transient overlays: floating tooltips, toasts/notifications — nothing in Gamut sits above this |
+
+**Talk to web platform before adding a new token to the scale.** Third-party widgets (injected marketing/chat scripts) set their own z-index and are outside Gamut's control — `topmost` is the ceiling for anything Gamut owns.
+
+## Usage
+
+### `zIndex` system prop
+
+Available on `Box`/`FlexBox`/`GridBox` and any styled component composing `system.positioning`. Accepts, in order of preference:
+
+1. A token name: `zIndex="popover"`
+2. The scale's numeric value: `zIndex={zIndexes.popover}`
+3. Arithmetic on a token, for a deliberate offset within a layer: `zIndex={zIndexes.foreground - 2}`
+4. A raw number as an escape hatch for a genuine one-off: `zIndex={550}` — leave a comment justifying it
+
+```tsx
+<Box position="absolute" zIndex="popover">
+ …
+</Box>
+```
+
+### In `css()` / `variant()` / `states()`
+
+Token names resolve the same way inside these — including in nested pseudo-selector objects — because they share the same scale-aware property config as the `zIndex` prop:
+
+```tsx
+import { css, variant } from '@codecademy/gamut-styles';
+
+const styles = css({
+ position: 'absolute',
+ zIndex: 'popover',
+ '&::before': {
+ content: '""',
+ zIndex: 'underlay',
+ },
+});
+
+const cardVariants = variant({
+ base: { zIndex: 'base' },
+ variants: { raised: { zIndex: 'foreground' } },
+});
+```
+
+### Typing a component's `zIndex` prop
+
+Use `ZIndexType` (a token name, a raw number, or a CSS global like `'inherit'`) instead of `number` so consumers can pass a token:
+
+```tsx
+import { ZIndexType } from '@codecademy/gamut-styles';
+
+export interface OverlayProps {
+ zIndex?: ZIndexType;
+}
+```
+
+### Portal / overlay component defaults
+
+Several Gamut components already default their `zIndex` prop to a scale token — override only for a deliberate custom stacking order:
+
+| Component | Default | Notes |
+| ------------------------------- | ------------ | ---------------------------------------------------------- |
+| `BodyPortal` | `"floating"` | Base portal primitive several others build on |
+| `Overlay` (→ `Modal`, `Dialog`) | `"modal"` | |
+| `Flyout` | `"flyout"` | Passes `zIndex="flyout"` to its internal `Overlay` |
+| `PopoverContainer`, `Popover` | `"popover"` | Portals via `<BodyPortal zIndex="popover">` |
+| `SelectDropdown` menu | `"popover"` | `react-select`'s `menuPortal`, portaled to `document.body` |
+| `Toaster` | `"topmost"` | |
+
+## The `gamut/no-raw-z-index` eslint rule
+
+**Level:** `error` (repo-wide, via root `.eslintrc.js`)
+
+Flags a raw numeric literal in a `zIndex` JSX prop or a `zIndex`/`'z-index'` style-object key. Token names and arithmetic on a token are allowed; a variable is never flagged (the rule can't know what it resolves to).
+
+```tsx
+// ❌ Flagged
+<Box zIndex={2} />;
+const styles = css({ zIndex: 100 });
+
+// ✅ OK
+<Box zIndex="foreground" />;
+const styles = css({ zIndex: 'popover' });
+<Box zIndex={zIndexes.foreground + 1} />; // deliberate offset, arithmetic on a token
+```
+
+For a genuine one-off that doesn't map to a token, disable the rule inline with a comment justifying the choice:
+
+```tsx
+// eslint-disable-next-line gamut/no-raw-z-index -- must sit one layer below the legacy FCN nav (12)
+<Box zIndex={11} />
+```
+
+## Common mistakes to avoid
+
+- Don't hardcode a number when a token already names the intent (`zIndex={500}` → `zIndex="modal"`).
+- Don't reference `elements.headerZ` in new code — it's a legacy alias for `zIndexes.appBar`; use the token directly.
+- Don't type a new `zIndex` prop as `number` — use `ZIndexType` so callers can pass a token name.
+- Don't add a new token to the scale without checking with web platform first; reach for the escape hatch (a raw number, or arithmetic on a token) for a one-off instead.