Skip to content

Commit a2e0721

Browse files
committed
docs: document @scope-based .no-nimble opt-out feature
Update spec, README, and migration guide for the global-vs-scopeable architecture added in v0.7.0. Add Bangtastic migration log entry.
1 parent dbaf2ec commit a2e0721

3 files changed

Lines changed: 223 additions & 48 deletions

File tree

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,33 @@ Content is centered at `720px` by default — no class needed. These opt-in util
104104
| `.visually-hidden` | Hidden visually, accessible to screen readers |
105105
| `.overflow-auto` | Scrollable container |
106106

107+
## Third-Party Component Isolation
108+
109+
nimble.css styles can conflict with third-party components (datatables, rich text editors, etc.) that expect unstyled elements. Add `.no-nimble` to opt out of nimble's component styles inside a subtree:
110+
111+
```html
112+
<main class="fluid full-bleed">
113+
<h1>Styled by nimble</h1>
114+
115+
<!-- Third-party component: nimble styles don't apply inside -->
116+
<div class="no-nimble full-bleed">
117+
<ThirdPartyDataTable />
118+
</div>
119+
</main>
120+
```
121+
122+
**What's excluded:** Typography, links, buttons, forms, tables, code, media, article, details, dialog, and non-layout utilities.
123+
124+
**What still applies:** Reset, colors/custom properties, body grid, layout utilities (`.fluid`, `.full-bleed`, `.wide`, `.container`), and print styles. This means layout classes work on `.no-nimble` elements.
125+
126+
This works via CSS `@scope` (Chrome 118+, Safari 17.4+, Firefox 128+). To disable scoping entirely (smaller output, no opt-out):
127+
128+
```scss
129+
@use '@leftium/nimble.css/scss' with (
130+
$exclude-selector: null
131+
);
132+
```
133+
107134
## Customization
108135

109136
### CSS Custom Properties
@@ -160,6 +187,9 @@ Build a CSS file with new defaults. SCSS-unique options listed first; the rest m
160187
$enable-switch: true,
161188
$enable-details: true,
162189

190+
// Scoping (set to null to disable @scope wrapping)
191+
$exclude-selector: '.no-nimble',
192+
163193
// Surface fine-tuning
164194
$surface-chroma: 0.002,
165195
$surface-light-base: 0.985, // light mode base lightness

specs/nimble-css.md

Lines changed: 137 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,39 @@ nimble.css is authored in **SCSS** and compiled to pure CSS for distribution. Th
129129

130130
**Users never need to touch SCSS** unless they want build-time customization. The prebuilt `dist/nimble.css` is plain CSS that works everywhere.
131131

