diff --git a/.cspell.json b/.cspell.json index a2df3d0adabe..19b89343cc8f 100644 --- a/.cspell.json +++ b/.cspell.json @@ -59,6 +59,8 @@ "DOMCONTENT", "Dripicons", "dropend", + "dropleft", + "dropright", "dropstart", "dropup", "dgst", @@ -108,6 +110,7 @@ "lightningcss", "linkinator", "llms", + "llmstxt", "longdesc", "Lowercased", "markdownify", @@ -186,6 +189,7 @@ "Unported", "unstylable", "unstyled", + "unthemed", "Uppercased", "urlize", "urlquery", diff --git a/eslint.config.js b/eslint.config.js index 16a4016e83a6..38002a2a42a2 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -418,6 +418,23 @@ const eslintConfig = [ rules: { 'unicorn/prefer-node-protocol': 'off' } + }, + + // Markdown TypeScript code blocks — parsed for syntax only, never type-checked + { + files: ['**/*.md/*.ts'], + languageOptions: { + parser: tseslint.parser + } + }, + + // Agent skill code blocks are illustrative docs (mirroring the docs guides), + // so allow side-effect imports like `import './styles.scss'` in bundler setups. + { + files: ['skills/**/*.md/*.js', 'skills/**/*.md/*.mjs'], + rules: { + 'import/no-unassigned-import': 'off' + } } ] diff --git a/site/data/sidebar.yml b/site/data/sidebar.yml index 7c09ea8b90e6..31f68ea8fc44 100644 --- a/site/data/sidebar.yml +++ b/site/data/sidebar.yml @@ -7,6 +7,7 @@ icon_color: indigo pages: - title: Install + - title: AI - title: Approach - title: CSS variables - title: JavaScript diff --git a/site/src/components/shortcodes/SkillsTable.astro b/site/src/components/shortcodes/SkillsTable.astro new file mode 100644 index 000000000000..dbe0d54b5747 --- /dev/null +++ b/site/src/components/shortcodes/SkillsTable.astro @@ -0,0 +1,57 @@ +--- +import { getConfig } from '@libs/config' +import { docsPages, getDocsPageSlug } from '@libs/content' +import { getVersionedDocsPath } from '@libs/path' +import { getSkills } from '@libs/skills' + +// Render the agent skills table from each `SKILL.md` frontmatter so the docs +// stay in sync with the skill files. Markup mirrors the `BsTable` shortcode. +const skills = getSkills() +const { repo } = getConfig() + +function getGuide(guide: string | undefined) { + if (!guide) { + return undefined + } + + const slug = guide.replace(/^\//, '') + const page = docsPages.find((docsPage) => getDocsPageSlug(docsPage.id) === slug) + + if (!page) { + return undefined + } + + return { href: getVersionedDocsPath(slug), label: page.data.title } +} +--- + +
+ + + + + + + + + + { + skills.map((skill) => { + const guide = getGuide(skill.guide) + + return ( + + + + + + ) + }) + } + +
SkillDescriptionGuide
+ + {skill.name} + + {skill.description}{guide ? {guide.label} : '—'}
+
diff --git a/site/src/components/shortcodes/index.ts b/site/src/components/shortcodes/index.ts index 5a9674f619ce..1bbf86b949c0 100644 --- a/site/src/components/shortcodes/index.ts +++ b/site/src/components/shortcodes/index.ts @@ -18,6 +18,7 @@ export { default as Placeholder } from './Placeholder.astro' export { default as ResizableExample } from './ResizableExample.astro' export { default as ScrollspyDebug } from './ScrollspyDebug.astro' export { default as ScssDocs } from './ScssDocs.astro' +export { default as SkillsTable } from './SkillsTable.astro' export { default as SpacingNotation } from './SpacingNotation.astro' export { default as SpacingResponsive } from './SpacingResponsive.astro' export { default as Swatch } from './Swatch.astro' diff --git a/site/src/content/docs/getting-started/ai.mdx b/site/src/content/docs/getting-started/ai.mdx new file mode 100644 index 000000000000..137f7e6135b0 --- /dev/null +++ b/site/src/content/docs/getting-started/ai.mdx @@ -0,0 +1,26 @@ +--- +title: Using Bootstrap with AI +description: Learn about Bootstrap’s machine-readable docs and skills files for working with LLMs and agentic developer tools. +toc: true +--- + +Bootstrap ships resources built for large language models and AI-powered coding tools: machine-readable documentation you can feed to an LLM, and a set of agent skills that walk agents through common Bootstrap tasks step by step. + +## LLM-ready docs + +Our docs are available in a machine-readable format, following the [llms.txt](https://llmstxt.org/) convention. + + +| File | Description | Link | +| --- | --- | --- | +| `llms.txt` | A curated index of every documentation page with links and descriptions. | [/llms.txt](/llms.txt) | +| `llms-full.txt` | The full text of the documentation concatenated into a single file for direct ingestion. | [/llms-full.txt](/llms-full.txt) | + + +## Skills + +Bootstrap ships [agent skills](https://github.com/twbs/bootstrap/tree/main/skills) in the `skills/` directory of our repository. Each skill is a `SKILL.md` file—a focused, step-by-step playbook that an agent can follow to complete a specific Bootstrap task, from migrating a project or setting up a new build to authoring a component and customizing our color system. + +To use one, point your agent at the relevant `SKILL.md` file (or copy it into your project) and describe what you want to do. The skill’s `description` field tells the agent when it applies, and its steps keep the agent aligned with our official guidance. Always review an agent’s changes against the matching documentation. + + diff --git a/site/src/content/docs/getting-started/install.mdx b/site/src/content/docs/getting-started/install.mdx index fcf778353250..b8bae14a1131 100644 --- a/site/src/content/docs/getting-started/install.mdx +++ b/site/src/content/docs/getting-started/install.mdx @@ -148,3 +148,7 @@ You can also do the old fashioned thing and download Bootstrap manually. Choose Should you require our full set of [build tools]([[docsref:/guides/contribute#tooling-setup]]), they are included for developing Bootstrap and its docs, but they’re likely unsuitable for your own purposes. + +## For AI tools & LLMs + +Bootstrap’s documentation is available in a machine-readable format, and we ship agent skills for AI-powered coding tools. See [AI]([[docsref:/getting-started/ai]]) for our `llms.txt` files and the full list of skills. diff --git a/site/src/content/docs/guides/npm.mdx b/site/src/content/docs/guides/npm.mdx index 12b81d9cec02..535e9651ef87 100644 --- a/site/src/content/docs/guides/npm.mdx +++ b/site/src/content/docs/guides/npm.mdx @@ -11,6 +11,10 @@ thumbnail: guides/bootstrap-npm@2x.png **Want to skip to the end?** Download the source code and working demo for this guide from the [twbs/examples repository](https://github.com/twbs/examples/tree/sass-js/). You can also [open the example in StackBlitz](https://stackblitz.com/github/twbs/examples/tree/main/sass-js?file=index.html). + +**Using an AI coding agent?** Point it at our [`bootstrap-npm` skill](https://github.com/twbs/bootstrap/tree/main/skills/bootstrap-npm) to automate this setup. See [AI]([[docsref:/getting-started/ai]]) for all available skills. + + ## What is npm? [npm](https://www.npmjs.com/) is the default package manager for Node.js. In this guide, we use npm to install Bootstrap and its dependencies, then compile Sass to CSS using command-line tools—no bundler required. diff --git a/site/src/content/docs/guides/parcel.mdx b/site/src/content/docs/guides/parcel.mdx index 672590e526ca..a9dbc4c33489 100644 --- a/site/src/content/docs/guides/parcel.mdx +++ b/site/src/content/docs/guides/parcel.mdx @@ -11,6 +11,10 @@ thumbnail: guides/bootstrap-parcel@2x.png **Want to skip to the end?** Download the source code and working demo for this guide from the [twbs/examples repository](https://github.com/twbs/examples/tree/main/parcel). You can also [open the example in StackBlitz](https://stackblitz.com/github/twbs/examples/tree/main/parcel?file=index.html) but not run it because Parcel isn’t currently supported there. + +**Using an AI coding agent?** Point it at our [`bootstrap-parcel` skill](https://github.com/twbs/bootstrap/tree/main/skills/bootstrap-parcel) to automate this setup. See [AI]([[docsref:/getting-started/ai]]) for all available skills. + + ## What is Parcel? [Parcel](https://parceljs.org/) is a web application bundler designed to simplify the development process with a zero-configuration setup out of the box. It offers features found in more advanced bundlers while focusing on ease of use, making it ideal for developers seeking a quick start. diff --git a/site/src/content/docs/guides/vite.mdx b/site/src/content/docs/guides/vite.mdx index 8d5751fca01e..fec44ec2358b 100644 --- a/site/src/content/docs/guides/vite.mdx +++ b/site/src/content/docs/guides/vite.mdx @@ -11,6 +11,10 @@ thumbnail: guides/bootstrap-vite@2x.png **Want to skip to the end?** Download the source code and working demo for this guide from the [twbs/examples repository](https://github.com/twbs/examples/tree/main/vite). You can also [open the example in StackBlitz](https://stackblitz.com/github/twbs/examples/tree/main/vite?file=index.html) for live editing. + +**Using an AI coding agent?** Point it at our [`bootstrap-vite` skill](https://github.com/twbs/bootstrap/tree/main/skills/bootstrap-vite) to automate this setup. See [AI]([[docsref:/getting-started/ai]]) for all available skills. + + ## What is Vite? [Vite](https://vite.dev/) is a modern frontend build tool designed for speed and simplicity. It provides an efficient and streamlined development experience, especially for modern JavaScript frameworks. diff --git a/site/src/content/docs/guides/webpack.mdx b/site/src/content/docs/guides/webpack.mdx index e55c7526db92..5729556b337d 100644 --- a/site/src/content/docs/guides/webpack.mdx +++ b/site/src/content/docs/guides/webpack.mdx @@ -11,6 +11,10 @@ thumbnail: guides/bootstrap-webpack@2x.png **Want to skip to the end?** Download the source code and working demo for this guide from the [twbs/examples repository](https://github.com/twbs/examples/tree/main/webpack). You can also [open the example in StackBlitz](https://stackblitz.com/github/twbs/examples/tree/main/webpack?file=index.html) for live editing. + +**Using an AI coding agent?** Point it at our [`bootstrap-webpack` skill](https://github.com/twbs/bootstrap/tree/main/skills/bootstrap-webpack) to automate this setup. See [AI]([[docsref:/getting-started/ai]]) for all available skills. + + ## What is Webpack? [Webpack](https://webpack.js.org/) is a JavaScript module bundler that processes modules and their dependencies to generate static assets. It simplifies managing complex web applications with multiple files and dependencies. diff --git a/site/src/libs/llms.ts b/site/src/libs/llms.ts new file mode 100644 index 000000000000..6aaba28aaf6c --- /dev/null +++ b/site/src/libs/llms.ts @@ -0,0 +1,88 @@ +import { docsPages, getDocsPageSlug } from '@libs/content' +import { getData, type SidebarItem, type SidebarSubItem } from '@libs/data' +import { getVersionedDocsPath } from '@libs/path' +import { getSlug } from '@libs/utils' + +export interface LlmsPage { + title: string + description: string + // The versioned docs path, e.g. `/docs/6.0/getting-started/install`. + path: string + // The raw MDX body of the page, if available. + body: string +} + +export interface LlmsSection { + heading: string + pages: LlmsPage[] +} + +// Resolve a sidebar entry (by its group slug and title) to a docs collection +// entry, mirroring the slug logic in `DocsSidebar.astro`. Unlike the sidebar, +// this stays resilient and returns `undefined` when there is no match instead +// of throwing, so the generated `llms.txt` files never fail a build over a +// single stray sidebar entry. +function resolvePage(groupSlug: string, title: string | undefined): LlmsPage | undefined { + if (!title) { + return undefined + } + + const unversionedPageSlug = `${groupSlug}/${getSlug(title)}` + const entry = docsPages.find((page) => getDocsPageSlug(page.id) === unversionedPageSlug) + + if (!entry) { + return undefined + } + + return { + title: entry.data.title, + description: entry.data.description, + path: getVersionedDocsPath(unversionedPageSlug), + body: entry.body ?? '' + } +} + +// Build an ordered, grouped list of docs pages from `site/data/sidebar.yml`. +// Both `/llms.txt` and `/llms-full.txt` consume this so they stay in sync with +// the site navigation. +export function getLlmsSections(): LlmsSection[] { + const sidebar = getData('sidebar') + const sections: LlmsSection[] = [] + + for (const group of sidebar) { + if (!group.pages) { + continue + } + + const groupSlug = getSlug(group.title) + const pages: LlmsPage[] = [] + + for (const item of group.pages as SidebarItem[]) { + // Nested sub-group (e.g. Utilities): flatten its pages under the parent + // group heading. + if (item.group && item.pages) { + for (const subPage of item.pages as SidebarSubItem[]) { + const resolved = resolvePage(groupSlug, subPage.title) + + if (resolved) { + pages.push(resolved) + } + } + + continue + } + + const resolved = resolvePage(groupSlug, item.title) + + if (resolved) { + pages.push(resolved) + } + } + + if (pages.length > 0) { + sections.push({ heading: group.title, pages }) + } + } + + return sections +} diff --git a/site/src/libs/skills.ts b/site/src/libs/skills.ts new file mode 100644 index 000000000000..5175a1d66c9e --- /dev/null +++ b/site/src/libs/skills.ts @@ -0,0 +1,89 @@ +import fs from 'node:fs' +import path from 'node:path' +import { load as yamlLoad } from 'js-yaml' + +// The order the skills should appear in the docs. Any skill not listed here is +// appended alphabetically, so adding a new `skills//SKILL.md` shows up +// automatically without touching this file. +const skillOrder = [ + 'bootstrap-v4-v6-migration', + 'bootstrap-v5-v6-migration', + 'bootstrap-v6-install', + 'bootstrap-npm', + 'bootstrap-webpack', + 'bootstrap-parcel', + 'bootstrap-vite', + 'bootstrap-component', + 'bootstrap-component-js', + 'bootstrap-color-system', + 'bootstrap-utility-api' +] + +// The skills directory lives at the repository root, resolved relative to the +// current working directory (the repo root during dev and build), mirroring how +// `config.ts` reads `./config.yml` and `data.ts` reads `./site/data`. +const skillsDirectory = './skills' + +export interface Skill { + name: string + description: string + // A docs path (e.g. `/guides/npm`) linking to the matching human guide. + guide?: string +} + +// Extract and parse the YAML frontmatter block from a `SKILL.md` file. +function parseFrontmatter(content: string): Record { + const match = /^---\r?\n(?[\s\S]*?)\r?\n---/.exec(content) + + if (!match?.groups?.frontmatter) { + return {} + } + + const data = yamlLoad(match.groups.frontmatter) + + return typeof data === 'object' && data !== null ? (data as Record) : {} +} + +// Read every `skills//SKILL.md` frontmatter and return an ordered list. +// Used to generate the skills table in the docs so it never drifts from the +// actual skill files. +export function getSkills(): Skill[] { + const skills = fs + .readdirSync(skillsDirectory, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry): Skill | undefined => { + const skillPath = path.join(skillsDirectory, entry.name, 'SKILL.md') + + if (!fs.existsSync(skillPath)) { + return undefined + } + + const frontmatter = parseFrontmatter(fs.readFileSync(skillPath, 'utf8')) + + return { + name: typeof frontmatter.name === 'string' ? frontmatter.name : entry.name, + description: typeof frontmatter.description === 'string' ? frontmatter.description : '', + guide: typeof frontmatter.guide === 'string' ? frontmatter.guide : undefined + } + }) + .filter((skill): skill is Skill => skill !== undefined) + + return skills.sort((a, b) => { + const indexA = skillOrder.indexOf(a.name) + const indexB = skillOrder.indexOf(b.name) + + if (indexA !== -1 && indexB !== -1) { + return indexA - indexB + } + + if (indexA !== -1) { + return -1 + } + + if (indexB !== -1) { + return 1 + } + + return a.name.localeCompare(b.name) + }) +} diff --git a/site/src/pages/llms-full.txt.ts b/site/src/pages/llms-full.txt.ts new file mode 100644 index 000000000000..ea8d4c0a6b10 --- /dev/null +++ b/site/src/pages/llms-full.txt.ts @@ -0,0 +1,39 @@ +import type { APIRoute } from 'astro' +import { getConfig } from '@libs/config' +import { getLlmsSections } from '@libs/llms' + +// Strip leading MDX `import ... from '...'` statements so the concatenated +// output reads as prose. Shortcode tags (e.g. ``, ``) are +// left inline — full fidelity isn't achievable from raw MDX without rendering, +// but the content stays highly usable for ingestion. +function cleanBody(body: string): string { + return body.replace(/^import\s.+?\n/gm, '').trim() +} + +// Generates `/llms-full.txt`, the full text of the documentation concatenated +// into a single file for direct ingestion by LLMs. See https://llmstxt.org/. +export const GET: APIRoute = function GET({ site }) { + const { title, description } = getConfig() + const sections = getLlmsSections() + + const lines: string[] = [`# ${title}`, '', `> ${description}`, ''] + + for (const section of sections) { + for (const page of section.pages) { + const url = new URL(page.path, site) + const body = cleanBody(page.body) + + lines.push('---', '', `# ${page.title}`, '', `Source: ${url}`, '', page.description, '') + + if (body.length > 0) { + lines.push(body, '') + } + } + } + + return new Response(`${lines.join('\n').trimEnd()}\n`, { + headers: { + 'Content-Type': 'text/plain; charset=utf-8' + } + }) +} diff --git a/site/src/pages/llms.txt.ts b/site/src/pages/llms.txt.ts new file mode 100644 index 000000000000..e0d42cde85f0 --- /dev/null +++ b/site/src/pages/llms.txt.ts @@ -0,0 +1,35 @@ +import type { APIRoute } from 'astro' +import { getConfig } from '@libs/config' +import { getLlmsSections } from '@libs/llms' + +// Generates `/llms.txt`, a curated index of the docs for LLMs, following the +// llmstxt.org format: an H1 title, a blockquote summary, then grouped lists of +// links with descriptions. See https://llmstxt.org/. +export const GET: APIRoute = function GET({ site }) { + const { title, description } = getConfig() + const sections = getLlmsSections() + + const lines: string[] = [`# ${title}`, '', `> ${description}`, ''] + + lines.push( + `The full text of the documentation is available at [${new URL('llms-full.txt', site)}](${new URL('llms-full.txt', site)}).`, + '' + ) + + for (const section of sections) { + lines.push(`## ${section.heading}`, '') + + for (const page of section.pages) { + const url = new URL(page.path, site) + lines.push(`- [${page.title}](${url}): ${page.description}`) + } + + lines.push('') + } + + return new Response(`${lines.join('\n').trimEnd()}\n`, { + headers: { + 'Content-Type': 'text/plain; charset=utf-8' + } + }) +} diff --git a/skills/bootstrap-color-system/SKILL.md b/skills/bootstrap-color-system/SKILL.md new file mode 100644 index 000000000000..d34c6cd1f1d4 --- /dev/null +++ b/skills/bootstrap-color-system/SKILL.md @@ -0,0 +1,243 @@ +--- +name: bootstrap-color-system +description: Work with Bootstrap 6's color system in Sass and CSS. Use when customizing colors, adding or overriding theme colors, working with color modes, or using the color scales, semantic tokens, and theme classes. +guide: /customize/color +--- + +# Bootstrap 6 color system + +Bootstrap 6 colors flow through a fixed pipeline: a handful of `oklch()` hues become full scales, a subset of those scales gets semantic roles, and components read the roles — never the hues. Customizing colors means picking the right layer to change, so start by identifying which one your task belongs to. + +## Workflow + +- [ ] Step 1: Locate the layer you need to change +- [ ] Step 2: Base hues and scales +- [ ] Step 3: Theme colors +- [ ] Step 4: Layer colors for backgrounds, foregrounds, and borders +- [ ] Step 5: Color modes +- [ ] Step 6: Compile-time overrides +- [ ] Step 7: Runtime overrides +- [ ] Step 8: Functions and utilities +- [ ] Step 9: Verify + +--- + +## Step 1: Locate the layer you need to change + +The pipeline, with the file that owns each stage: + +1. **Base hues** — `scss/_colors.scss` defines 16 hues as `oklch()` scalars and collects them in `$colors`. +2. **Color scales** — the same file derives steps `025`–`975` from each hue with `color-mix()`, emitting `--blue-500`, `--red-200`, and so on into `:root`. +3. **Theme colors** — `scss/_theme.scss` maps semantic names (`primary`, `danger`, …) onto those scale tokens, one role at a time. +4. **Root tokens** — `scss/_root.scss` emits every theme role as `---` (`--danger-bg-subtle`) plus the layer color tokens. +5. **Theme classes** — `scss/helpers/_theme-colors.scss` outputs `.theme-` in `@layer helpers`, remapping `---` onto the generic `--theme-`. +6. **Components and utilities** — read `--theme-*` (with a neutral fallback) and the layer tokens. Nothing reads a raw hue. + +Match the task to the layer: a different brand blue is stage 1, a new semantic color is stage 3, restyling one component is a component token (see the `bootstrap-component` skill), and a one-off tweak in an app is a runtime CSS variable override. + +--- + +## Step 2: Base hues and scales + +Each hue is a single `oklch()` value that anchors step 500: + +```scss +$blue: oklch(60% 0.24 240) !default; +``` + +The 16 hues are blue, indigo, violet, purple, pink, red, orange, amber, yellow, lime, green, teal, cyan, brown, gray, and pewter, plus `$white` and `$black`. `$colors` collects them through `defaults()`, so the map merges rather than replaces. + +Scales are generated, not hand-picked. Every hue is mixed toward `$tint-color` (`var(--white)`) for steps `025 050 100 200 300 400` and toward `$shade-color` (`var(--black)`) for `600 700 800 900 950 975`, in the `$color-mix-space` color space (`lab`). Change a hue and its whole scale re-derives. + +Add a hue by merging into `$colors` — you get its full scale and any color utilities built from `$colors` automatically: + +```scss +@use "bootstrap/scss/bootstrap" with ( + $colors: ( + "slate": oklch(55% 0.04 250), + ) +); +``` + +Tint and shade percentages (`$color-tints`, `$color-shades`), the mixing space, and the mix targets are all configurable the same way. Individual steps can be overridden through `$color-tokens`. + +Mixing happens against `var(--white)` / `var(--black)` on purpose: it keeps `color-mix()` live in the output instead of letting the CSS minifier flatten it into static `lab()` values. + +--- + +## Step 3: Theme colors + +`$theme-colors` in `scss/_theme.scss` defines eight semantic colors — `primary`, `accent`, `success`, `danger`, `warning`, `info`, `inverse`, `secondary` — each a nested map of nine roles: + +| Role | What it's for | +| --- | --- | +| `base` | The raw brand color the rest is derived from | +| `bg` | Solid background, e.g. a solid button or badge | +| `bg-subtle` | Tinted surface for alerts, subtle buttons, callouts | +| `bg-muted` | One step stronger than subtle | +| `fg` | Text on a subtle surface | +| `fg-emphasis` | Higher-contrast text on a subtle surface | +| `contrast` | Text and icons sitting on `base` / `bg` | +| `border` | Borders on subtle surfaces | +| `focus-ring` | Focus outline color | + +Most roles are `light-dark(…)` pairs, and every value is a `var()` reference to a scale token rather than a literal color: + +```scss +"danger": ( + "base": var(--red-500), + "fg": light-dark(var(--red-600), var(--red-400)), + "bg-subtle": light-dark(var(--red-100), var(--red-900)), + "border": light-dark(var(--red-300), var(--red-600)), + "contrast": var(--white) +), +``` + +The map produces two things: `--danger-fg`-style tokens on `:root`, and a `.theme-danger` class that assigns them to the generic `--theme-fg`, `--theme-bg-subtle`, and friends. That indirection is the whole point — a component written against `--theme-*` supports every color, current and future, with one rule. + +The `primary`, `success`, and `danger` keys are required; other code (links, focus rings, form states, checked controls) reads their tokens. Renaming or removing them breaks the build. + +--- + +## Step 4: Layer colors for backgrounds, foregrounds, and borders + +Neutral surfaces don't live in `$theme-colors`. Three separate maps in `scss/_theme.scss` cover them: + +- `$theme-bgs` → `--bg-body`, `--bg-1` through `--bg-4` (increasingly contrasty surfaces), plus `--bg-white`, `--bg-black`, `--bg-transparent`. +- `$theme-fgs` → `--fg-body`, `--fg-1` through `--fg-4` (decreasing emphasis). +- `$theme-borders` → `--border-body`, `--border-subtle`, `--border-muted`, `--border-emphasized`. + +Use these as the *unthemed* fallbacks in component tokens: `var(--theme-bg-subtle, var(--bg-1))`, `var(--theme-border, var(--border-color))`. Note that `--border-color` is a separate global token in `$root-tokens` — the default border color for components — distinct from this `--border-*` family. + +Unlike `$theme-colors`, these are flat maps, which makes them easy to extend with an extra surface step. + +--- + +## Step 5: Color modes + +Bootstrap 6 does not redeclare variables per mode. Every adaptive token is a `light-dark()` pair, and CSS resolves it from the inherited `color-scheme`: + +- `:root` sets `color-scheme: light dark`, so the default is whatever the OS prefers. +- `[data-bs-theme="dark"]` and `[data-bs-theme="light"]` set `color-scheme` on that subtree, forcing a mode. It works on any element, nests, and needs no JavaScript. + +So switching modes is a matter of setting one attribute, and authoring adaptive colors is a matter of using `light-dark()` instead of writing a dark-mode override block. + +Two mixins exist for the cases `light-dark()` can't cover: + +```scss +// Non-color values can't go through light-dark() +@include color-mode(dark, $root: true) { + --shadow-strength: 2.4; +} +``` + +`color-mode($mode, $root)` honors the `$color-mode-type` config, which defaults to `"media-query"` (emitting `@media (prefers-color-scheme: …)`); set it to `"data"` to emit `[data-bs-theme="…"]` selectors instead. `color-scheme($name)` is the plain media-query wrapper when you always want the query. + +Custom modes work the same way: add your own `[data-bs-theme="blue"]` selector and override tokens inside it. + +--- + +## Step 6: Compile-time overrides + +There's an asymmetry worth internalizing before you write any `with ()` config: + +- Maps built with `defaults()` — `$colors`, `$color-tokens`, `$root-tokens`, and every component `$*-tokens` — **merge**. Pass only the keys you're changing, and pass `null` to remove one. +- `$theme-colors`, `$theme-bgs`, `$theme-fgs`, and `$theme-borders` are plain `!default` maps, so `with ()` **replaces them wholesale**. + +Retuning an existing theme color is therefore easiest one layer down — change the hue and everything derived from it follows: + +```scss +@use "bootstrap/scss/bootstrap" with ( + $blue: oklch(58% 0.21 250) +); +``` + +To restructure the semantic layer itself, configure the `theme` module before Bootstrap loads it, and pass the **full** map (start from the one in `scss/_theme.scss`, keeping `primary`, `success`, and `danger`): + +```scss +@use "bootstrap/scss/theme" with ( + $theme-colors: ( + "primary": ( + "base": var(--indigo-500), + "fg": light-dark(var(--indigo-600), var(--indigo-400)), + "fg-emphasis": light-dark(var(--indigo-800), var(--indigo-300)), + "bg": var(--indigo-500), + "bg-subtle": light-dark(var(--indigo-100), var(--indigo-900)), + "bg-muted": light-dark(var(--indigo-200), var(--indigo-800)), + "border": light-dark(var(--indigo-300), var(--indigo-600)), + "focus-ring": light-dark(color-mix(in oklch, var(--indigo-500) 50%, var(--bg-body)), color-mix(in oklch, var(--indigo-500) 75%, var(--bg-body))), + "contrast": var(--white) + ), + // … the remaining theme colors + ) +); +@use "bootstrap/scss/bootstrap"; +``` + +Order matters: the configured `@use` must come first, since a module can only be configured on its first load. + +When all you want is *one more* theme color, skip the Sass config and write the class yourself. Components only care about the `--theme-*` tokens: + +```scss +@layer helpers { + .theme-brand { + --theme-base: var(--purple-500); + --theme-bg: var(--purple-500); + --theme-bg-subtle: light-dark(var(--purple-100), var(--purple-900)); + --theme-fg: light-dark(var(--purple-600), var(--purple-400)); + --theme-border: light-dark(var(--purple-300), var(--purple-600)); + --theme-contrast: var(--white); + } +} +``` + +`.theme-brand` then works on every theme-aware component. The trade-off: utilities and `--brand-*` root tokens are generated from `$theme-colors`, so `.bg-brand` and `var(--brand-bg)` won't exist. + +--- + +## Step 7: Runtime overrides + +Every token is a CSS custom property, so most color changes need no recompile at all: + +```css +/* Global */ +:root { + --bs-primary-bg-subtle: #eef2ff; +} + +/* One subtree */ +.marketing-hero { + --bs-theme-bg-subtle: #eef2ff; +} +``` + +Token names are written **unprefixed** in Sass (`--primary-bg-subtle`). Bootstrap's dist and CDN CSS run PostCSS to add the `--bs-` prefix, so use `--bs-*` when overriding the shipped CSS and unprefixed names when you compile the source yourself. + +Runtime overrides are the right tool for per-page or per-tenant theming; compile-time config is for changing the system's defaults. + +--- + +## Step 8: Functions and utilities + +Helpers in `scss/_theme.scss` for reading across the color maps, mostly used when generating utilities: + +- `theme-color-values($role)` — flat map of color name to the raw value of that role. +- `theme-color-refs($role)` — same, but as `var(---)` references. +- `theme-token-refs($map, $prefix)` — the equivalent for the layer color maps. +- `theme-opacity-values($var, $fallback)` — an opacity ladder built with `color-mix()` against `transparent`. + +`color-contrast($background)` in `scss/_functions.scss` returns a light, dark, or black foreground using the WCAG contrast algorithm. It works on Sass color values, not `var()` references, so it's for build-time swatches and generated classes — the `contrast` role in `$theme-colors` is the runtime answer. + +The generated utilities follow the role names: `.fg-primary`, `.fg-emphasis-primary`, `.fg-contrast-primary`, `.bg-primary`, `.bg-subtle-primary`, `.bg-muted-primary`, `.border-primary`, and `.border-subtle-primary`, for every theme color. The layer colors get the same treatment (`.bg-1` through `.bg-4`, `.fg-1` through `.fg-4`, `.border-subtle`), and opacity is a numeric ladder on the same class names — `.fg-50`, `.bg-70`, `.border-30`, in steps of 10. + +Reach for `.theme-*` instead when you want a whole element themed rather than one property recolored. + +--- + +## Step 9: Verify + +1. `npm run css-lint`, then `npm run css`. +2. Grep the compiled `dist/css/bootstrap.css` for the tokens you expected (`--bs-primary-bg-subtle`, `--bs-slate-500`) to confirm they were emitted. +3. Check both color modes: OS preference, `data-bs-theme="dark"`, and a nested `data-bs-theme="light"` inside it. +4. Verify contrast for any new theme color — at minimum `contrast` on `bg`, and `fg` on `bg-subtle`, in both modes. +5. If you added or removed entries in `$colors` or `$theme-colors`, confirm the generated utilities and `.theme-*` classes changed accordingly, and that `npm run docs-build` still passes (the docs render swatches from these maps). diff --git a/skills/bootstrap-component-js/SKILL.md b/skills/bootstrap-component-js/SKILL.md new file mode 100644 index 000000000000..132bd9feed4f --- /dev/null +++ b/skills/bootstrap-component-js/SKILL.md @@ -0,0 +1,339 @@ +--- +name: bootstrap-component-js +description: Add JavaScript behavior to a Bootstrap 6 component. Use when writing a component's TypeScript source, extending BaseComponent, wiring the data API, adding events or config options, or integrating Floating UI or a third-party library. +guide: /getting-started/javascript +--- + +# Bootstrap 6 component JavaScript + +Every Bootstrap plugin is a TypeScript module in `js/src/` that extends `BaseComponent`, which handles instance registration, config merging, and teardown. Follow the shared structure and a component gets `getInstance()`, `getOrCreateInstance()`, `data-bs-*` config, namespaced events, and `dispose()` without writing any of it. + +The sources are TypeScript, but the runtime patterns are the same ones v5 used with types layered on top. Bootstrap compiles them to JavaScript and emits its own declarations, so consumers need no `@types/bootstrap`. + +For the component's styles, see the `bootstrap-component` skill. + +## Workflow + +- [ ] Step 1: Scaffold the module +- [ ] Step 2: Define config options +- [ ] Step 3: Implement behavior and events +- [ ] Step 4: Wire the data API +- [ ] Step 5: Position with Floating UI (optional) +- [ ] Step 6: Wrap a third-party library (optional) +- [ ] Step 7: Clean up in dispose() +- [ ] Step 8: Register and test +- [ ] Step 9: Verify + +--- + +## Step 1: Scaffold the module + +Create `js/src/.ts`. Read `js/src/alert.ts` first — it's the smallest complete example. + +```ts +/** + * -------------------------------------------------------------------------- + * Bootstrap widget.ts + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +import BaseComponent from './base-component.js' +import EventHandler from './dom/event-handler.js' + +/** + * Constants + */ + +const NAME = 'widget' +const DATA_KEY = 'bs.widget' +const EVENT_KEY = `.${DATA_KEY}` +const DATA_API_KEY = '.data-api' + +const EVENT_SHOW = `show${EVENT_KEY}` +const EVENT_SHOWN = `shown${EVENT_KEY}` +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}` + +const CLASS_NAME_SHOW = 'show' +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="widget"]' + +/** + * Class definition + */ + +class Widget extends BaseComponent { + // Getters + static override get NAME(): string { + return NAME + } +} + +export default Widget +``` + +- `NAME` is the only required static. `BaseComponent` derives `DATA_KEY` (`bs.`) and `EVENT_KEY` (`.bs.`) from it, stores the instance on the element with `Data.set()`, and disposes any previous instance bound to the same element. +- Keep the section comments (`Constants`, `Class definition`, `Data API implementation`) and member grouping (`constructor`, `// Getters`, `// Public`, `// Private`, `// Static`) — every module in `js/src/` looks the same. +- Repo style: ES modules only, no semicolons, 2-space indent, single quotes, and `override` on every member that overrides `BaseComponent` or `Config`. +- Relative imports keep the ESM `.js` extension even though the file on disk is `.ts`, because `tsconfig.json` sets `moduleResolution: nodenext`. Rolldown, Vite, and Vitest all map the specifier back to the `.ts` source. +- The compiler is strict in ways worth knowing before you write: `verbatimModuleSyntax` wants type imports marked (`import EventHandler, { type BootstrapEvent } from './dom/event-handler.js'`), and `erasableSyntaxOnly` rules out enums, namespaces, and constructor parameter properties. +- Declare instance state as `protected declare _foo: Type` and assign it in the constructor, the way `toast.ts` does. `declare` is type-only; a real field without an initializer would emit `_foo = undefined` under `useDefineForClassFields` and wipe whatever the base constructor set. +- v6 has **no `defineJQueryPlugin`** and **no `js/src/util.ts`**. Helpers live in `js/src/util/`; DOM wrappers in `js/src/dom/` (`event-handler`, `selector-engine`, `manipulator`, `data`). + +--- + +## Step 2: Define config options + +Declare the config shape, the defaults, and the runtime types as module constants, then expose them as statics: + +```ts +type WidgetConfig = { + animation: boolean + delay: number +} + +const Default: WidgetConfig = { + animation: true, + delay: 5000 +} + +const DefaultType = { + animation: 'boolean', + delay: 'number' +} + +class Widget extends BaseComponent { + protected declare _config: WidgetConfig + protected declare _timeout: number | null + + constructor(element?: string | Element | null, config?: Partial | null) { + super(element, config) + + this._timeout = null + } + + // Getters + static override get Default(): WidgetConfig { + return Default + } + + static override get DefaultType(): Record { + return DefaultType + } + + static override get NAME(): string { + return NAME + } +} + +export default Widget +export type { WidgetConfig } +``` + +- The config type does double duty: `Default` is typed with it, the constructor accepts a `Partial<>` of it since callers pass a subset, and redeclaring `protected declare _config: WidgetConfig` narrows `BaseComponent`'s loose config so `this._config.delay` is a `number`. `DefaultType` stays a plain string map — it drives the runtime check, not the compiler. Export the type; the type tests import it. +- Merge order, applied by `Config._mergeConfigObj()` (last wins): `Default` → the JSON in `data-bs-config` → individual `data-bs-*` attributes → the object passed to the constructor. You don't write any of this. +- `data-bs-*` names are camelCased when read, so `data-bs-auto-hide="false"` becomes `autoHide: false`. +- Every key in `DefaultType` is validated and throws a `TypeError` on mismatch. Values are matched as regex, so unions work: `'boolean'`, `'(number|string)'`, `'(string|element|function)'`. +- Normalize a value before validation by overriding `_configAfterMerge(config)` — the usual place to resolve a selector string into an element or coerce a shorthand. + +--- + +## Step 3: Implement behavior and events + +```ts +class Widget extends BaseComponent { + // Public + show(): void { + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW) + + if (showEvent.defaultPrevented) { + return + } + + this._element.classList.add(CLASS_NAME_SHOW) + + this._queueCallback(() => { + EventHandler.trigger(this._element, EVENT_SHOWN) + }, this._element, this._config.animation) + } +} +``` + +- Events come in pairs: an infinitive fired before the action (`show.bs.widget`, cancelable) and a past participle after it (`shown.bs.widget`). Always bail when the "before" event is `defaultPrevented`. +- `_queueCallback(callback, element, isAnimated)` runs the callback after the CSS transition on `element` finishes, and skips it if the instance was disposed mid-transition. Pass the animation flag from config so non-animated components fire immediately. +- Add a payload when listeners need context: `EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget })`. `trigger()` returns a `BootstrapEvent`, an `Event` widened with the payload keys, so reading them back in a handler needs no cast. +- Query with `SelectorEngine` (`findOne`, `find`, `next`, `prev`, `getElementFromSelector`) instead of raw `querySelector`, and read or write attributes with `Manipulator`. +- Bind internal listeners with `EventHandler.on(this._element, EVENT_MOUSEOVER, …)` using the namespaced constants, so `dispose()` removes them in one call. + +--- + +## Step 4: Wire the data API + +The data API goes at the bottom of the module, after the class, as an import side effect: + +```ts +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { + event.preventDefault() + Widget.getOrCreateInstance(this).toggle() +}) +``` + +- Delegate from `document` and namespace the event with `.data-api` so it stays distinguishable from user listeners. +- Keep `function` rather than an arrow when you need the delegated element: `EventHandler` types the callback's `this`, so `this` is the match for `SELECTOR_DATA_TOGGLE`. Use an arrow and reach for `event.target` only when you need the actual click target. +- Prefer the shared helpers in `js/src/util/component-functions.ts` over hand-rolled handlers: + - `enableDismissTrigger(Widget)` wires `[data-bs-dismiss="widget"]` to `hide()`. Pass a method name for anything else: `enableDismissTrigger(Alert, 'close')`. + - `eventActionOnPlugin(Widget, 'click', SELECTOR_DATA_TOGGLE, 'toggle')` handles the "trigger points at one or more targets via `data-bs-target`" case and skips disabled elements. +- Because these are side effects, the data API only exists once the module is imported. Anything importing a single component file gets that component's data API and nothing else. + +--- + +## Step 5: Position with Floating UI (optional) + +Menus, tooltips, and popovers position with `@floating-ui/dom`, an optional peer dependency. Guard for it in the constructor before calling `super()` — legal here because the class declares its fields instead of initializing them: + +```ts +import { autoUpdate, computePosition } from '@floating-ui/dom' + +class Widget extends BaseComponent { + protected declare _floatingCleanup: (() => void) | null + + constructor(element?: string | Element | null, config?: Partial | null) { + if (typeof computePosition === 'undefined') { + throw new TypeError('Bootstrap\'s widgets require Floating UI (https://floating-ui.com)') + } + + super(element, config) + this._floatingCleanup = null + } +} +``` + +Create the positioner when the element is shown and keep the cleanup function so it can be cancelled: + +```ts +class Widget extends BaseComponent { + // Private + protected _createFloating(): void { + this._updateFloatingPosition() + this._floatingCleanup = autoUpdate(this._element, this._floatingEl, () => this._updateFloatingPosition()) + } + + protected _disposeFloating(): void { + if (this._floatingCleanup) { + this._floatingCleanup() + this._floatingCleanup = null + } + } +} +``` + +`js/src/util/floating-ui.ts` handles placement so you don't reimplement it: `getDefaultPlacement()` flips `-start` / `-end` for RTL, `parseResponsivePlacement()` and `getResponsivePlacement()` support `data-bs-placement="bottom-start md:top-end"`, and `createBreakpointListeners()` / `disposeBreakpointListeners()` re-position on breakpoint changes. + +--- + +## Step 6: Wrap a third-party library (optional) + +`js/src/datepicker.ts` wraps `vanilla-calendar-pro` and is the reference for this pattern: + +1. Keep Bootstrap's config surface. Users set `data-bs-*` options; a private builder (`_buildCalendarOptions()`) translates them into the library's option shape. +2. Instantiate in a private initializer called from the constructor and hold the instance on a private field typed with the library's own type (`protected declare _calendar: Calendar | null`). +3. Re-emit the library's callbacks as namespaced Bootstrap events so consumers only learn one API. +4. Call the library's teardown in `dispose()` before `super.dispose()`, and null the field. +5. Add the package to `peerDependencies` in `package.json`, guard for its absence, and document the extra install step on the component's docs page. + +Don't fork or vendor the library, and don't expose its instance as public API — anything users need should be a method or event on the Bootstrap class. + +--- + +## Step 7: Clean up in dispose() + +```ts +class Widget extends BaseComponent { + // Public + override dispose(): void { + clearTimeout(this._timeout!) + this._observer?.disconnect() + this._disposeFloating() + + super.dispose() + } +} +``` + +`super.dispose()` removes the `Data` entry, removes every listener bound on `this._element` under `EVENT_KEY`, and nulls the instance's own properties. Everything else is yours to undo first: timers, `MutationObserver` / `ResizeObserver` instances, listeners bound on `document` or `window`, Floating UI cleanups, and third-party instances. Leaks here show up as failing "should dispose" specs. + +--- + +## Step 8: Register and test + +1. Add the export to `js/src/index.ts`: + + ```ts + export { default as Widget } from './widget.js' + ``` + + That barrel is the package entry point and feeds both `dist/js/bootstrap.js` and the bundle. Nothing else needs updating: Rolldown builds from `js/src/index.ts`, `build/build-plugins.mjs` globs `js/src/**/*.ts` for the per-plugin files in `js/dist/`, and Vitest picks up `js/tests/unit/**/*.spec.js`. + +2. Extend both type tests. `js/tests/types/api.ts` asserts the public surface against the sources; `js/tests/types/consumer.ts` asserts the same surface against the shipped `js/dist/*.d.ts`, imported through the package name the way a downstream project does. Add the class, its config type, and its methods to both — a declaration-emit bug passes the first and fails the second. + +3. Add a spec at `js/tests/unit/.spec.js`. Specs are plain JavaScript and run in a real Chromium through Vitest browser mode: + + ```js + import Widget from '../../src/widget.js' + import { clearFixture, getFixture } from '../helpers/fixture.js' + + describe('Widget', () => { + let fixtureEl + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + }) + + it('should expose the plugin name and data key', () => { + expect(Widget.NAME).toEqual('widget') + expect(Widget.DATA_KEY).toEqual('bs.widget') + }) + }) + ``` + + - `describe`, `it`, `expect`, and the hooks are globals (`globals: true` in `js/tests/vitest.config.mts`), so don't import them. Import `vi` from `vitest` when you need spies or fake timers — that's the preferred API for new specs. + - `js/tests/vitest-setup.js` maps the Jasmine API the older specs are written against onto Vitest, so `spyOn`, `jasmine.clock()`, `toHaveClass`, `toBeTrue`, `toBeFalse`, and `toHaveSize` still work. Two gotchas: the shimmed `spyOn` stubs the method (add `.and.callThrough()` for the real one), and Vitest 4 dropped the `done` callback, so an asynchronous test returns a promise and resolves it from the listener. + - Cover the constructor with both a selector and an element, config coming from `data-bs-*`, each public method, both events including the cancelable one, the data API without manual instantiation, and `dispose()`. Coverage counts every file in `js/src`, so a component with no spec drags the run below the thresholds in `js/tests/vitest.config.mts` (90% of statements, functions, and lines; 88% of branches) and fails the build. + +4. Document it on the component's docs page: add `js: optional` (or `js: required`) to the frontmatter, `` in an `### Options` section, and `` tables for options, methods, and events. Demo snippets live in `site/src/assets/partials/snippets.js` and render with ``. + +--- + +## Step 9: Verify + +1. `npm run js-lint` +2. `npm run js-typecheck` — `tsc --noEmit` over `js/src` and the type tests. +3. `npm run js-test-unit`, or `npm run js-test` to add the two Rolldown integration bundles that prove the module is tree-shakeable. Playwright needs its browser once per machine (`npx playwright install chromium`), and `npm run js-debug` runs the specs in a visible browser. +4. `npm run js` — Rolldown compiles the sources to `dist/js` and `js/dist`, terser minifies, and `tsc` emits the `.d.ts` files. Check the bundle grew by roughly what you'd expect; `npm run bundlewatch` enforces size budgets. +5. `npm start` and exercise the component on its docs page, with the keyboard as well as the mouse. + +--- + +## In your own project + +`package.json` maps `bootstrap/js/src/*.js` to the compiled `js/dist/*.js` and its declarations, so the same specifier works from JavaScript and TypeScript, and custom components can build on the same base class: + +```ts +import BaseComponent from 'bootstrap/js/src/base-component.js' +import EventHandler from 'bootstrap/js/src/dom/event-handler.js' + +class Widget extends BaseComponent { + static override get NAME(): string { + return 'widget' + } +} +``` + +Two things to keep in mind. Importing a Bootstrap component module runs its data API as a side effect, which is what you want in an app entry point but not inside a library. And `DATA_KEY` is derived as `bs.`, so pick a `NAME` that doesn't collide with a Bootstrap plugin or your instance will replace theirs on shared elements. diff --git a/skills/bootstrap-component/SKILL.md b/skills/bootstrap-component/SKILL.md new file mode 100644 index 000000000000..e0fba1cfa223 --- /dev/null +++ b/skills/bootstrap-component/SKILL.md @@ -0,0 +1,284 @@ +--- +name: bootstrap-component +description: Build a Bootstrap 6 component's CSS and markup. Use when creating a new component, adding modifier or variant classes, or authoring component styles with Bootstrap's Sass token maps and theme overrides. +guide: /customize/components +--- + +# Bootstrap 6 component CSS + +Author a component's styles and markup — the non-JavaScript pieces. Bootstrap 6 components are built from a Sass map of CSS custom properties (the token map), a base class that emits those tokens, and modifier classes that override them. + +For interactive behavior, see the `bootstrap-component-js` skill. For palettes, theme colors, and color modes, see the `bootstrap-color-system` skill. + +## Workflow + +- [ ] Step 1: Plan the component +- [ ] Step 2: Create the partial and token map +- [ ] Step 3: Write the base class +- [ ] Step 4: Add modifiers, variants, and sizes +- [ ] Step 5: Wire up theme colors +- [ ] Step 6: Markup and accessibility +- [ ] Step 7: Register the partial +- [ ] Step 8: Document the component +- [ ] Step 9: Verify + +--- + +## Step 1: Plan the component + +1. Pick a singular, kebab-case name. It drives everything: the partial `scss/_.scss`, the base class `.`, the token prefix `---*`, and the map `$-tokens`. +2. Decide what the component needs before writing CSS: + - **Tokens only** — one look, customized through CSS variables. Model on `scss/_alert.scss`. + - **Variants** — contextual color treatments like `.badge-subtle`. Model on `scss/_badge.scss`. + - **Variants and sizes** — model on `scss/buttons/_button.scss`. +3. Check whether an existing component already covers the need. Extending one with a modifier is preferable to adding a near-duplicate. + +Never edit generated output in `dist/`, `js/dist/`, or `_site/`. Source lives in `scss/` and `js/src/`. + +--- + +## Step 2: Create the partial and token map + +Create `scss/_.scss`. Declare the empty map first, then fill it through `defaults()`: + +```scss +@use "config" as *; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/tokens" as *; + +$widget-tokens: () !default; + +// scss-docs-start widget-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$widget-tokens: defaults( + ( + --widget-gap: var(--spacer-2), + --widget-padding-x: var(--spacer-4), + --widget-padding-y: var(--spacer-2), + --widget-color: var(--theme-fg, inherit), + --widget-bg: var(--theme-bg-subtle, var(--bg-1)), + --widget-border-color: var(--theme-border, var(--border-color)), + --widget-border-radius: var(--radius-5), + ), + $widget-tokens +); +// scss-docs-end widget-tokens +``` + +Rules that matter here: + +- The `$widget-tokens: () !default;` line is what makes the map overridable — `@use "bootstrap/scss/bootstrap" with ($widget-tokens: (…))` writes into it, and `defaults()` merges those overrides over your defaults. Passing `null` for a key removes that token entirely. +- The second assignment needs the `stylelint-disable-next-line scss/dollar-variable-default` comment, since it intentionally reassigns without `!default`. +- Token names are namespaced to the component and written **unprefixed**. Bootstrap's dist and CDN CSS run PostCSS to add the `--bs-` prefix (`--bs-widget-bg`); compiling the source yourself keeps them unprefixed. +- Build values from existing global tokens — `var(--spacer-3)`, `var(--radius-5)`, `var(--border-width)`, `var(--font-weight-semibold)` — rather than hardcoded lengths, so the component scales with the rest of the framework. +- The `scss-docs-start` / `scss-docs-end` markers are load-bearing: the docs page renders the map with ``, which throws at build time if the markers are missing. + +--- + +## Step 3: Write the base class + +Declarations go inside a cascade layer, with the token map emitted on the root selector: + +```scss +@layer components { + .widget { + @include tokens($widget-tokens); + + display: flex; + gap: var(--widget-gap); + align-items: center; + padding: var(--widget-padding-y) var(--widget-padding-x); + color: var(--widget-color); + background-color: var(--widget-bg); + border: var(--border-width) solid var(--widget-border-color); + @include border-radius(var(--widget-border-radius)); + } +} +``` + +- Pick the right layer. Components use `@layer components`, form controls `@layer forms`, helpers `@layer helpers`. The order is declared once in `scss/_root.scss`: `colors, config, root, reboot, layout, content, forms, components, custom, helpers, utilities`. +- `@include tokens($widget-tokens)` comes first, then declarations, then nested rules. Declarations read `var(--widget-*)`, never the Sass map directly. +- Use `@include border-radius()` and `@include transition()` instead of the raw properties — stylelint blocks the raw ones because the mixins honor `$enable-rounded`, `$enable-transitions`, and `prefers-reduced-motion`. +- Use logical properties (`padding-inline-start`, `margin-block-end`, `inset-inline-start`) so the component works in RTL without an override. +- No `lighten()` / `darken()` — use `color-mix()`, relative `oklch()`, or `light-dark()`. No `border: none` / `outline: none` — use `0`. + +--- + +## Step 4: Add modifiers, variants, and sizes + +A modifier only overrides tokens; it should not repeat the base declarations: + +```scss +.widget-lg { + --widget-padding-x: var(--spacer-6); + --widget-padding-y: var(--spacer-3); + --widget-border-radius: var(--radius-7); +} +``` + +When a component has a set of contextual treatments, drive them from an overridable map whose values are **theme sub-keys**, then loop. This is the `scss/_badge.scss` pattern: + +```scss +// scss-docs-start widget-variants +$widget-variants: ( + "subtle": ( + "color": "fg", + "bg": "bg-subtle", + "border-color": "transparent" + ), + "outline": ( + "color": "fg", + "bg": "transparent", + "border-color": "border" + ) +) !default; +// scss-docs-end widget-variants + +@layer components { + // scss-docs-start widget-variant-loop + @each $variant, $properties in $widget-variants { + .widget-#{$variant} { + @each $property, $value in $properties { + @if $value == "transparent" { + --widget-#{$property}: transparent; + } @else { + --widget-#{$property}: var(--theme-#{$value}); + } + } + } + } + // scss-docs-end widget-variant-loop +} +``` + +Mapping to theme sub-keys rather than literal colors is what lets `.widget-outline.theme-success` work without a variant-per-color explosion. + +Sizes follow the button pattern: a list run through `defaults()`, looped into classes that re-point the component's tokens at the shared `--btn-input--*` scale. + +```scss +$widget-sizes: () !default; +$widget-sizes: defaults( + ("sm", "lg"), + $widget-sizes +); +``` + +`defaults()` turns a list into a map, so a project can drop a size with `$widget-sizes: ("sm": null)` or add its own. + +--- + +## Step 5: Wire up theme colors + +Theme classes (`.theme-primary`, `.theme-danger`, …) are generated from `$theme-colors` and set a fixed set of variables: `--theme-base`, `--theme-fg`, `--theme-fg-emphasis`, `--theme-bg`, `--theme-bg-subtle`, `--theme-bg-muted`, `--theme-border`, `--theme-focus-ring`, and `--theme-contrast`. + +A component gets theme support for free by reading those variables with a neutral fallback, which is why the defaults in Step 2 look like this: + +```scss +--widget-color: var(--theme-fg, inherit), +--widget-bg: var(--theme-bg-subtle, var(--bg-1)), +--widget-border-color: var(--theme-border, var(--border-color)), +``` + +Unthemed markup uses the fallbacks; adding a theme class recolors the component with no extra CSS: + +```html +
+``` + +Do not hardcode `var(--red-500)` or a specific theme color in component tokens. Adding or overriding theme colors themselves is covered by the `bootstrap-color-system` skill. + +--- + +## Step 6: Markup and accessibility + +- Start from the semantic element. Add ARIA only where the markup can't convey meaning on its own — for example `role="alert"` on a live message, `aria-expanded` on a disclosure trigger. +- Interactive parts must be reachable by keyboard and show a visible focus ring. The `.focus-ring` helper applies `outline: var(--focus-ring-width) solid var(--theme-focus-ring, var(--focus-ring-color))` on `:focus-visible`; theme-aware components inherit the right color automatically. +- Give icon-only controls an accessible name with `.visually-hidden` text or `aria-label`. +- Never signal state through color alone; pair it with text, an icon, or an ARIA attribute. +- Keep the markup RTL-safe: logical properties in CSS, no left/right assumptions in the DOM order. + +--- + +## Step 7: Register the partial + +Forward the partial from `scss/bootstrap.scss` in the components block, keeping the existing ordering: + +```scss +@forward "widget"; +``` + +Components that live in a subdirectory (`scss/buttons/`, `scss/forms/`, `scss/helpers/`) are forwarded from that directory's `index.scss` barrel, and the barrel is forwarded from `scss/bootstrap.scss`. Add new files to the barrel, not to `bootstrap.scss`, in that case. + +--- + +## Step 8: Document the component + +1. Add `site/src/content/docs/components/.mdx`: + + ```markdown + --- + title: Widget + description: Documentation and examples for Bootstrap’s widget component. + toc: true + css_layer: components + --- + ``` + + Add `js: optional` (or `js: required`) when the component ships a JavaScript plugin. + +2. Add an entry to `site/data/sidebar.yml` under the Components section. The sidebar drives the nav, the page ordering, and inclusion in `llms.txt`, so a page without an entry is effectively invisible: + + ```yaml + - title: Widget + meta: + - added: 6.0.0 + ``` + +3. Show usage with `` for live demos, and render the token map under a `### Variables` section: + + ```markdown + + + + ``` + +4. Link internally with `[[docsref:/components/widget/]]` rather than a hardcoded versioned path. + +--- + +## Step 9: Verify + +1. `npm run css-lint` — catches disallowed properties, wrong declaration order, and missing logical properties. +2. `npm run css` — compiles, prefixes, and minifies. Confirm the tokens land in `dist/css/bootstrap.css` with the `--bs-` prefix. +3. `npm run docs-build` — fails on a broken `docsref`, a missing sidebar entry, or missing `scss-docs` markers. +4. Check the component in the browser: with and without a theme class, in both color modes, with `dir="rtl"` on `` (v6 has no separate RTL stylesheet — logical properties do the work), and with reduced motion enabled if it animates. + +--- + +## In your own project + +The same pattern works for your own components on top of Bootstrap. In `custom.scss`, `@use` the pieces you need and follow Steps 2–6 unchanged: + +```scss +@use "bootstrap/scss/config" as *; +@use "bootstrap/scss/mixins/tokens" as *; +@use "bootstrap/scss/bootstrap"; + +$widget-tokens: () !default; +$widget-tokens: defaults( + ( + --widget-bg: var(--theme-bg-subtle, var(--bg-1)), + ), + $widget-tokens +); + +@layer custom { + .widget { + @include tokens($widget-tokens); + background-color: var(--widget-bg); + } +} +``` + +Use the `custom` layer, which sits after `components` and before `helpers` and `utilities`, so your styles win over component defaults while utilities still override yours. Retuning an existing Bootstrap component doesn't require any of this — override its CSS variables at runtime, or pass its `$*-tokens` map through `with ()` at compile time. diff --git a/skills/bootstrap-npm/SKILL.md b/skills/bootstrap-npm/SKILL.md new file mode 100644 index 000000000000..933006baf315 --- /dev/null +++ b/skills/bootstrap-npm/SKILL.md @@ -0,0 +1,173 @@ +--- +name: bootstrap-npm +description: Build a Bootstrap 6 starter project using just npm and command-line tools (no bundler). Use when setting up Bootstrap with npm, compiling Bootstrap's Sass to CSS from the command line, or creating an npm-based Bootstrap project. +guide: /guides/npm +--- + +# Bootstrap 6 with npm + +Set up a Bootstrap 6 project that compiles Sass to CSS using npm command-line tools, with no JavaScript bundler. Bootstrap's pre-built JS bundle is loaded directly via a ` + + + ``` + +3. `package.json` — add scripts. Note `--load-path=node_modules` so Sass resolves `bootstrap`: + + ```json + { + "scripts": { + "css-compile": "sass --load-path=node_modules --style expanded --source-map --embed-sources scss:css", + "css-prefix": "postcss --config postcss.config.js --replace \"css/*.css\" \"!css/*.min.css\"", + "build": "npm-run-all --sequential css-compile css-prefix", + "watch": "nodemon --watch scss/ --ext scss --exec \"npm run build\"", + "serve": "sirv --port 8080 --dev .", + "start": "npm-run-all build --parallel watch serve", + "lint": "stylelint \"scss/**/*.scss\"" + } + } + ``` + +4. `stylelint.config.mjs`: + + ```js + /** @type {import('stylelint').Config} */ + export default { + extends: 'stylelint-config-twbs-bootstrap' + } + ``` + +5. Start the dev server: + + ```sh + npm start + ``` + +--- + +## Step 4: Import Bootstrap's Sass + +Add to `scss/styles.scss` using the modern `@use` rule (never `@import`): + +```scss +// Import all of Bootstrap's CSS +@use "bootstrap/scss/bootstrap"; +``` + +Customize tokens with `with ()` — include only the keys you want to change: + +```scss +@use "bootstrap/scss/bootstrap" with ( + $root-tokens: ( + --border-radius: .25rem, + --spacer: 1.5rem, + ) +); +``` + +Override CSS custom properties through the token maps (`$root-tokens` and each component's `$*-tokens` map), following [Customize › Sass](https://getbootstrap.com/docs/6.0/customize/sass/#compile-time-overrides). Token names are written **unprefixed** in Sass (e.g. `--border-radius`, `--spacer`); Bootstrap's dist/CDN CSS runs PostCSS to add the `--bs-` prefix, so compiling the source yourself keeps them unprefixed. + +--- + +## Step 5: Verify + +1. Run `npm start` and open `http://localhost:8080` — the `.container` and `.btn-solid.theme-primary` button should be styled once the CSS compiles. +2. Confirm `css/styles.css` is generated and updates when you edit `scss/styles.scss` (watch is running). +3. Confirm no console errors from the JS bundle. +4. Run `npm run lint` to check Sass against Bootstrap's stylelint config. +5. Bootstrap 6 requires a browser that supports `oklch()` and `color-mix()`. + +For an optimized build that imports only the parts of Bootstrap you use, see the [twbs/examples sass-js project](https://github.com/twbs/examples/tree/main/sass-js). diff --git a/skills/bootstrap-parcel/SKILL.md b/skills/bootstrap-parcel/SKILL.md new file mode 100644 index 000000000000..900cc2b375c4 --- /dev/null +++ b/skills/bootstrap-parcel/SKILL.md @@ -0,0 +1,144 @@ +--- +name: bootstrap-parcel +description: Include and bundle Bootstrap 6's CSS and JavaScript in a project using Parcel. Use when setting up Bootstrap with Parcel, bundling Bootstrap's Sass and JS through Parcel, or creating a Parcel-based Bootstrap project. +guide: /guides/parcel +--- + +# Bootstrap 6 with Parcel + +Set up a Bootstrap 6 project bundled with Parcel. Parcel is zero-config and auto-installs language transformers (like Sass) as it detects them. Requires Node.js and terminal familiarity. + +## Workflow + +- [ ] Step 1: Set up npm and install dependencies +- [ ] Step 2: Create the project structure +- [ ] Step 3: Configure Parcel (HTML + npm script) +- [ ] Step 4: Import Bootstrap's CSS and JS +- [ ] Step 5: Verify + +--- + +## Step 1: Set up npm and install dependencies + +1. Create the project and initialize npm: + + ```sh + mkdir my-project && cd my-project + npm init -y + ``` + +2. Install Parcel: + + ```sh + npm i --save-dev parcel + ``` + +3. Install Bootstrap and its optional peer dependencies (omit `@floating-ui/dom` if not using menus/popovers/tooltips; omit `vanilla-calendar-pro` if not using the datepicker): + + ```sh + npm i --save bootstrap @floating-ui/dom vanilla-calendar-pro + ``` + +Parcel will auto-install the [Sass plugin](https://parceljs.org/languages/sass/) when it detects `.scss`. To install it manually: `npm i --save-dev @parcel/transformer-sass`. + +--- + +## Step 2: Create the project structure + +```sh +mkdir {src,src/js,src/scss} +touch src/index.html src/js/main.js src/scss/styles.scss +``` + +Target layout (Parcel needs no config file): + +```text +my-project/ +├── src/ +│ ├── js/ +│ │ └── main.js +│ ├── scss/ +│ │ └── styles.scss +│ └── index.html +├── package-lock.json +└── package.json +``` + +--- + +## Step 3: Configure Parcel + +1. `src/index.html` — link the Sass and JS entry points directly (Parcel resolves and bundles them): + + ```html + + + + + + Bootstrap w/ Parcel + + + + +
+

