bootstrap-component
Builds Bootstrap 6 components with CSS and HTML code for web pages.
Installation
Paste this into Claude Code, Cursor, or any agent that can run commands.
SKILL.mdShow the author's original SKILL.md
---
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/_<name>.scss`, the base class `.<name>`, the token prefix `--<name>-*`, and the map `$<name>-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/_<name>.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 `<ScssDocs>`, 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: () !default;
$widget-variants: defaults(
(
"subtle": (
"color": "fg",
"bg": "bg-subtle",
"border-color": "transparent"
),
"outline": (
"color": "fg",
"bg": "transparent",
"border-color": "border"
)
),
$widget-variants
);
// 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-<size>-*` 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
<div class="widget theme-danger">…</div>
```
Use `.theme-reset` on a nested subtree to drop inherited `--theme-*` tokens and return to those fallbacks.
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/<name>.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 `<Example>` for live demos, and render the token map under a `### Variables` section:
```markdown
<CSSVariables component="Widgets" className="widget" />
<ScssDocs name="widget-tokens" file="scss/_widget.scss" />
```
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 `<html>` (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.
Mirrored from the author's public source. Install counts from the open skills registry.