css-is-awesome 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [1.3.0](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.2.0...v1.3.0) (2026-09-04)
2
+
3
+
4
+ ### Features
5
+
6
+ * **nav:** cia ships mobile navigation - hamburger + drawer, zero JS ([d58f8f1](https://github.com/Jerry2d3d/css-is-awesome/commit/d58f8f15d877b974b978e5b2b3d35122e3f0da0f))
7
+
1
8
  # [1.2.0](https://github.com/Jerry2d3d/css-is-awesome/compare/v1.1.1...v1.2.0) (2026-09-04)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "css-is-awesome",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "A token-driven SCSS design system with light/dark theming, semantic color tokens, and a 800+ LOC mixin API.",
5
5
  "homepage": "https://github.com/Jerry2d3d/css-is-awesome#readme",
6
6
  "bugs": {
@@ -1,5 +1,5 @@
1
1
  // ============================================================================
2
- // NAVIGATION — Navbar, Nav, Breadcrumb, Tabs, Pagination
2
+ // NAVIGATION — Navbar, Nav, Breadcrumb, Tabs, Pagination, Hamburger, Drawer
3
3
  // ============================================================================
4
4
  @use 'sass:map';
5
5
  @use '../theme' as t;
@@ -184,3 +184,139 @@
184
184
 
185
185
  &:disabled, &[disabled] { @include m.disabled; }
186
186
  }
187
+
188
+ // ----------------------------------------------------------------------------
189
+ // HAMBURGER — three-bar menu icon button that morphs into an X when open.
190
+ // ----------------------------------------------------------------------------
191
+ // Zero JavaScript. Pair with a popover drawer: the browser manages
192
+ // aria-expanded on any <button popovertarget="..."> invoker, and the morph
193
+ // keys off that attribute (semantic state lives in ARIA, never a class).
194
+ //
195
+ // Usage (mixin — the primary API):
196
+ // .menu-button { @include hamburger; }
197
+ // <button class="menu-button" popovertarget="site-menu" aria-label="Menu">
198
+ // <span></span><span></span><span></span>
199
+ // </button>
200
+ //
201
+ // Checkbox fallback (no popover): apply the open-state morph yourself —
202
+ // .menu-check:checked + .menu-button { @include hamburger-open; }
203
+ //
204
+ // A11y: a real <button>, so focus/Enter/Space are native. Tap target
205
+ // defaults to 44px (WCAG 2.5.8 AAA; the 24px minimum is SC-level).
206
+
207
+ @mixin hamburger-open {
208
+ > span:nth-child(1) { translate: 0 var(--cia-bar-shift); rotate: 45deg; }
209
+ > span:nth-child(2) { opacity: 0; }
210
+ > span:nth-child(3) { translate: 0 calc(var(--cia-bar-shift) * -1); rotate: -45deg; }
211
+ }
212
+
213
+ @mixin hamburger(
214
+ $color: text-primary,
215
+ $bar-width: 1.375rem,
216
+ $bar-height: 2px,
217
+ $gap: 5px,
218
+ $target: 44px
219
+ ) {
220
+ @include m.button-reset;
221
+ // Distance each outer bar travels to meet the middle one for the X.
222
+ --cia-bar-shift: calc(#{$bar-height} + #{$gap});
223
+ cursor: pointer;
224
+ display: inline-flex;
225
+ flex-direction: column;
226
+ align-items: center;
227
+ justify-content: center;
228
+ gap: $gap;
229
+ min-width: var(--touch-target-min, #{$target});
230
+ min-height: var(--touch-target-min, #{$target});
231
+ border-radius: m.radius(sm);
232
+ // button-reset strips the outline; a keyboard user must still see focus.
233
+ @include m.focus-ring;
234
+
235
+ > span {
236
+ display: block;
237
+ width: $bar-width;
238
+ height: $bar-height;
239
+ border-radius: m.radius(full);
240
+ background: m.color($color);
241
+ @include m.transition(translate, rotate, opacity);
242
+ }
243
+
244
+ &[aria-expanded="true"] { @include hamburger-open; }
245
+ }
246
+
247
+ // ----------------------------------------------------------------------------
248
+ // DRAWER — slide-in navigation panel on the native Popover API.
249
+ // ----------------------------------------------------------------------------
250
+ // Zero JavaScript. The element carries the `popover` attribute; any
251
+ // <button popovertarget="..."> opens it. The browser gives light-dismiss
252
+ // (tap outside), Esc-to-close, top-layer stacking, and focus handling —
253
+ // no script tag, no hydration. Popover is Baseline 2024 (Chrome 114,
254
+ // Firefox 125, Safari 17). The slide-in animation rides @starting-style +
255
+ // allow-discrete and degrades to an instant open on older engines.
256
+ //
257
+ // Usage (mixin — the primary API):
258
+ // .site-drawer { @include drawer; } // slides from end
259
+ // .site-drawer { @include drawer($side: start); } // nav sidebar
260
+ // .site-drawer { @include drawer($side: top, $size: auto); } // menu sheet
261
+ //
262
+ // <nav id="site-menu" class="site-drawer" popover aria-label="Site menu">
263
+ // ...links...
264
+ // </nav>
265
+ //
266
+ // $side is logical (start/end follow reading direction), so RTL works free.
267
+
268
+ @mixin drawer(
269
+ $side: end, // start | end | top | bottom
270
+ $size: 20rem, // inline-size (start/end) or block-size (top/bottom)
271
+ $bg: surface-default,
272
+ $backdrop: rgba(0, 0, 0, 0.4),
273
+ $p: 5,
274
+ $shadow: lg
275
+ ) {
276
+ position: fixed;
277
+ margin: 0;
278
+ border: 0;
279
+ padding: m.space($p);
280
+ background: m.color($bg);
281
+ color: m.color(text-primary);
282
+ box-shadow: m.shadow($shadow);
283
+ overscroll-behavior: contain;
284
+ overflow: auto;
285
+
286
+ @if $side == start or $side == end {
287
+ inset-block: 0;
288
+ block-size: 100%;
289
+ inline-size: min(#{$size}, 85vw);
290
+ @if $side == start { inset-inline: 0 auto; --cia-drawer-slide: -100% 0; } @else { inset-inline: auto 0; --cia-drawer-slide: 100% 0; }
291
+ } @else {
292
+ inset-inline: 0;
293
+ inline-size: 100%;
294
+ block-size: $size;
295
+ max-block-size: 85dvh;
296
+ @if $side == top { inset-block: 0 auto; --cia-drawer-slide: 0 -100%; } @else { inset-block: auto 0; --cia-drawer-slide: 0 100%; }
297
+ }
298
+
299
+ // Closed state (display:none via popover) exits with a slide; entry
300
+ // animates from @starting-style. allow-discrete lets `display` wait for
301
+ // the transition — engines without it simply open/close instantly.
302
+ translate: var(--cia-drawer-slide);
303
+ transition:
304
+ translate 240ms ease,
305
+ display 240ms ease allow-discrete,
306
+ overlay 240ms ease allow-discrete;
307
+
308
+ &:popover-open {
309
+ translate: 0 0;
310
+ @starting-style {
311
+ translate: var(--cia-drawer-slide);
312
+ }
313
+ }
314
+
315
+ &::backdrop {
316
+ background: $backdrop;
317
+ }
318
+
319
+ @media (prefers-reduced-motion: reduce) {
320
+ transition: none;
321
+ }
322
+ }
@@ -0,0 +1,231 @@
1
+ ---
2
+ name: mobile-nav
3
+ description: Hamburger button + slide-in drawer for mobile navigation — zero JavaScript, built on the native Popover API.
4
+ category: navigation
5
+ complexity: simple
6
+ cia-version: ">=1.3.0"
7
+ ---
8
+
9
+ ## Use this when
10
+
11
+ Your top navigation has more links than a phone screen can hold and you want
12
+ the classic hamburger-opens-a-drawer pattern without shipping any JavaScript.
13
+ This recipe is for **site/app navigation**; for content that overlays mid-page
14
+ (confirmations, forms), use the `dialog` recipe instead.
15
+
16
+ ## Structure (raw HTML)
17
+
18
+ Two pieces: a real `<button>` that points at the drawer via `popovertarget`,
19
+ and the drawer itself — any element carrying the `popover` attribute.
20
+
21
+ ```html
22
+ <header class="site-bar" data-cia-recipe="mobile-nav">
23
+ <a href="/" data-slot="brand">MySite</a>
24
+
25
+ <!-- Only visible at mobile widths (see Styling). The browser keeps
26
+ aria-expanded on this button in sync with the popover — for free. -->
27
+ <button class="menu-button" popovertarget="site-menu" aria-label="Menu">
28
+ <span></span><span></span><span></span>
29
+ </button>
30
+
31
+ <!-- Desktop inline nav AND the drawer content are the same list. -->
32
+ <nav id="site-menu" class="site-menu" popover aria-label="Site menu">
33
+ <a href="/">Home</a>
34
+ <a href="/about">About</a>
35
+ <a href="/services">Services</a>
36
+ <a href="/contact">Contact</a>
37
+ </nav>
38
+ </header>
39
+ ```
40
+
41
+ ## Styling (cia mixins)
42
+
43
+ ```scss
44
+ // SiteBar.module.scss — component stylesheet, so import the zero-emit barrel.
45
+ @use 'css-is-awesome/api' as cia;
46
+
47
+ .site-bar {
48
+ @include cia.navbar-base;
49
+ }
50
+
51
+ .menu-button {
52
+ display: none; // desktop: the inline nav is visible, no burger
53
+
54
+ @include cia.media-down(md) {
55
+ @include cia.hamburger; // three bars; morphs to an X while expanded
56
+ }
57
+ }
58
+
59
+ .site-menu {
60
+ // Desktop: a plain inline nav. `popover` makes it display:none by
61
+ // default, so opt it back in above the breakpoint.
62
+ @include cia.media(md) {
63
+ display: flex;
64
+ gap: cia.space(5);
65
+ }
66
+
67
+ // Mobile: the same element becomes a slide-in drawer.
68
+ @include cia.media-down(md) {
69
+ @include cia.drawer($side: end, $size: 18rem);
70
+ display: flex;
71
+ flex-direction: column;
72
+ gap: cia.space(2);
73
+ }
74
+ }
75
+ ```
76
+
77
+ Pick the drawer's edge with `$side` (`start`/`end`/`top`/`bottom` — logical,
78
+ so RTL pages mirror automatically). `$side: top, $size: auto` gives the
79
+ drop-down-sheet look instead of a side panel.
80
+
81
+ ## Interactivity
82
+
83
+ **There is zero JavaScript, and nothing to wire up.** The Popover API does
84
+ all of it natively:
85
+
86
+ - Tapping the button opens/closes the drawer (`popovertarget` toggles).
87
+ - The browser maintains `aria-expanded` on the button — which is exactly what
88
+ `cia.hamburger` keys the bars-to-X morph off.
89
+ - **Esc closes. Tapping outside closes** (light dismiss). The drawer sits in
90
+ the top layer above everything, no z-index management.
91
+ - The slide-in animation runs on `@starting-style` + `allow-discrete`
92
+ transitions; engines that predate those simply open instantly.
93
+
94
+ Popover is Baseline 2024 (Chrome 114, Firefox 125, Safari 17). If you must
95
+ support older engines, see the checkbox variant below.
96
+
97
+ ## A11y checklist
98
+
99
+ - [ ] The trigger is a real `<button>` with an accessible name
100
+ (`aria-label="Menu"` or visible text) — never a bare `<label>` or `<div>`
101
+ ([WCAG 4.1.2 Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html))
102
+ - [ ] `aria-expanded` reflects open state — the browser does this for
103
+ `popovertarget` invokers; verify with an inspector
104
+ ([WAI-ARIA APG: Disclosure pattern](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/))
105
+ - [ ] The drawer element is a `<nav>` with `aria-label` so it's discoverable
106
+ as a landmark
107
+ ([WAI-ARIA APG: Landmark regions](https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/))
108
+ - [ ] Esc closes the drawer and focus returns to the button (native popover
109
+ behavior — verify if you've added scripts elsewhere)
110
+ ([WAI-ARIA APG: Disclosure navigation example](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/))
111
+ - [ ] Focus is visible on the button (`cia.hamburger` routes `focus-ring`)
112
+ and on every link inside the drawer
113
+ ([WCAG 2.4.7 Focus Visible](https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html))
114
+ - [ ] Tap target ≥ 24px minimum; `cia.hamburger` defaults to 44px
115
+ ([WCAG 2.5.8 Target Size (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html))
116
+ - [ ] Reduced motion honored — both mixins disable their transitions under
117
+ `prefers-reduced-motion`, baked in
118
+ ([WCAG 2.3.3 Animation from Interactions](https://www.w3.org/WAI/WCAG22/Understanding/animation-from-interactions.html))
119
+
120
+ ## Framework examples
121
+
122
+ ### React
123
+
124
+ ```jsx
125
+ export function SiteBar() {
126
+ return (
127
+ <header className={styles.siteBar}>
128
+ <a href="/">MySite</a>
129
+ <button className={styles.menuButton} popoverTarget="site-menu" aria-label="Menu">
130
+ <span /><span /><span />
131
+ </button>
132
+ <nav id="site-menu" className={styles.siteMenu} popover="auto" aria-label="Site menu">
133
+ <a href="/">Home</a>
134
+ <a href="/about">About</a>
135
+ </nav>
136
+ </header>
137
+ );
138
+ }
139
+ // React 19 forwards `popover` / `popoverTarget` as attributes — still no JS of yours.
140
+ ```
141
+
142
+ ### Vue
143
+
144
+ ```vue
145
+ <template>
146
+ <header class="site-bar">
147
+ <a href="/">MySite</a>
148
+ <button class="menu-button" popovertarget="site-menu" aria-label="Menu">
149
+ <span></span><span></span><span></span>
150
+ </button>
151
+ <nav id="site-menu" class="site-menu" popover aria-label="Site menu">
152
+ <a href="/">Home</a>
153
+ <a href="/about">About</a>
154
+ </nav>
155
+ </header>
156
+ </template>
157
+ ```
158
+
159
+ ### Svelte
160
+
161
+ ```svelte
162
+ <header class="site-bar">
163
+ <a href="/">MySite</a>
164
+ <button class="menu-button" popovertarget="site-menu" aria-label="Menu">
165
+ <span></span><span></span><span></span>
166
+ </button>
167
+ <nav id="site-menu" class="site-menu" popover aria-label="Site menu">
168
+ <a href="/">Home</a>
169
+ <a href="/about">About</a>
170
+ </nav>
171
+ </header>
172
+ ```
173
+
174
+ ### Vanilla (Web Component)
175
+
176
+ ```html
177
+ <!-- No component needed — the pattern IS vanilla HTML. Drop the Structure
178
+ markup in as-is; the popover attributes are the whole mechanism. -->
179
+ <script type="module">
180
+ // Intentionally empty. If you later want to close the drawer on in-page
181
+ // link clicks (same-page anchors don't navigate), this one line is the
182
+ // only JS the pattern can ever need:
183
+ // document.querySelectorAll('#site-menu a').forEach(a =>
184
+ // a.addEventListener('click', () => document.getElementById('site-menu').hidePopover()));
185
+ </script>
186
+ ```
187
+
188
+ ## Variants
189
+
190
+ **Checkbox fallback (pre-popover engines).** The classic hidden-checkbox hack
191
+ — works everywhere, at the cost of weaker semantics (a `<label>` isn't a
192
+ button, there's no Esc-close, and you must manage `aria-expanded` yourself or
193
+ accept its absence):
194
+
195
+ ```html
196
+ <input type="checkbox" id="menu-toggle" class="menu-check" aria-hidden="true">
197
+ <label for="menu-toggle" class="menu-button" aria-label="Menu">
198
+ <span></span><span></span><span></span>
199
+ </label>
200
+ <nav class="site-menu" aria-label="Site menu">…</nav>
201
+ ```
202
+
203
+ ```scss
204
+ .menu-check { display: none; }
205
+ .menu-button { @include cia.hamburger; }
206
+ .menu-check:checked + .menu-button { @include cia.hamburger-open; }
207
+ .site-menu { display: none; }
208
+ .menu-check:checked ~ .site-menu { display: flex; }
209
+ ```
210
+
211
+ `cia.hamburger-open` exists exactly for this: it is the bars-to-X morph as a
212
+ standalone mixin, so state can come from `:checked` instead of
213
+ `[aria-expanded]`.
214
+
215
+ ## Pitfalls
216
+
217
+ - **`popover` hides the element everywhere**, including desktop — that's why
218
+ the Styling section explicitly restores `display: flex` above the
219
+ breakpoint. Forgetting this makes the desktop nav vanish.
220
+ - **Don't add `open`/`is-open` classes.** State lives in `[aria-expanded]`
221
+ and `:popover-open`; a class-based copy will drift.
222
+ - **Don't wrap the button in the popover element** — the invoker must live
223
+ outside the drawer or it disappears with it.
224
+ - The drawer is `position: fixed` in the top layer; giving it a `z-index` or
225
+ a positioned ancestor does nothing (and confuses readers).
226
+
227
+ ## Related recipes
228
+
229
+ - `dialog` — modal overlays for content (native `<dialog>`, same zero-JS
230
+ philosophy).
231
+ - `combobox` — another browser-native disclosure pattern.