Hello, Bootstrap and Parcel!

+ +
+ + + ``` + +2. `package.json` — add the start script: + + ```json + { + "scripts": { + "start": "parcel serve src/index.html --public-url / --dist-dir dist", + "test": "echo \"Error: no test specified\" && exit 1" + } + } + ``` + +3. Start Parcel: + + ```sh + npm start + ``` + +--- + +## Step 4: Import Bootstrap's CSS and JS + +1. `src/scss/styles.scss` — import the source Sass with `@use` (never `@import`): + + ```scss + // Import all of Bootstrap's CSS + @use "bootstrap/scss/bootstrap"; + ``` + + To customize, override Bootstrap's CSS token maps (`$root-tokens`, `$*-tokens`) — see [Customize › Sass](https://getbootstrap.com/docs/6.0/customize/sass/#compile-time-overrides). Token names are written unprefixed in Sass; Bootstrap's dist/CDN CSS adds the `--bs-` prefix via PostCSS, so compiling the source yourself keeps them unprefixed. + +2. `src/js/main.js` — import Bootstrap's JS. Floating UI and Vanilla Calendar Pro are imported automatically through Bootstrap: + + ```js + // Import all of Bootstrap's JS + import * as bootstrap from 'bootstrap' + ``` + + Import only the plugins you need to keep bundle sizes down: + + ```js + import { Tooltip, Toast, Popover } from 'bootstrap' + ``` + +--- + +## Step 5: Verify + +1. Run `npm start` and open the served URL — the `.container` and `.btn-solid.theme-primary` button should be styled. +2. Confirm Parcel installed the Sass transformer (no `.scss` build errors) and the console is free of module errors. +3. Trigger a JS component (tooltip/dialog) to confirm the JS bundle works. +4. Bootstrap 6 requires a browser that supports `oklch()` and `color-mix()`. + +For an optimized build that imports only the parts of Bootstrap you use, see the [twbs/examples Parcel project](https://github.com/twbs/examples/tree/main/parcel). diff --git a/skills/bootstrap-utility-api/SKILL.md b/skills/bootstrap-utility-api/SKILL.md new file mode 100644 index 000000000000..63ec3505c10d --- /dev/null +++ b/skills/bootstrap-utility-api/SKILL.md @@ -0,0 +1,271 @@ +--- +name: bootstrap-utility-api +description: Use, extend, and customize Bootstrap 6's utility API. Use when adding a custom utility class, modifying or removing Bootstrap's default utilities, enabling responsive or state variants, or trimming the generated utility CSS. +guide: /utilities/api +--- + +# Bootstrap 6 utility API + +Every utility class Bootstrap ships is generated from one Sass map, `$utilities`. Each entry describes a family of classes — the property it sets, the values it accepts, and which variants to generate. Customizing utilities means editing that map, not writing CSS. + +## Workflow + +- [ ] Step 1: Understand how generation works +- [ ] Step 2: Read a utility definition +- [ ] Step 3: Add a utility +- [ ] Step 4: Modify, rename, or remove a default utility +- [ ] Step 5: Generate responsive, state, print, and dark variants +- [ ] Step 6: Compose values with custom properties and advanced selectors +- [ ] Step 7: Trim the generated CSS +- [ ] Step 8: Add a utility to Bootstrap itself +- [ ] Step 9: Verify + +--- + +## Step 1: Understand how generation works + +`scss/utilities/_api.scss` is the whole pipeline: + +```scss +@include generate-utility-at-properties($utilities); + +@layer utilities { + @include generate-utilities-loop($utilities, $breakpoints); +} +``` + +Three consequences worth knowing before you touch anything: + +- **Utilities are the last cascade layer**, so they win over component styles without needing `!important`. Only groups that opt in with `important: true` add it. +- **`@property` registration happens outside the layer.** Every custom property a utility assigns (`--bg`, `--fg`, `--bc`, `--rounded-size`, …) is registered with `syntax: "*"; inherits: false`, so applying `.border-inverse` doesn't leak its composed value into descendants. New utilities that assign custom properties get this automatically. +- **The map is configured on the `config` module, not `bootstrap`.** `$utilities` is declared in `scss/_config.scss` and only *populated* in `scss/_utilities.scss` (which loads config with `@use "config" as *`). Configuring the wrong module fails: + +```scss +// Does NOT work — "This variable was not declared with !default in the @used module." +@use "bootstrap/scss/bootstrap" with ( + $utilities: (…) +); + +// Works — configure config first, then load Bootstrap +@use "bootstrap/scss/config" with ( + $utilities: (…) +); +@use "bootstrap/scss/bootstrap"; +``` + +The same applies to any variable that lives in `_config.scss` (`$radius`, `$spacers`, `$breakpoints`, `$zindex-levels`). Variables declared in a partial that `bootstrap.scss` forwards — `$root-tokens`, `$colors`, `$blue` — can be configured on `bootstrap` directly. + +--- + +## Step 2: Read a utility definition + +A group is a map. The simplest form needs two keys: + +```scss +"overflow": ( + property: overflow, + values: auto hidden visible scroll, +), +``` + +The full option set, as validated by `generate-utility()`: + +| Key | Required | What it does | +| --- | --- | --- | +| `property` | Yes | Property name, space-separated list of names, or a map of property-to-value pairs | +| `values` | Yes | List (class suffix equals the value) or map (keys become suffixes; a `null` key omits the suffix) | +| `class` | No | Class name to use instead of the property name | +| `selector` | No | `class` (default), `attr-starts`, or `attr-includes` | +| `child-selector` | No | Descendant selector, wrapped in `:where()` for zero specificity | +| `variables` | No | Custom properties to emit — a map of static values, or a list that each receive the utility value | +| `state` | No | Pseudo-class variants to generate, e.g. `hover focus` | +| `responsive` | No | Generate a class per breakpoint (default `false`) | +| `print` | No | Generate `print:`-prefixed classes (default `false`) | +| `dark` | No | Generate `dark:`-prefixed classes inside `prefers-color-scheme: dark` (default `false`) | +| `important` | No | Append `!important` (default `false`) | +| `enabled` | No | Set `false` to suppress output while keeping the definition (default `true`) | + +The generator validates as it goes, which makes mistakes cheap to find: a missing `property` or `values` is a hard `@error`, a non-boolean `responsive` is a hard `@error`, and a misspelled key is a `@warn` listing the valid ones. + +--- + +## Step 3: Add a utility + +Your map is merged over Bootstrap's defaults, so pass only what's new: + +```scss +@use "bootstrap/scss/config" with ( + $utilities: ( + "cursor": ( + property: cursor, + class: cursor, + responsive: true, + values: auto pointer grab, + ), + ) +); +@use "bootstrap/scss/bootstrap"; +``` + +That yields `.cursor-auto`, `.cursor-pointer`, `.cursor-grab`, plus `.sm:cursor-*` through `.2xl:cursor-*`. + +Value shapes control the class names: + +```scss +// List — suffix is the value itself: .cursor-pointer +values: auto pointer grab, + +// Map — keys become suffixes: .tint-brand +values: (brand: var(--purple-500), muted: var(--gray-200)), + +// null key — no suffix at all: .shout +values: (null: uppercase), +``` + +--- + +## Step 4: Modify, rename, or remove a default utility + +The merge is **shallow**: supplying an existing group key replaces that entire group. Overriding one option and omitting the rest fails loudly — + +```scss +// Errors: "Utility is missing required `property` key" +"overflow": ( + responsive: true, + values: visible hidden scroll auto, +), +``` + +so copy the full definition from `scss/_utilities.scss` and add your change: + +```scss +@use "bootstrap/scss/config" with ( + $utilities: ( + // Make overflow responsive + "overflow": ( + property: overflow, + values: auto hidden visible scroll, + responsive: true, + ), + // Remove a family entirely + "width": null, + // Rename .ms-* to v4-style .ml-*: drop the original, add a replacement + "margin-start": null, + "margin-start-alias": ( + property: margin-inline-start, + class: ml, + responsive: true, + values: (0: 0, 1: var(--spacer-1), 2: var(--spacer-2), auto: auto), + ), + ) +); +@use "bootstrap/scss/bootstrap"; +``` + +Note the CSS tokens in that last group. The file configuring `config` can't read Bootstrap's Sass members — `$spacers`, `$negative-spacers`, and helpers like `map-merge-multiple()` aren't in scope yet — so restate values as CSS tokens (`var(--spacer-2)`, which tracks the spacer scale) or as literals. + +To *extend* an existing family without copying its definition, register a new group that reuses the same `class`: + +```scss +"width-extra": ( + property: width, + class: w, + responsive: true, + values: (10: 10%, 15: 15%), +), +``` + +This adds `.w-10` and `.w-15` next to Bootstrap's `.w-25` … `.w-100`, and is usually the better choice — it survives Bootstrap updates that change the original group. + +Prefer `enabled: false` over `null` when you want the definition to stay visible in the map (useful in a shared config where the group is documented but intentionally off). + +--- + +## Step 5: Generate responsive, state, print, and dark variants + +v6 uses **prefixes**, not infixes or suffixes. All four variant types compose with the base class name: + +| Option | Generated class | Notes | +| --- | --- | --- | +| `responsive: true` | `.md:cursor-pointer` | One pass per key in `$breakpoints`; written `.md\:` in the CSS | +| `state: hover focus` | `.hover:cursor-pointer` | Selector is `.hover\:cursor-pointer:hover` | +| `print: true` | `.print:d-none` | Wrapped in `@media print` | +| `dark: true` | `.dark:bg-black` | Wrapped in `@media (prefers-color-scheme: dark)` | + +`responsive` and `state` stack, producing `.md:hover:cursor-pointer`. Each variant multiplies the generated CSS, so enable them per group rather than globally. + +--- + +## Step 6: Compose values with custom properties and advanced selectors + +Most color-ish utilities don't set the property directly. They assign a local custom property and then consume it, which is what makes opacity and composition work: + +```scss +"tint": ( + property: ( + "--tint": null, + "background-color": var(--tint), + ), + class: tint, + values: (brand: var(--purple-500)), +), +``` + +A `null` value in the property map means "receive the utility's value"; every other entry is emitted as written. The above generates: + +```css +.tint-brand { + --tint: var(--purple-500); + background-color: var(--tint); +} +``` + +Related mechanics: + +- **`variables`** emits extra custom properties. As a map, each gets a static value on every class in the group; as a list, each receives the utility's value. +- **Property maps plus map values.** When `property` is a map and a `values` entry is itself a map keyed by property name, each property picks its own value — useful for a group that sets several related properties at different values. +- **`selector: attr-starts` / `attr-includes`** targets `[class^="…"]` / `[class*="…"]` instead of a class, and requires a `class` key. Bootstrap uses it so `.ratio-16x9` can feed `aspect-ratio` through `--ratio`. +- **`child-selector`** wraps the rule in `:where()`, e.g. `:where(.stack-gap-1 > *)`. Zero specificity means a child's own styles still win. + +Custom property names are written **unprefixed** in Sass. Bootstrap's dist and CDN CSS run PostCSS to add `--bs-`, so compiling the source yourself keeps them unprefixed. + +--- + +## Step 7: Trim the generated CSS + +Utilities are the biggest slice of Bootstrap's CSS, and the map is the cheapest place to cut: + +1. Start from the utilities-only entry point if you don't need components: + + ```scss + @use "bootstrap/scss/config" with ($utilities: (…)); + @use "bootstrap/scss/bootstrap-utilities"; + ``` + +2. Set unused groups to `null` (or `enabled: false`) rather than relying on the minifier or a purge step. +3. Turn `responsive` off on groups you only use at one breakpoint — a responsive group costs six copies of itself. +4. Narrow `values` maps instead of deleting whole groups when you only need a few steps. + +See [Optimize]([[docsref:/customize/optimize/]]) for the wider picture, including Lightning CSS and purging. + +--- + +## Step 8: Add a utility to Bootstrap itself + +When contributing a utility to the framework rather than a project: + +1. Add the group to the `$utilities` map in `scss/_utilities.scss`, near related groups, wrapped in `// scss-docs-start utils-` / `// scss-docs-end utils-` markers. +2. Prefer composing through a custom property when the utility should interact with opacity or theme colors, and reuse existing token values (`$spacers`, `$util-opacity`, `theme-color-refs()`) instead of literals. +3. Document it on the matching page in `site/src/content/docs/utilities/`, rendering the definition with ``, and add a `site/data/sidebar.yml` entry if the page is new. +4. Add coverage in `scss/tests/utilities/_api.test.scss`, which calls the generator mixins directly with a fixture map. +5. Run `npm run css`. Its `css-docs` step regenerates `dist/css/bootstrap-utilities.metadata.json`, which the docs read — commit it alongside your change. + +--- + +## Step 9: Verify + +1. `npm run css-lint`, then `npm run css`. +2. Grep the compiled CSS for the classes you expected, including the escaped variants (`\.md\\:cursor-pointer`), and confirm nothing you removed is still present. +3. Confirm new custom properties appear in the `@property` block at the top of the file — if one is missing, the utility isn't assigning it through `property` or `variables`. +4. Watch the Sass output for `@warn` lines about unknown keys; they're the fastest signal that an option was misspelled and silently ignored. +5. Check specificity in the browser: utilities should override component styles through the layer order alone, without `!important`. diff --git a/skills/bootstrap-v4-v6-migration/SKILL.md b/skills/bootstrap-v4-v6-migration/SKILL.md new file mode 100644 index 000000000000..56a43b90cf5c --- /dev/null +++ b/skills/bootstrap-v4-v6-migration/SKILL.md @@ -0,0 +1,227 @@ +--- +name: bootstrap-v4-v6-migration +description: Migrate a project from Bootstrap 4 to Bootstrap 6. Use when upgrading from Bootstrap 4, jumping two major versions, or updating v4 class names, data attributes, forms, and JavaScript across the v4-to-v5 and v5-to-v6 steps. +guide: /guides/migration +--- + +# Bootstrap v4 to v6 Migration + +There is no direct v4 to v6 path. Migrate in two stages — v4 to v5 first (this skill), then v5 to v6 (the `bootstrap-v5-v6-migration` skill). Skipping the middle stage doesn't work, because many v4 names were renamed in v5 and renamed *again* in v6: `.text-primary` becomes `.fg-primary` only after passing through v5, and `.badge-primary` becomes `.badge-subtle .theme-primary` by way of `.badge.bg-primary`. + +## Workflow + +Work through each step in order. After each step, search the codebase for remaining v4 patterns before moving on. + +- [ ] Step 1: Plan the two-stage migration +- [ ] Step 2: v4 to v5 — dependencies, build, and Sass +- [ ] Step 3: v4 to v5 — JavaScript +- [ ] Step 4: v4 to v5 — classes and attributes +- [ ] Step 5: v5 to v6 — hand off to the v6 migration skill +- [ ] Step 6: Verify + +--- + +## Step 1: Plan the two-stage migration + +1. Get the project onto Bootstrap 5 and **working** — build passing, pages rendering, tests green — before touching v6. Landing the v5 stage as its own commit (or PR) keeps the two rename waves separable when something breaks. +2. Inventory what you're dealing with first. The v4 features that cost the most time are custom forms, `.input-group-append` / `.input-group-prepend`, jumbotrons, `.media` objects, `.card-deck` / `.card-columns`, and any jQuery that calls Bootstrap plugins. +3. Decide about jQuery separately. Bootstrap 5 dropped it, but your app may use it for other things; removing Bootstrap's dependency on it does not require removing jQuery itself. +4. Note the browser floor. Bootstrap 5 drops IE, and Bootstrap 6 requires `oklch()` and `color-mix()` support. + +--- + +## Step 2: v4 to v5 — dependencies, build, and Sass + +1. Update the dependency and drop the jQuery/Popper v1 pair: + + ```sh + npm i --save bootstrap@5 @popperjs/core + npm uninstall popper.js + ``` + + `popper.js` (v1) is replaced by `@popperjs/core` (v2). Bootstrap 5's bundle includes Popper; the standalone `bootstrap.js` does not. + +2. Switch the Sass compiler to Dart Sass (`npm i --save-dev sass`). Node Sass / LibSass is not supported. Keep using `@import` for now — v5 still uses it; the move to `@use` happens in the v6 stage. + +3. Sass maps no longer merge automatically. In v4, adding one key to `$theme-colors` was enough; in v5 you must define the complete map, including the keys you're keeping. + +4. Update renamed functions and mixins: + + | v4 | v5 | + | --- | --- | + | `color-yiq()` | `color-contrast()` | + | `theme-color()`, `gray()` | Removed — reference `$theme-colors` / `$grays` directly | + | `theme-color-level()` | `tint-color()` / `shade-color()` | + | `lighten()` / `darken()` | `tint-color()` / `shade-color()` | + | `media-breakpoint-down(md)` | `media-breakpoint-down(lg)` — takes the breakpoint itself, not the next one down | + | `media-breakpoint-between(sm, md)` | `media-breakpoint-between(sm, lg)` — same shift on the upper bound | + + The breakpoint parameter change is silent: nothing errors, your media queries just apply one breakpoint off. Audit every call. + +5. Print styles, `$enable-*` flags, and the utility API all changed shape in v5. If you generated custom utilities with v4 mixins, rewrite them through v5's `$utilities` map. + +--- + +## Step 3: v4 to v5 — JavaScript + +1. Namespace every data attribute with `bs`. This is a mechanical but wide-reaching change: + + | v4 | v5 | + | --- | --- | + | `data-toggle` | `data-bs-toggle` | + | `data-target` | `data-bs-target` | + | `data-dismiss` | `data-bs-dismiss` | + | `data-parent` | `data-bs-parent` | + | `data-ride` | `data-bs-ride` | + | `data-slide`, `data-slide-to` | `data-bs-slide`, `data-bs-slide-to` | + | `data-spy` | `data-bs-spy` | + | `data-backdrop`, `data-keyboard` | `data-bs-backdrop`, `data-bs-keyboard` | + | `data-content`, `data-placement`, `data-trigger` | `data-bs-content`, `data-bs-placement`, `data-bs-trigger` | + + Any option passed as an attribute follows the same rule: `data-