Why Your Bootstrap 5 Modal Is Not Displaying: Debugging Tailwind CSS Compile-Time Generation
If your Bootstrap 5 modal fails to appear when the button is clicked, and you're using Tailwind CSS for styling, the issue likely stems from missing modal utilities in Tailwind's compile-time CSS generation pipeline.
When migrating from Bootstrap 5 to Tailwind CSS, developers often expect modal components to work immediately after including JavaScript. However, Tailwind operates as a compile-time CSS generator, not a runtime JavaScript library like Bootstrap. If your Bootstrap 5 modal is not displaying when clicked, the root cause often traces back to Tailwind's internal pipeline—from AST parsing in src/index.ts through candidate compilation in src/compile.ts—where missing utilities or unregistered variants prevent the modal CSS from ever reaching your browser.
How Tailwind's Compile-Time Pipeline Affects Modal Display
Tailwind CSS walks your source files, registers variants, utilities, and theme values, then generates the output CSS that your page finally receives. If the compiled CSS never contains the required .modal-* rules, the browser cannot display the modal, regardless of any JavaScript you attach to the button.
Below is a high-level walk-through of Tailwind's core pipeline, showing where the CSS for a component such as a modal would be generated (or omitted) and how you can verify each step.
| Step | What Tailwind Does | Relevant Source |
|---|---|---|
1. Parse the input CSS – The compile function parses the raw stylesheet into an AST. |
let ast = CSS.parse(css, { from: opts.from }) |
[src/index.ts](https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/src/index.ts#L29-L31) |
2. Build the design system – Constructs a DesignSystem object that holds the theme, utilities, and variant registries. |
let designSystem = buildDesignSystem(theme, utilitiesNode?.src) |
[src/index.ts](https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/src/index.ts#L98-L100) |
3. Register built-in utilities – createUtilities registers every core utility like bg-…, flex, and the @utility-based modal helpers (if you added any). |
let utilities = createUtilities(theme) |
[src/design-system.ts](https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/src/design-system.ts#L74-L75) |
4. Register built-in variants – createVariants registers pseudo-class variants (hover, focus, …) and compound variants (group, peer, aria). |
let variants = createVariants(theme) |
[src/design-system.ts](https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/src/design-system.ts#L75-L76) |
5. Parse each candidate – When you later call compileCandidates, Tailwind tokenises class names (modal-open, modal-enter, …) into candidates. If a candidate does not match any registered utility, it is marked invalid. |
parseCandidate(candidate, designSystem) inside compiledAstNodes |
[src/design-system.ts](https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/src/design-system.ts#L78-L80) |
6. Generate the CSS AST for a candidate – compileAstNodes creates the actual rule nodes for a valid candidate, applying any needed variants. |
compileAstNodes(candidate, designSystem, flags) |
[src/design-system.ts](https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/src/design-system.ts#L84-L86) |
7. Apply polyfills & optimisations – The final AST is run through optimizeAst, which removes dead rules and merges selectors. If the modal utility never made it past step 5, there will be nothing to optimise. |
optimizeAst(ast, designSystem, opts.polyfills) |
[src/index.ts](https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/src/index.ts#L76-L78) |
8. Emit final CSS – toCss serialises the AST back to a stylesheet that the browser loads. |
return toCss(newAst, !!opts.from) |
[src/index.ts](https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/src/index.ts#L43-L45) |
Why a Bootstrap 5 Modal Might Not Appear in Tailwind Projects
When integrating Bootstrap 5's modal JavaScript with Tailwind CSS styling, or when recreating modal behavior using Tailwind utilities, several compile-time failures can prevent the modal from displaying:
-
Utility Not Defined – Tailwind's core does not ship a
modalutility. If you are trying to use something likemodal-openwithout first defining it (via@utilityor a plugin), step 5 will treat the class as invalid, so no CSS is emitted. -
Custom Variant Mis-registered – If you created a custom variant (e.g.,
modal) but missed the registration call invariants.ts, the compiler will not apply the selector, resulting again in missing CSS. See the registration flow invariants.tswhere each static variant is added viavariants.static. | [src/variants.ts](https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/src/variants.ts#L54-L66) | -
Theme Value Missing – Many modal utilities depend on CSS custom properties (e.g.,
--tw-modal-bg). If those variables are never defined in an@themeblock,Theme.addwill never store them, and the generated rule will reference an undefined var, leading to a no-op in the browser. | [src/theme.ts](https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/src/theme.ts#L60-L86) | -
@tailwind utilitiesNot Inserted – If the source file never contains@tailwind utilities, the compiler never replaces that placeholder with the generated utilities (step 7). Verify that the placeholder exists and isn't stripped by a prior plugin. | [src/index.ts](https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/src/index.ts#L73-L80) | -
Build Caching – Tailwind caches compiled candidates. If you added a new modal utility after an initial build, you must trigger a rebuild (e.g., by restarting the dev server) so the cache invalidates and picks up the new candidate. | [
src/index.ts](https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/src/index.ts#L59-L66) |
Quick Debug Checklist for Missing Modal CSS
| Checklist Item | How to Verify |
|---|---|
| Utility Exists | Search for modal in src/utilities.ts. If missing, add it with @utility or a plugin. |
| Variant Registration | Look for variants.static('modal', …) or a custom-variant registration in src/variants.ts. |
| Theme Variables | Inspect any @theme blocks or the generated :root output (view the compiled CSS) to ensure --tw-modal-* variables are present. |
@tailwind utilities Placeholder |
Confirm your entry CSS contains @tailwind utilities and that it is not removed by a preceding plugin. |
| Cache Invalidation | Restart the Tailwind dev server or delete node_modules/.cache/tailwindcss (if present). |
Code Examples: Implementing Modal Utilities in Tailwind
Defining a Simple Modal Utility
If you are migrating from Bootstrap 5 to Tailwind and need to recreate modal functionality, define the utility in your CSS:
/* src/styles.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Define a custom modal utility */
@utility modal {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0,0,0,0.5);
}
/* Define an open-state variant */
@custom-variant modal-open & {
opacity: 1;
pointer-events: auto;
}
The @utility modal block registers the core modal container. The @custom-variant modal-open creates a variant that toggles visibility. Tailwind will now generate classes .modal and .modal-open\:& that you can toggle with JavaScript.
Using the Modal in HTML
<button id="openBtn" class="px-4 py-2 bg-blue-600 text-white">Open modal</button>
<div class="modal hidden modal-open:&">
<div class="bg-white p-6 rounded max-w-sm">
<h2 class="text-lg font-bold mb-4">Modal title</h2>
<p class="mb-4">Modal content goes here.</p>
<button id="closeBtn" class="px-3 py-1 bg-gray-200 rounded">Close</button>
</div>
</div>
<script>
const modal = document.querySelector('.modal')
document.getElementById('openBtn').addEventListener('click', () => {
modal.classList.remove('hidden')
modal.classList.add('modal-open:&')
})
document.getElementById('closeBtn').addEventListener('click', () => {
modal.classList.add('hidden')
modal.classList.remove('modal-open:&')
})
</script>
The modal-open:& variant is applied only when the class is present, toggling the visibility defined in the custom utility.
Building Tailwind with a Custom Plugin (JS)
If you prefer to keep modal logic inside a JS plugin instead of @utility syntax:
// tailwind-plugin.js
const plugin = require('tailwindcss/plugin')
module.exports = plugin(function({ addUtilities, addVariant }) {
const modal = {
'.modal': {
position: 'fixed',
inset: '0',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
'background-color': 'rgba(0,0,0,0.5)',
},
}
addUtilities(modal)
// Variant that adds the selector "&" after the class (e.g. .modal-open\:&)
addVariant('modal-open', ({ modifySelectors, separator }) => {
modifySelectors(({ className }) => {
return `.modal-open${separator}${className}`
})
})
})
module.exports = {
content: ['./src/**/*.html'],
plugins: [require('./tailwind-plugin')],
}
Now you can use <div class="modal modal-open:opacity-100">…</div> and control the modal via the modal-open: prefix.
Key Files in Tailwind's Compilation Pipeline
| File | Role | Link |
|---|---|---|
src/index.ts |
Entry point; orchestrates parsing, design-system construction, and final build. | View |
src/design-system.ts |
Creates the central DesignSystem object (theme + utilities + variants). |
View |
src/theme.ts |
Manages CSS custom properties, prefixing, and variable resolution. | View |
src/variants.ts |
Registers static, functional, and compound variants; handles @custom-variant. |
View |
src/utilities.ts |
Generates the core utility map (e.g., bg-, flex-). |
View |
src/candidate.ts |
Parses class-name candidates into utility + variant tokens. | View |
src/compile.ts |
Turns parsed candidates into AST nodes (compileCandidates). |
View |
src/ast.ts |
Low-level AST node constructors (rule, atRule, decl, …) and serializer. |
View |
src/compat/* |
Back-compatibility layer for legacy Tailwind features. | View |
These files together form the compile-time engine that decides whether a class like modal-open ends up as real CSS. If any of the steps above fail—missing utility, unregistered variant, or absent @tailwind utilities placeholder—the modal's CSS will never be generated, and the button click will appear to do nothing. Fixing the issue means ensuring the utility/variant is correctly defined, the placeholder exists, and the Tailwind build is refreshed.
Summary
- Tailwind CSS generates CSS at compile-time, not runtime. If your Bootstrap 5 modal is not displaying, the required CSS may never have been emitted.
- Missing utilities in
src/utilities.tsor undefined@utilityblocks prevent modal classes from being generated. - Unregistered variants in
src/variants.tscause state-based classes (likemodal-open) to be stripped duringcompileCandidates. - Absent placeholders like
@tailwind utilitiesin your entry CSS prevent the compiler from injecting generated rules. - Build caching in
src/index.tscan hide newly added modal utilities until you restart the dev server or clear the cache.
Frequently Asked Questions
Why does my modal work in development but not production?
If your Bootstrap 5 modal displays in development but fails in production, content scanning is likely the culprit. Tailwind scans your content files for class names to generate. If your production build uses a different file glob or tree-shaking removes the modal markup, Tailwind never sees the modal or modal-open classes during parseCandidate in src/candidate.ts, so no CSS is generated. Ensure your tailwind.config.js content array includes all files containing modal markup.
Can I use Bootstrap's modal JavaScript with Tailwind CSS styling?
Yes, but you must define the modal utilities that Bootstrap's JavaScript expects. Bootstrap's modal JS toggles classes like modal and show, but Tailwind doesn't include these by default. Use @utility in your CSS or addUtilities in a plugin to define the .modal fixed positioning and .show visibility rules. Without these definitions, createUtilities in src/design-system.ts won't register the classes, and compileAstNodes will skip them.
How do I debug which utilities Tailwind is generating?
Run your build with the Tailwind CLI and inspect the output CSS file. Search for your modal class names (e.g., .modal or .modal-open). If they are missing, the issue occurs during candidate parsing in src/candidate.ts or AST compilation in src/compile.ts. Check that your modal classes appear in your HTML/JSX files that are included in the content configuration, and verify you have restarted the dev server to clear the cache managed in src/index.ts.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →