You are working on CurlyFramework, a lightweight frontend framework for accessibility and sustainability, designed to work with CMS solutions like MODX and Kirby. It combines Tachyons (custom fork) utility-first CSS with Alpine.js reactive components. Published to npm as curlyframework.
Build Process
SCSS compilation and JS bundling are handled by CodeKit (macOS app). Additionally, npm scripts are available for SCSS compilation (via Dart Sass) and CSS post-processing (via Lightning CSS). Source files live in /dev, compiled output goes to /styleguide.
| Command | What it does |
|---|
npm run build | Full production build: compile + minify all CSS and JS in one step |
npm run scss | Compile SCSS only (no post-processing) |
npm run compile | Compile SCSS + Lightning CSS without minify (dev use) |
npm run compile:css | Run Lightning CSS only on .src.css files (no sass step) |
npm run lcss | Compile SCSS + Lightning CSS with minification (CSS production build) |
npm run bundle:js | Bundle dev/js/bundle.js with esbuild + minify with Terser (full CodeKit JS fallback) |
npm run minify:js | Minify already-bundled styleguide/js/script.js with Terser only |
npm run purge | Remove unused CSS from style.css based on *.html + JS content |
npm run dev | Watch CSS + JS + browser-sync hot reload (full dev workflow) |
npm run serve | Start browser-sync server only (no watching) |
npx prettier --write . | Format code |
npm install | Install dependencies |
Compiled files in /styleguide/css/ and /styleguide/js/ should be committed — they are the distributed assets.
CSS pipeline details
- Dart Sass compiles SCSS from
dev/css/ → styleguide/css/ with --no-source-map
- Lightning CSS post-processes in place with
--targets 'defaults' --minify
- Watch mode uses
& between commands to run all three watchers in parallel
When adding a new SCSS entry point
- Add the source file in
dev/css/
- Add it to all three scripts in
package.json: scss, lcss, and lcss:watch
- Add it to the Source → Output mapping in
CLAUDE.md
- Register it in
.config.codekit3 if CodeKit is also used
Architecture
Source → Output mapping:
dev/css/style.scss → styleguide/css/style.css (main stylesheet, ~165KB compiled)
dev/css/print.scss → styleguide/css/print.css
dev/css/modx.scss → styleguide/css/modx.css
dev/js/bundle.js → styleguide/js/script.js (Alpine.js + plugins, ~62KB compiled)
SCSS structure in dev/css/:
_vars.scss — CSS custom properties, breakpoints (small: 30em, medium: 48em, large: 60em), responsive scaling
_bundle.scss — imports Tachyons + all custom modules
_a11y.scss — skiplinks, reduced-motion, ARIA patterns
_mixins.scss — reusable SCSS helpers
_typo.scss — typography utilities (--typo-1 through --typo-5)
_form.scss, _animation.scss — component-specific modules
JS (dev/js/bundle.js): Initializes Alpine.js with plugins: @alpinejs/collapse, @alpinejs/focus, @alpinejs/intersect, @alpinejs/persist.
index.html (root) is both the primary documentation and the demo page.
Coding Conventions
- Indentation: 4 spaces for CSS, JS, JSON; 2 spaces for Markdown and YAML
- Quotes: Single quotes in JS/CSS
- Line endings: LF
- CSS naming: kebab-case with BEM patterns (
.component__element--modifier)
- SCSS partials: underscore prefix (
_filename.scss)
- JS: camelCase variables/functions, PascalCase classes
- CSS custom properties: kebab-case (
--animation-time-small)
- Commit messages: Conventional commits —
feat:, fix:, docs:, release:, chore:
Key Constraints
- Accessibility first: All components require proper ARIA attributes, keyboard navigation, and screen reader compatibility (WCAG 2.2).
- Sustainability: Minimize bundle size, prefer CSS-only solutions over JS when possible, avoid heavy dependencies.
- New dependencies must align with accessibility and sustainability goals — prefer lightweight, well-maintained packages.
- CSS classes: Only use classes from the compiled styleguide (
styleguide/css/). Do not use Tailwind, Bootstrap, or any other external CSS framework.
Tachyons CSS Reference
Tachyons is a functional/atomic CSS framework providing single-purpose utility classes. Classes are composed directly in HTML — no custom CSS needed for most styling. Mobile-first, ~14kB, optimized for gzip compression.
Naming Pattern
{property}{value} or {property}{direction}{value} — e.g. pa3 = padding-all 1rem, mt2 = margin-top 0.5rem, f4 = font-size 1.25rem.
Responsive Suffixes
| Suffix | Breakpoint |
|---|
| (none) | All (mobile-first base) |
-ns | ≥30em (not small) |
-m | 30em–60em (medium only) |
-l | ≥60em (large and up) |
Example: dn db-ns = hidden on mobile, block on medium+
Spacing Scale (8px baseline)
| Step | Value |
|---|
| 0 | 0 |
| 1 | 0.25rem (4px) |
| 2 | 0.5rem (8px) |
| 3 | 1rem (16px) |
| 4 | 2rem (32px) |
| 5 | 4rem (64px) |
| 6 | 8rem (128px) |
| 7 | 16rem (256px) |
Padding: pa, pl, pr, pt, pb, pv (vertical), ph (horizontal) + scale number
Margin: ma, ml, mr, mt, mb, mv, mh + scale number
Type Scale
| Class | Size (:root default) |
|---|
.f1 | 0.75rem |
.f2 | 0.875rem |
.f3 | 1rem |
.f4 | 1.25rem |
.f5 | 1.25rem |
.f6 | 1.5rem |
.f7 | 2rem |
Note: inside .curlyframework, --typo-1–--typo-5 are overridden to 1rem, 1.25rem, 1.5rem, 2rem, 4rem. The scale also extends to --typo-12 (12rem).
Width Scale
Fixed (powers of 2): w1–w5 (1rem–16rem)
Percentage: w-10, w-20, w-25, w-33, w-50, w-75, w-100, w-third, w-two-thirds, w-auto
Color System
Grayscale: black, near-black, dark-gray, mid-gray, gray, silver, light-silver, moon-gray, light-gray, near-white, white
Transparency: black-90 … black-05, white-90 … white-10
Colors: dark-red, red, orange, gold, yellow, purple, dark-pink, hot-pink, pink, dark-green, green, light-green, navy, dark-blue, blue, light-blue, and washed-* variants
- Text color: class name directly (e.g.
blue, dark-gray)
- Background:
bg- prefix (e.g. bg-blue)
- Border color:
b-- prefix (e.g. b--dark-gray)
Common Classes
Display: dn, di, db, dib, flex, inline-flex
Flexbox: flex-column, flex-row, flex-wrap, items-center, items-start, items-end, justify-center, justify-between, justify-around, flex-auto, flex-none
Position: static, relative, absolute, fixed, sticky + coordinates top-0, right-0, bottom-0, left-0
Border radius: br0–br4, br-100 (circle), br-pill
Directional: br--top, br--bottom, br--left, br--right
Borders: Width bw0–bw5 · Style b--solid, b--dashed, b--none · Sides ba, bt, br, bb, bl, bn
Typography: Weight fw1–fw9, b, normal · Alignment tl, tr, tc, tj · Transform ttu, ttl, ttc · Decoration underline, no-underline, strike · Line height lh-solid (1), lh-title (1.25), lh-copy (1.5)
Visibility: Opacity o-0–o-100 · v-hidden, v-visible, clip
Z-index: z-0–z-5, z-999, z-9999, z-max
Hovers: Prefix any color/bg class with hover- (e.g. hover-bg-blue, hover-black)
Component Patterns
html
1<!-- Skiplinks -->
2<ul class="skiplinks">
3 <li><a href="#main">main content</a></li>
4</ul>
5
6<!-- Button -->
7<button
8 @click="open = !open"
9 :aria-pressed="open"
10 class="pointer p1 ph4-m pv3-m bg-transparent ba b--inherit br-100 transition-small"
11>
12 <span class="clip">Label</span>
13</button>
14
15<!-- Button with visible text + icon -->
16<button
17 @click="open = true"
18 :aria-expanded="open"
19 aria-controls="popup"
20 class="btn pointer p0 bg-transparent bn"
21>
22 <span class="bb">open <span aria-hidden="true">⇢</span></span>
23</button>
24
25<!-- Modal (Alpine.js x-trap for focus) -->
26<div x-data="{ open: false }">
27 <button @click="open = true" :aria-expanded="open" aria-controls="popup" class="btn pointer p0 bg-transparent bn">
28 <span class="bb">open <span aria-hidden="true">⇢</span></span>
29 </button>
30 <div x-cloak x-transition x-show="open" @click="open = false" class="fixed top-0 left-0 right-0 bottom-0 bg-blur"></div>
31 <div
32 x-cloak x-transition x-trap.inert.noscroll="open" x-show="open"
33 @keyup.escape.window="open = false"
34 id="popup" aria-modal="true" role="dialog"
35 class="fixed top-0 right-0 bottom-0 z-max w-80 w-third-m p3 p5-m"
36 >
37 <button @click="open = false" class="btn pointer p0 bg-transparent bn">
38 <span class="bb">close <span aria-hidden="true">×</span></span>
39 </button>
40 </div>
41</div>
42
43<!-- Accordion (native details element) -->
44<details x-data="{ open: false }" @toggle="open = $el.open" class="w-100 mb1 ba">
45 <summary class="pointer flex justify-between w-100 p2 bg-transparent bn">
46 <span>Accordion title</span>
47 <span :class="{ 'rotate-180': open }" aria-hidden="true" class="transition-small">⇣</span>
48 </summary>
49 <p class="p2 m0 bt">Answer text.</p>
50</details>
51
52<!-- Tabs -->
53<div x-data="{ tabs: 3, selected: 1 }">
54 <div @keydown.right.prevent.stop="$focus.wrap().next()" @keydown.left.prevent.stop="$focus.wrap().prev()" role="tablist" class="list flex p0 m0">
55 <template x-for="i in tabs">
56 <button @click="selected = i" :id="`tab${i}`" :tabindex="selected == i ? 0 : -1"
57 :aria-selected="selected == i" :aria-controls="`tabcontent${i}`" role="tab" class="btn btn--clean">
58 Tab <span x-text="i"></span>
59 </button>
60 </template>
61 </div>
62 <template x-for="i in tabs">
63 <section x-show="selected == i" :id="`tabcontent${i}`" :aria-labelledby="`tab${i}`" role="tabpanel" class="p3">
64 Content <span x-text="i"></span>
65 </section>
66 </template>
67</div>
68
69<!-- Form field -->
70<div class="form__item">
71 <label for="name" class="form__label">
72 Name
73 <span aria-hidden="true">*</span>
74 <span class="clip">(required)</span>
75 </label>
76 <div class="form__field form__field--input">
77 <input type="text" name="name" id="name" required />
78 </div>
79</div>
80
81<!-- Checkbox -->
82<div class="form__item">
83 <div class="form__field form__field--checkbox">
84 <input type="checkbox" name="accept" id="accept" value="yes" required />
85 <label for="accept" class="form__label">
86 <span>I agree. <span aria-hidden="true">*</span><span class="clip">(required)</span></span>
87 </label>
88 </div>
89</div>
90
91<!-- Live region for form status -->
92<div x-ref="message" aria-live="assertive" role="status" class="form__message">
93 <p class="b p2 ba bw2 br2 mb3">Success</p>
94</div>
95
96<!-- Responsive grid -->
97<ul class="dg cols-2 cols-4-m g2 list p0 m0">
98 <template x-for="item in items">
99 <li class="ba br2 aspect-ratio-square">
100 <span class="flex justify-center items-center h-100"></span>
101 </li>
102 </template>
103</ul>
104
105<!-- Richtext content block -->
106<div class="richtext">
107 <h4>Headline</h4>
108 <p>A <strong>paragraph</strong> with a <a href="#">link</a>.</p>
109</div>