132-
### 3.5 Base Reset
132+
### 3.5 Scoping Architecture: Global vs Scopeable
133+
134+
nimble.css splits its styles into two categories:
135+
136+
**Global styles** — always apply regardless of scoping:
137+
- Reset (`_reset.scss`)
138+
- Colors/custom properties (`_colors.scss`)
139+
- Document/body grid (`_document.scss`)
140+
- Grid column assignment (`_grid-columns.scss`)
141+
- Layout utilities (`_layout-utilities.scss`) — `.fluid`, `.full-bleed`, `.wide`, `.container`
142+
- Print styles (`_print.scss`)
143+
144+
**Scopeable styles** — wrapped in `@scope (:root) to (.no-nimble)` by default:
145+
- Typography, links, buttons, forms, tables, code, media, article, details, dialog
146+
- Non-layout utilities (`.striped`, `.visually-hidden`, `.overflow-auto`)
147+
148+
The `_scopeable.scss` module uses `meta.load-css()` to load all scopeable partials, allowing the entry point (`nimble.scss`) to conditionally wrap them in `@scope`:
149+
150+
```scss
151+
@if $exclude-selector {
152+
@scope (:root) to (#{$exclude-selector}) {
153+
@include scopeable.load;
154+
}
155+
} @else {
156+
@include scopeable.load;
157+
}
158+
```
159+
160+
Layout utilities are intentionally global because they interact with the body grid (e.g., `.full-bleed` sets `grid-column: 1 / -1`). An element with `class="no-nimble full-bleed"` should still participate in the body grid layout even though nimble's component styles (typography, forms, etc.) don't apply inside it.
161+
162+
See [Section 15.2](#152-third-party-component-isolation-no-nimble-opt-out) for usage details and design rationale.
163+
164+
### 3.6 Base Reset
133165

134166
nimble.css embeds a trimmed version of **sanitize.css** as its reset foundation because:
135167

@@ -863,9 +895,13 @@ Basic print rules: force black-on-white, show link URLs, prevent orphaned headin
863895

864896
## 10. Utility Classes
865897

866-
nimble.css includes a **minimal** set of utility classes. These are the only classes nimble.css provides:
898+
nimble.css includes a **minimal** set of utility classes. These are the only classes nimble.css provides.
899+
900+
Utilities are split into **global** (always apply, including on `.no-nimble` elements) and **scoped** (excluded inside `.no-nimble` subtrees). See [Section 3.5](#35-scoping-architecture-global-vs-scopeable) for the architectural rationale.
867901

868-
### 10.1 Layout
902+
### 10.1 Layout (Global)
903+
904+
These interact with the body grid and must work everywhere, including on `.no-nimble` elements:
869905

870906
```css
871907
.container /* centered content width (useful inside fluid layout) */
@@ -874,27 +910,33 @@ nimble.css includes a **minimal** set of utility classes. These are the only cla
874910
.wide /* break out to 1200px max-width */
875911
```
876912

877-
### 10.2 Buttons
913+
### 10.2 Buttons (Scoped)
878914

879915
```css
880916
.secondary /* secondary button style (uses --nc-secondary) */
881917
.outline /* outline button style */
882918
```
883919

884-
### 10.3 Tables
920+
### 10.3 Tables (Scoped)
885921

886922
```css
887923
.striped /* striped table rows */
888924
.overflow-auto /* scrollable container */
889925
```
890926

891-
### 10.4 Visibility
927+
### 10.4 Visibility (Scoped)
892928

893929
```css
894930
.visually-hidden /* accessible hidden (screen readers only) */
895931
```
896932

897-
**Total class count: ~8.**
933+
### 10.5 Component Isolation
934+
935+
```css
936+
.no-nimble /* opt out of nimble's component styles (see §15.2) */
937+
```
938+
939+
**Total class count: ~9** (~8 utilities + `.no-nimble` opt-out).
898940

899941
## 11. Breakpoints
900942

@@ -923,34 +965,40 @@ That's it. The grid-based centered layout is inherently responsive without break
923965
```
924966
nimble.css/
925967
src/
926-
_config.scss # All configurable variables with !default
927-
_layers.scss # @layer order declaration (must precede all other output)
928-
_reset.scss # Trimmed sanitize.css
929-
_colors.scss # Color properties (oklch generation + light-dark())
930-
_document.scss # html, body, *, ::selection
931-
_typography.scss # headings, p, lists, blockquote, hr, mark, etc.
932-
_links.scss # a
933-
_buttons.scss # button, [role="button"], button groups
934-
_forms.scss # input, select, textarea, label, fieldset, switch
935-
_tables.scss # table, th, td
936-
_code.scss # pre, code, kbd, samp
937-
_media.scss # img, video, figure, figcaption, iframe
938-
_details.scss # details, summary
939-
_dialog.scss # dialog
940-
_print.scss # @media print rules
941-
_utilities.scss # utility classes
942-
nimble.scss # entry point (@use all above, wraps in @layer)
968+
_config.scss # All configurable variables with !default (incl. $exclude-selector)
969+
_layers.scss # @layer order declaration (must precede all other output)
970+
_reset.scss # Trimmed sanitize.css
971+
_colors.scss # Color properties (oklch generation + light-dark())
972+
_document.scss # html, body, *, ::selection
973+
_grid-columns.scss # Global: body grid column assignment (body > *)
974+
_layout-utilities.scss # Global: .fluid, .full-bleed, .wide, .container
975+
_scopeable.scss # Mixin loading scopeable modules via meta.load-css()
976+
_typography.scss # Scopeable: headings, p, lists, blockquote, hr, mark
977+
_links.scss # Scopeable: a
978+
_buttons.scss # Scopeable: button, [role="button"], button groups
979+
_forms.scss # Scopeable: input, select, textarea, label, fieldset, switch
980+
_tables.scss # Scopeable: table, th, td
981+
_code.scss # Scopeable: pre, code, kbd, samp
982+
_media.scss # Scopeable: img, video, figure, figcaption, iframe
983+
_article.scss # Scopeable: article card styling
984+
_details.scss # Scopeable: details, summary
985+
_dialog.scss # Scopeable: dialog
986+
_print.scss # Global: @media print rules
987+
_utilities.scss # Scopeable: non-layout utilities (.striped, .visually-hidden, etc.)
988+
nimble.scss # Entry point: global + conditional @scope wrapper
989+
nimble-core.scss # Core entry point (without progress/meter/select)
943990
dist/
944-
nimble.css # full build (generated, not committed)
945-
nimble.min.css # minified (generated, not committed)
946-
nimble-base.css # reset + base (no utilities)
947-
nimble-reset.css # just the reset
991+
nimble.css # full build (generated, not committed)
992+
nimble.min.css # minified (generated, not committed)
993+
nimble-base.css # reset + base (no utilities)
994+
nimble-reset.css # just the reset
948995
demo/
949-
index.html # html5-test-page based demo (vanilla HTML)
950-
extended.html # extended demo with form patterns, card layouts, etc.
996+
index.html # html5-test-page based demo (vanilla HTML)
997+
extended.html # extended demo with form patterns, card layouts, etc.
951998
specs/
952-
nimble-css.md # this spec
953-
build.js # Build script (Sass compile + Lightning CSS minify)
999+
nimble-css.md # this spec
1000+
pico-migration.md # Pico CSS → nimble.css migration guide
1001+
build.js # Build script (Sass compile + Lightning CSS minify)
9541002
package.json
9551003
LICENSE
9561004
README.md
@@ -989,6 +1037,11 @@ $enable-dialog: true !default;
9891037
$enable-switch: true !default;
9901038
$enable-details: true !default;
9911039
1040+
// --- Scoping ---
1041+
// Component styles wrapped in @scope (:root) to ($exclude-selector).
1042+
// Set to null to disable (all styles apply globally).
1043+
$exclude-selector: '.no-nimble' !default;
1044+
9921045
// --- Colors (oklch parameters) ---
9931046
$primary-hue: 250 !default;
9941047
$primary-chroma: 0.2 !default;
@@ -1024,9 +1077,10 @@ $breakpoint-phone: 720px !default;
10241077
// User's project stylesheet
10251078
@use 'nimble' with (
10261079
$prefix: '--my-',
1027-
$primary-hue: 25, // orange
1028-
$surface-hue: 30, // warm sand neutrals
1029-
$enable-dialog: false, // exclude dialog styles
1080+
$primary-hue: 25, // orange
1081+
$surface-hue: 30, // warm sand neutrals
1082+
$enable-dialog: false, // exclude dialog styles
1083+
$exclude-selector: null, // disable @scope wrapping (no .no-nimble opt-out)
10301084
$content-width: 800px,
10311085
);
10321086
```
@@ -1128,22 +1182,61 @@ Lessons from MVP.css, new.css, and HN discussions:
11281182
- **Pure classless is insufficient** for real-world use. You need at least: a way to distinguish primary/secondary buttons, striped tables, layout modes, and full-bleed content.
11291183
- **Minimum viable classes**: nimble.css uses ~8 classes total. Every class has a clear, non-overlapping purpose.
11301184

1131-
### 15.2 Third-Party Component Isolation
1185+
### 15.2 Third-Party Component Isolation (`.no-nimble` Opt-Out)
11321186

11331187
CSS cascade layers solve most specificity conflicts with third-party components (Svelte scoped styles, web components, etc.) because unlayered styles always beat layered styles. However, nimble.css's element styles can still "fill in" CSS properties that a component never explicitly sets, subtly changing its appearance.
11341188

1135-
For the rare cases where this matters, use `revert-layer` to undo nimble's styles inside a component wrapper:
1189+
nimble.css provides a built-in opt-out mechanism using CSS `@scope`. Component-level styles (typography, links, buttons, forms, tables, code, media, article, details, dialog, and non-layout utilities) are wrapped in:
11361190

11371191
```css
1138-
/* Reset nimble styles inside a specific component */
1139-
.datatable-wrapper th,
1140-
.datatable-wrapper td,
1141-
.datatable-wrapper input {
1142-
all: revert-layer;
1192+
@scope (:root) to (.no-nimble) {
1193+
/* component styles */
11431194
}
11441195
```
11451196

1146-
`revert-layer` (supported in all browsers that support `@layer`) reverts properties to the value they'd have without the current layer. This is documented as a recommended pattern, not built into nimble.css itself.
1197+
This means nimble's component styles apply everywhere **except** inside elements with `class="no-nimble"`. Document-level styles (reset, colors, body grid, layout utilities, print) remain global — they always apply.
1198+
1199+
**Usage:**
1200+
1201+
```html
1202+
<!-- nimble styles apply here -->
1203+
<main class="fluid full-bleed">
1204+
<h1>Styled by nimble</h1>
1205+
1206+
<!-- nimble component styles do NOT apply inside this element -->
1207+
<div class="no-nimble full-bleed">
1208+
<ThirdPartyDataTable />
1209+
</div>
1210+
</main>
1211+
```
1212+
1213+
Note that layout utilities (`.fluid`, `.full-bleed`, `.wide`, `.container`) are global, so they work on `.no-nimble` elements — you can still control layout while opting out of nimble's component styling.
1214+
1215+
**SCSS configuration:**
1216+
1217+
The exclude selector is configurable via `$exclude-selector`:
1218+
1219+
```scss
1220+
@use '@leftium/nimble.css/scss' with (
1221+
$exclude-selector: '.no-nimble' // default — @scope wraps component styles
1222+
);
1223+
```
1224+
1225+
Set to `null` to disable scoping entirely (all styles apply globally, no `@scope` wrapper emitted):
1226+
1227+
```scss
1228+
@use '@leftium/nimble.css/scss' with (
1229+
$exclude-selector: null // no @scope — smaller output, no opt-out
1230+
);
1231+
```
1232+
1233+
**Size overhead:** The `@scope` wrapper adds ~1.7 KB (~6%) to the minified output. After gzip/brotli compression, the overhead is negligible.
1234+
1235+
**Browser support:** `@scope` is supported in Chrome 118+, Safari 17.4+, and Firefox 128+ — the same modern browser targets nimble.css already requires for `light-dark()` and oklch.
1236+
1237+
**Why not `revert-layer`?** Early prototyping used `all: revert-layer` on wrapper elements, but this approach was either too aggressive (broke third-party component layout by reverting grid/flex properties) or lost specificity battles with scoped component styles. `@scope` cleanly prevents nimble's styles from entering the subtree at all.
1238+
1239+
**Why not opt-in (Pico's `$parent-selector` approach)?** Pico CSS supports `$parent-selector: '.pico'` so styles only apply inside a class. nimble.css's body grid requires rules on `body` itself, which can't be nested inside a class. Opt-out (default-on with `.no-nimble` escape hatch) avoids this architectural conflict.
11471240

11481241
### 15.3 Why SCSS?
11491242

@@ -1343,6 +1436,7 @@ Utilities, extended demo, and final validation.
13431436
- Refactor (date/time dedup, extract progress/meter/select to add-on sub-bundles): `nimble.css` 24,963 B / `nimble.min.css` 19,355 B / gzipped ~4.4 KB / brotli ~3.9 KB
13441437
- Flip model (nimble = core without extras, nimble-full = everything): `nimble.min.css` 15,796 B / gzipped ~3.8 KB / brotli ~3.3 KB
13451438
- Fix progress by excluding from reset `background-repeat` rule; progress add-on fully opt-in: `nimble.min.css` 15,809 B / gzipped ~3.8 KB / brotli ~3.3 KB
1439+
- Add `@scope`-based `.no-nimble` opt-out: split SCSS into global vs scopeable modules; component styles wrapped in `@scope (:root) to (.no-nimble)`; layout utilities kept global: `nimble.min.css` 22,655 B / `nimble-core.min.css` 18,978 B
13461440

13471441
---
13481442

0 commit comments

Comments
 (0)