The classic z-index bug: you set something to z-index: 999, something else to 9999, then a third thing breaks and you add 99999. It works, nobody knows why, and six months later nobody touches it.

The fix is to stop using arbitrary numbers and define a named layer scale instead.

CSS custom properties

:root {
  --z-index-max: 9999;
  --z-index-min: -9999;
  --z-index-overlays: 1900;
  --z-index-modal: 1800;
  --z-index-header: 1700;
  --z-index-drawer: 1600;
  --z-index-backdrop: 1500;
  --z-index-fixed: 1400;
  --z-index-sticky: 1300;
  --z-index-dropdown: 1200;
  --z-index-floating: 1100;
  --z-index-footer: 1000;
  --z-index-0: 0;
  --z-index-1: 10;
  --z-index-2: 20;
  --z-index-3: 30;
  --z-index-4: 40;
  --z-index-5: 50;
}

Now every component uses a name, not a number:

.modal { z-index: var(--z-index-modal); }
.header { z-index: var(--z-index-header); }
.tooltip { z-index: var(--z-index-tooltip); }

When a modal needs to appear above a sticky header, you don’t guess — the answer is already in the scale.

Tailwind

Extend the theme in tailwind.config.ts:

export default {
  theme: {
    extend: {
      zIndex: {
        'max': '9999',
        'min': '-9999',
        'overlays': '1900',
        'modal': '1800',
        'header': '1700',
        'drawer': '1600',
        'backdrop': '1500',
        'fixed': '1400',
        'sticky': '1300',
        'dropdown': '1200',
        'floating': '1100',
        'footer': '1000',
      },
    },
  },
}

Then use z-modal, z-header, z-tooltip as utility classes.

SCSS

If you’re on Sass, a map works well:

$z-index: (
  'max':      9999,
  'min':      -9999,
  'modal':    1800,
  'header':   1700,
  'drawer':   1600,
  'backdrop': 1500,
  'dropdown': 1200,
  'floating': 1100,
  'footer':   1000,
);

@function z($layer) {
  @return map.get($z-index, $layer);
}
.modal { z-index: z('modal'); }

Why it’s worth doing

All stacking decisions live in one place. When a conflict comes up, you don’t grep through the codebase looking for who wrote z-index: 9999 — you open the scale and reason about it in a minute.

Code review gets easier too. A PR with z-index: var(--z-index-modal) is self-explanatory. A PR with z-index: 10000 opens a conversation about why, and that conversation will happen at 5pm on a Friday.

The gaps matter more than they look. 100 units between each layer means you can insert a new one anywhere without touching existing values. Need something between dropdown and floating? 1150 fits and nothing else changes.

The numeric scale (--z-index-1 through --z-index-5, stepping by 10) is for local stacking within a component — a badge over an avatar, an overlay over a card. Small adjustments that don’t need a name. --z-index-min covers the other end: background decorations and pseudo-elements that need to sit behind everything else.