ply-css 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/CLAUDE.md +176 -0
- package/LICENSE +22 -0
- package/PLY.md +646 -0
- package/README.md +170 -0
- package/dist/css/ply-core.css +6175 -0
- package/dist/css/ply-core.min.css +1 -0
- package/dist/css/ply-essentials.min.css +1 -0
- package/dist/css/ply-helpers.min.css +1 -0
- package/dist/css/ply.css +7429 -0
- package/dist/css/ply.min.css +1 -0
- package/dist/css/styles.css +7432 -0
- package/dist/css/styles.min.css +1 -0
- package/llms-full.txt +834 -0
- package/llms.txt +34 -0
- package/package.json +70 -0
- package/ply-classes.json +2625 -0
- package/snippets/accessible-drag-and-drop.html +122 -0
- package/snippets/card.html +58 -0
- package/snippets/contact-form.html +49 -0
- package/snippets/custom-theme.html +280 -0
- package/snippets/dashboard.html +77 -0
- package/snippets/data-table.html +64 -0
- package/snippets/login-page.html +45 -0
- package/snippets/navbar-page.html +39 -0
- package/snippets/notifications.html +63 -0
- package/snippets/pricing-cards.html +95 -0
- package/snippets/responsive-header.html +98 -0
- package/snippets/starter-page.html +782 -0
- package/snippets/two-column-layout.html +40 -0
- package/src/scss/_ply-core-components.scss +32 -0
- package/src/scss/_ply.scss +47 -0
- package/src/scss/components/_accordion.scss +73 -0
- package/src/scss/components/_alignments.scss +64 -0
- package/src/scss/components/_autocomplete.scss +28 -0
- package/src/scss/components/_blocks-responsive.scss +30 -0
- package/src/scss/components/_blocks.scss +39 -0
- package/src/scss/components/_buttons.scss +452 -0
- package/src/scss/components/_colors.scss +447 -0
- package/src/scss/components/_container-queries.scss +35 -0
- package/src/scss/components/_cursors.scss +24 -0
- package/src/scss/components/_dialog-patterns.scss +176 -0
- package/src/scss/components/_dropdown.scss +68 -0
- package/src/scss/components/_filterbox.scss +57 -0
- package/src/scss/components/_flexible-embed.scss +19 -0
- package/src/scss/components/_forms.scss +450 -0
- package/src/scss/components/_grid.scss +210 -0
- package/src/scss/components/_helpers-core.scss +357 -0
- package/src/scss/components/_helpers.scss +466 -0
- package/src/scss/components/_labels.scss +105 -0
- package/src/scss/components/_livesearch.scss +233 -0
- package/src/scss/components/_loader.scss +24 -0
- package/src/scss/components/_media-queries.scss +9 -0
- package/src/scss/components/_mixins.scss +387 -0
- package/src/scss/components/_modal.scss +73 -0
- package/src/scss/components/_multi-step-form.scss +190 -0
- package/src/scss/components/_navigation-responsive.scss +63 -0
- package/src/scss/components/_navigation.scss +592 -0
- package/src/scss/components/_notifications.scss +185 -0
- package/src/scss/components/_prettyprint.scss +86 -0
- package/src/scss/components/_print.scss +74 -0
- package/src/scss/components/_progress.scss +32 -0
- package/src/scss/components/_reset.scss +365 -0
- package/src/scss/components/_rtl.scss +213 -0
- package/src/scss/components/_table-interactive.scss +110 -0
- package/src/scss/components/_tables.scss +52 -0
- package/src/scss/components/_themes.scss +6 -0
- package/src/scss/components/_tooltip.scss +35 -0
- package/src/scss/components/_typography.scss +565 -0
- package/src/scss/components/_upload.scss +19 -0
- package/src/scss/components/_variables.scss +129 -0
- package/src/scss/ply-core.scss +1 -0
- package/src/scss/ply-essentials.scss +15 -0
- package/src/scss/ply-helpers.scss +11 -0
- package/src/scss/ply-iso.scss +1 -0
- package/src/scss/styles.scss +9 -0
package/llms-full.txt
ADDED
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
# plycss (ply) — Complete Documentation
|
|
2
|
+
|
|
3
|
+
> ply is a ratio-based, AI-ready CSS framework with built-in dark mode, WCAG 2.1 AA accessibility, and a small footprint (~20KB gzipped). 421 utility classes, 60+ CSS custom properties, 13 auto-styled semantic elements. No JavaScript. No build step. Install via npm as `plycss` or use the CDN.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
### npm + Sass (recommended)
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install ply-css
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```scss
|
|
16
|
+
@use "ply-css/src/scss/ply" as *;
|
|
17
|
+
|
|
18
|
+
// Or import individual modules
|
|
19
|
+
@use "ply-css/src/scss/components/colors" as colors;
|
|
20
|
+
@use "ply-css/src/scss/components/variables" as variables;
|
|
21
|
+
@use "ply-css/src/scss/components/mixins" as mixins;
|
|
22
|
+
|
|
23
|
+
.custom {
|
|
24
|
+
color: colors.$color-blue;
|
|
25
|
+
background: colors.$color-blue-pastel;
|
|
26
|
+
@include mixins.border-bottom-radius(variables.$border-radius);
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
SCSS source: `src/scss/components/_colors.scss` (full color palette), `_variables.scss` (spacing, fonts, breakpoints), `_mixins.scss` (button generator, gradients, animations).
|
|
31
|
+
|
|
32
|
+
### CDN (prototyping only)
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ply-css@1/dist/css/ply.min.css">
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Core bundle (no labels, dropdowns, loaders, print):
|
|
39
|
+
|
|
40
|
+
```html
|
|
41
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ply-css@1/dist/css/ply-core.min.css">
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Bundles: `ply.min.css` (~20KB gzip, everything), `ply-core.min.css` (~17KB, grid+buttons+forms+nav+alerts+tables+typography+helpers), `ply-essentials.min.css` (~6KB, grid+helpers), `ply-helpers.min.css` (~4KB, helpers only).
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Core Rules
|
|
49
|
+
|
|
50
|
+
1. **ply is standalone** — Do NOT use Tailwind, Bootstrap, or other CSS frameworks alongside ply.
|
|
51
|
+
2. **Always wrap `unit-*` inside `units-row`** — they are flex children.
|
|
52
|
+
3. **Use `units-container`** for page-width centering (1200px max).
|
|
53
|
+
4. **Use `<button>` for buttons, not `<a>`** — links navigate, buttons act.
|
|
54
|
+
5. **Wrap forms in `.form`** for styled inputs.
|
|
55
|
+
6. **Use semantic HTML first** — ply auto-styles `<nav>`, `<table>`, `<code>`, `<pre>`, `<kbd>`, `<blockquote>`, `<mark>`, `<details>`, `<summary>`, `<dialog>`, `<progress>`, `<meter>`, `<hr>`, and headings.
|
|
56
|
+
7. **Only use documented classes** — do not invent class names. Search `ply-classes.json` first.
|
|
57
|
+
8. **Add responsive classes** — at minimum `tablet-unit-100` to stack on mobile.
|
|
58
|
+
9. **Use `--ply-*` custom properties** for colors — never hard-code values that break dark mode.
|
|
59
|
+
10. **Single-dash class names preferred** — `navbar-centered`, not `navbar--centered`.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Grid System
|
|
64
|
+
|
|
65
|
+
ply uses a ratio-based flexbox grid. Think in percentages, not arbitrary columns.
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<div class="units-container">
|
|
69
|
+
<div class="units-row gap">
|
|
70
|
+
<div class="unit-66 tablet-unit-100">Main content (2/3 width)</div>
|
|
71
|
+
<div class="unit-33 tablet-unit-100">Sidebar (1/3 width)</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- `units-container` — centers content at 1200px max-width
|
|
77
|
+
- `units-row` — creates the flex row
|
|
78
|
+
- `unit-*` — sets width as percentage. Available: 10, 12, 20, 25, 30, 33, 35, 38, 40, 50, 60, 62, 65, 66, 70, 75, 80, 88, 90, 100, auto
|
|
79
|
+
- Responsive prefixes: `tablet-unit-*` (≤ 1024px), `phone-unit-*` (≤ 767px), `small-desktop-unit-*`, `large-screen-unit-*`
|
|
80
|
+
- `gap-xs`, `gap-sm`, `gap`, `gap-lg`, `gap-xl` — spacing between flex children
|
|
81
|
+
- `equal-height` — on `units-row` to stretch all children to the tallest
|
|
82
|
+
- `units-row` can be nested inside units for complex layouts
|
|
83
|
+
|
|
84
|
+
### Two-Column Layout Example
|
|
85
|
+
|
|
86
|
+
```html
|
|
87
|
+
<div class="units-container">
|
|
88
|
+
<div class="units-row gap">
|
|
89
|
+
<div class="unit-66 tablet-unit-100">
|
|
90
|
+
<h2>Main Content</h2>
|
|
91
|
+
<p>Two-thirds width on desktop, full width on tablet.</p>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="unit-33 tablet-unit-100">
|
|
94
|
+
<aside>
|
|
95
|
+
<h3>Sidebar</h3>
|
|
96
|
+
<p class="text-secondary">Supplementary content.</p>
|
|
97
|
+
</aside>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Container Queries
|
|
106
|
+
|
|
107
|
+
Container queries let units respond to their parent's width instead of the viewport. Useful for reusable components (cards, widgets, sidebars) that adapt to available space.
|
|
108
|
+
|
|
109
|
+
**Wrapper:** `container-query` — sets `container-type: inline-size` on the parent.
|
|
110
|
+
|
|
111
|
+
**Container prefixes** mirror viewport prefixes:
|
|
112
|
+
|
|
113
|
+
| Container prefix | Breakpoint | Mirrors |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `container-phone-unit-*` | 480px | `phone-unit-*` |
|
|
116
|
+
| `container-large-phone-unit-*` | 650px | `large-phone-unit-*` |
|
|
117
|
+
| `container-tablet-unit-*` | 767px | `tablet-unit-*` |
|
|
118
|
+
| `container-small-desktop-unit-*` | 1024px | `small-desktop-unit-*` |
|
|
119
|
+
|
|
120
|
+
All 21 unit sizes available (100, 90, 88, 80, 75, 70, 66, 65, 62, 60, 50, 40, 38, 35, 33, 30, 25, 20, 12, 10, auto).
|
|
121
|
+
|
|
122
|
+
### Container Query Example
|
|
123
|
+
|
|
124
|
+
```html
|
|
125
|
+
<!-- A sidebar widget that stacks its columns when the sidebar is narrow -->
|
|
126
|
+
<aside class="container-query">
|
|
127
|
+
<div class="units-row">
|
|
128
|
+
<div class="unit-50 container-tablet-unit-100">
|
|
129
|
+
<p>Left content — stacks when container ≤ 767px</p>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="unit-50 container-tablet-unit-100">
|
|
132
|
+
<p>Right content — stacks when container ≤ 767px</p>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</aside>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Mixing Viewport and Container Prefixes
|
|
139
|
+
|
|
140
|
+
```html
|
|
141
|
+
<!-- Viewport prefix stacks on small screens; container prefix stacks when placed in a narrow parent -->
|
|
142
|
+
<div class="container-query">
|
|
143
|
+
<div class="units-row">
|
|
144
|
+
<div class="unit-66 tablet-unit-100 container-small-desktop-unit-100">Main</div>
|
|
145
|
+
<div class="unit-33 tablet-unit-100 container-small-desktop-unit-100">Side</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Container classes use `@container` rules (not `@media`) — they fire based on the `.container-query` element's inline size.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Buttons — Accessible by Default (WCAG 2.1 AA)
|
|
155
|
+
|
|
156
|
+
All ply buttons are WCAG 2.1 AA compliant out of the box. Always use `<button>`, not `<a>`.
|
|
157
|
+
|
|
158
|
+
### Button Hierarchy
|
|
159
|
+
|
|
160
|
+
- **`btn btn-primary`** — Primary call-to-action. Blue by default, themed via `--ply-btn-default-bg`. **WCAG AA compliant: 4.56:1 contrast ratio** (white text on blue background). In dark mode, contrast is maintained at AA level. `:focus-visible` shows a 2px blue outline for keyboard users. `prefers-contrast: more` strengthens colors further (WCAG 1.4.6).
|
|
161
|
+
- **`btn btn-secondary`** (or plain `btn`) — Secondary actions. Dark gray by default, themed via `--ply-btn-secondary-bg`. WCAG AA compliant.
|
|
162
|
+
- **`btn btn-primary-outline`** / **`btn btn-secondary-outline`** — Outlined variants. Transparent bg, border + text from the theme color. Fills on hover.
|
|
163
|
+
- **`btn btn-ghost`** — Ghost button. Text-only with subtle hover tint.
|
|
164
|
+
- **`btn btn-blue`**, **`btn btn-red`**, **`btn btn-green`**, **`btn btn-yellow`** — Static color buttons with hardcoded hex values. Immune to theming. Use for color-coded actions (e.g., delete = red, success = green). All meet WCAG AA contrast.
|
|
165
|
+
|
|
166
|
+
### Icon Buttons
|
|
167
|
+
|
|
168
|
+
- **`btn-icon`** — Icon-only button modifier. Equal padding for a square aspect ratio. Always add `aria-label` (no visible text). Combine with `btn-ghost` for toolbar-style actions.
|
|
169
|
+
- For icon + text buttons, use a regular `btn` with an inline SVG — no `btn-icon` needed.
|
|
170
|
+
|
|
171
|
+
```html
|
|
172
|
+
<button class="btn btn-ghost btn-icon" aria-label="Search"><svg>...</svg></button>
|
|
173
|
+
<button class="btn btn-primary"><svg>...</svg> Save</button>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Button Sizes
|
|
177
|
+
|
|
178
|
+
- `btn-big` (alias: `btn-lg`) — larger button
|
|
179
|
+
- `btn-small` (alias: `btn-sm`) — smaller button
|
|
180
|
+
- `btn-smaller` (alias: `btn-xs`) — smallest button
|
|
181
|
+
|
|
182
|
+
### Button Accessibility Features
|
|
183
|
+
|
|
184
|
+
Every ply button automatically gets:
|
|
185
|
+
- `:focus-visible` with a 2px solid blue outline and 2px offset — visible to keyboard users, invisible to mouse users (WCAG 2.4.7 Focus Visible)
|
|
186
|
+
- `prefers-contrast: more` support — colors become stronger in high-contrast mode (WCAG 1.4.6 Contrast Enhanced)
|
|
187
|
+
- `prefers-reduced-motion: reduce` — disables any hover/active transitions (WCAG 2.3.3)
|
|
188
|
+
- 3:1 non-text contrast on borders and focus indicators (WCAG 1.4.11)
|
|
189
|
+
- No JavaScript required — all button interactions are CSS-based
|
|
190
|
+
|
|
191
|
+
```html
|
|
192
|
+
<!-- Primary + Secondary pair -->
|
|
193
|
+
<button class="btn btn-primary">Save</button>
|
|
194
|
+
<button class="btn btn-secondary">Cancel</button>
|
|
195
|
+
|
|
196
|
+
<!-- Themed button via CSS custom property -->
|
|
197
|
+
<style>[data-theme="warm"] { --ply-btn-default-bg: #92400e; }</style>
|
|
198
|
+
|
|
199
|
+
<!-- Static color button (ignores theme) -->
|
|
200
|
+
<button class="btn btn-blue">Always Blue</button>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Dark Mode
|
|
206
|
+
|
|
207
|
+
ply auto-detects `prefers-color-scheme: dark` when no `data-theme` attribute is set on `<html>`. All `--ply-*` custom properties swap automatically — backgrounds, text, borders, buttons, links. WCAG AA contrast is maintained in both light and dark modes.
|
|
208
|
+
|
|
209
|
+
```html
|
|
210
|
+
<html> <!-- Auto (follows OS preference) -->
|
|
211
|
+
<html data-theme="light"> <!-- Force light -->
|
|
212
|
+
<html data-theme="dark"> <!-- Force dark -->
|
|
213
|
+
<html data-theme="warm"> <!-- Custom theme (no auto dark mode) -->
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Components that adapt automatically — no configuration needed:
|
|
217
|
+
|
|
218
|
+
```html
|
|
219
|
+
<div class="layer-1 padding border-radius">
|
|
220
|
+
<h2>Dashboard</h2>
|
|
221
|
+
<p class="text-secondary">This card adapts to light/dark automatically.</p>
|
|
222
|
+
<button class="btn btn-primary">Action</button>
|
|
223
|
+
</div>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`layer-1` uses `--ply-bg-surface` (white in light, `#262626` in dark). `text-secondary` uses `--ply-color-secondary` (`#525252` light, `#c6c6c6` dark). Every ply class is theme-aware.
|
|
227
|
+
|
|
228
|
+
Toggle with JavaScript:
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
document.documentElement.dataset.theme =
|
|
232
|
+
document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Custom Themes
|
|
238
|
+
|
|
239
|
+
Override `--ply-*` custom properties to theme the entire app with one CSS block:
|
|
240
|
+
|
|
241
|
+
```css
|
|
242
|
+
[data-theme="brand"] {
|
|
243
|
+
--ply-bg-body: #fefce8;
|
|
244
|
+
--ply-bg-surface: #fef9c3;
|
|
245
|
+
--ply-bg-muted: #fef08a;
|
|
246
|
+
--ply-color-body: #1a1a1a;
|
|
247
|
+
--ply-color-headings: #78350f;
|
|
248
|
+
--ply-border-color: #fbbf24;
|
|
249
|
+
--ply-btn-default-bg: #b45309;
|
|
250
|
+
--ply-btn-default-bg-hover: #92400e;
|
|
251
|
+
--ply-btn-default-bg-active: #7c2d12;
|
|
252
|
+
--ply-btn-secondary-bg: #78350f;
|
|
253
|
+
--ply-nav-bg: #fef3c7;
|
|
254
|
+
--ply-nav-border: #f59e0b;
|
|
255
|
+
--ply-font-body: "Palatino Linotype", Palatino, Georgia, serif;
|
|
256
|
+
--ply-font-heading: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
257
|
+
--ply-font-mono: "Fira Code", "Source Code Pro", monospace;
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
```html
|
|
262
|
+
<html data-theme="brand">
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Setting any `data-theme` value prevents auto dark mode from interfering. All 60+ `--ply-*` variables are listed in the `customProperties` section of `ply-classes.json`.
|
|
266
|
+
|
|
267
|
+
**Important:** If you override colors, verify 4.5:1 contrast for normal text and 3:1 for large text (18px bold / 24px) and UI components (WCAG 1.4.3).
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Navigation — Semantic and Responsive
|
|
272
|
+
|
|
273
|
+
### Basic Navbar
|
|
274
|
+
|
|
275
|
+
```html
|
|
276
|
+
<nav class="navbar" aria-label="Main navigation">
|
|
277
|
+
<ul>
|
|
278
|
+
<li class="active"><a href="/" aria-current="page">Home</a></li>
|
|
279
|
+
<li><a href="/about">About</a></li>
|
|
280
|
+
<li><a href="/services">Services</a></li>
|
|
281
|
+
<li><a href="/contact">Contact</a></li>
|
|
282
|
+
</ul>
|
|
283
|
+
</nav>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Navbar variants: `navbar-thick`, `navbar-borderless`, `navbar-border-blue`, `navbar-border-green`, `navbar-border-red`, `navbar-border-yellow`, `navbar-centered`.
|
|
287
|
+
|
|
288
|
+
### Responsive Collapsible Header (CSS-Only, No JavaScript)
|
|
289
|
+
|
|
290
|
+
Use `<details>`/`<summary>` as a hamburger toggle for mobile. The desktop nav shows inline; on mobile it collapses behind a toggle. Fully keyboard-accessible — Enter/Space toggles `<details>` natively.
|
|
291
|
+
|
|
292
|
+
```html
|
|
293
|
+
<a href="#main" class="skip-link">Skip to main content</a>
|
|
294
|
+
|
|
295
|
+
<header class="sticky" style="top: 0; z-index: 100;">
|
|
296
|
+
<nav class="navbar" aria-label="Main navigation" style="position: relative;">
|
|
297
|
+
<!-- Desktop nav — hidden on mobile (≤ 767px) -->
|
|
298
|
+
<ul class="phone-hide">
|
|
299
|
+
<li class="active"><a href="#" aria-current="page">Home</a></li>
|
|
300
|
+
<li><a href="#">About</a></li>
|
|
301
|
+
<li><a href="#">Services</a></li>
|
|
302
|
+
<li><a href="#">Pricing</a></li>
|
|
303
|
+
<li><a href="#">Contact</a></li>
|
|
304
|
+
</ul>
|
|
305
|
+
<!-- Mobile hamburger — hidden on tablet (≥ 768px) and desktop -->
|
|
306
|
+
<details class="tablet-hide desktop-hide">
|
|
307
|
+
<summary aria-label="Menu">☰ Menu</summary>
|
|
308
|
+
<ul>
|
|
309
|
+
<li class="active"><a href="#" aria-current="page">Home</a></li>
|
|
310
|
+
<li><a href="#">About</a></li>
|
|
311
|
+
<li><a href="#">Services</a></li>
|
|
312
|
+
<li><a href="#">Pricing</a></li>
|
|
313
|
+
<li><a href="#">Contact</a></li>
|
|
314
|
+
</ul>
|
|
315
|
+
</details>
|
|
316
|
+
</nav>
|
|
317
|
+
</header>
|
|
318
|
+
|
|
319
|
+
<main id="main">...</main>
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Required CSS for the mobile dropdown overlay:
|
|
323
|
+
|
|
324
|
+
```css
|
|
325
|
+
nav.navbar details > ul {
|
|
326
|
+
position: absolute;
|
|
327
|
+
left: 0; right: 0;
|
|
328
|
+
background: var(--ply-bg-surface);
|
|
329
|
+
border-bottom: 1px solid var(--ply-border-color);
|
|
330
|
+
padding: 0.5rem 0;
|
|
331
|
+
z-index: 99;
|
|
332
|
+
}
|
|
333
|
+
nav.navbar details > ul li { display: block; }
|
|
334
|
+
nav.navbar details > ul li a { display: block; padding: 0.5rem 1rem; }
|
|
335
|
+
nav.navbar details summary { cursor: pointer; padding: 0.5rem 1rem; list-style: none; }
|
|
336
|
+
nav.navbar details summary::-webkit-details-marker { display: none; }
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**How it works:**
|
|
340
|
+
- `sticky` + `top: 0` pins the header on scroll
|
|
341
|
+
- `phone-hide` hides the desktop `<ul>` on screens ≤ 767px
|
|
342
|
+
- `tablet-hide desktop-hide` shows the `<details>` toggle only on mobile
|
|
343
|
+
- `<details>`/`<summary>` is natively keyboard-accessible (Enter/Space toggles, no JS)
|
|
344
|
+
- `aria-label="Menu"` on `<summary>` and `aria-current="page"` on the active link for screen readers
|
|
345
|
+
|
|
346
|
+
**Responsive visibility classes:**
|
|
347
|
+
- `phone-hide` — hidden ≤ 767px
|
|
348
|
+
- `tablet-hide` — hidden 768px–1024px
|
|
349
|
+
- `desktop-hide` — hidden ≥ 1025px
|
|
350
|
+
- `phone-only` — visible only ≤ 767px
|
|
351
|
+
- `tablet-only` — visible only 768px–1024px
|
|
352
|
+
|
|
353
|
+
Always include: `<meta name="viewport" content="width=device-width, initial-scale=1.0">`
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Cards and Surfaces
|
|
358
|
+
|
|
359
|
+
```html
|
|
360
|
+
<div class="layer-1 padding-lg margin-bottom border border-radius">
|
|
361
|
+
<h3>Card Title</h3>
|
|
362
|
+
<p class="text-secondary">Description with theme-aware text.</p>
|
|
363
|
+
<button class="btn btn-primary">Learn More</button>
|
|
364
|
+
</div>
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
- `layer-1` — surface background (`--ply-bg-surface`) + subtle box-shadow
|
|
368
|
+
- `layer-2` — elevated surface (`--ply-bg-muted`) + stronger shadow
|
|
369
|
+
- `padding-xs` (0.25rem), `padding-sm` (0.5rem), `padding` (default), `padding-lg` (1.5rem), `padding-xl` (2rem)
|
|
370
|
+
- `margin-bottom`, `margin-top-lg`, `margin-xl` — directional + size variants
|
|
371
|
+
- `border` — 1px solid using `--ply-border-color` (theme-aware)
|
|
372
|
+
- `border-radius` (default), `border-radius-lg` (0.75rem), `border-radius-xl` (1.5rem)
|
|
373
|
+
|
|
374
|
+
Equal-height cards in a row:
|
|
375
|
+
|
|
376
|
+
```html
|
|
377
|
+
<div class="units-row gap-lg equal-height">
|
|
378
|
+
<div class="unit-33 tablet-unit-100">
|
|
379
|
+
<div class="layer-1 padding border-radius">Card 1</div>
|
|
380
|
+
</div>
|
|
381
|
+
<div class="unit-33 tablet-unit-100">
|
|
382
|
+
<div class="layer-2 padding border-radius">Card 2 (elevated)</div>
|
|
383
|
+
</div>
|
|
384
|
+
<div class="unit-33 tablet-unit-100">
|
|
385
|
+
<div class="layer-1 padding border-radius">Card 3</div>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## Typography and Text
|
|
393
|
+
|
|
394
|
+
- `text-primary` — primary text color (theme-aware)
|
|
395
|
+
- `text-secondary` — secondary/muted text
|
|
396
|
+
- `text-tertiary` — tertiary/subtle text
|
|
397
|
+
- `text-sm` — small text
|
|
398
|
+
- `text-lg` — large text
|
|
399
|
+
- `text-center`, `text-left`, `text-right` — alignment
|
|
400
|
+
- `text-balance` — CSS `text-wrap: balance` for headings
|
|
401
|
+
- `no-orphan` — prevents orphaned last words in paragraphs
|
|
402
|
+
- `.h1` through `.h6` — visual heading sizes without semantic heading level
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## Forms
|
|
407
|
+
|
|
408
|
+
Wrap in `.form` to activate ply input styling:
|
|
409
|
+
|
|
410
|
+
```html
|
|
411
|
+
<form class="form">
|
|
412
|
+
<label for="name">Name</label>
|
|
413
|
+
<input type="text" id="name" required>
|
|
414
|
+
<label for="email">Email</label>
|
|
415
|
+
<input type="email" id="email" required>
|
|
416
|
+
<button class="btn btn-primary" type="submit">Submit</button>
|
|
417
|
+
</form>
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Input sizes: `input-big` (alias: `input-lg`), `input-small` (alias: `input-sm`), `input-smaller` (alias: `input-xs`).
|
|
421
|
+
|
|
422
|
+
Validation states: `input-error`, `input-success`.
|
|
423
|
+
|
|
424
|
+
### Input Groups
|
|
425
|
+
|
|
426
|
+
Use `.input-groups` for prepend/append addons on inputs. **Buttons must be wrapped** in `.input-append` or `.btn-append` — placing a button directly inside `.input-groups` without a wrapper won't work.
|
|
427
|
+
|
|
428
|
+
```html
|
|
429
|
+
<!-- Text addon -->
|
|
430
|
+
<div class="input-groups">
|
|
431
|
+
<span class="input-prepend">$</span>
|
|
432
|
+
<input type="text" placeholder="Amount">
|
|
433
|
+
<span class="input-append">.00</span>
|
|
434
|
+
</div>
|
|
435
|
+
|
|
436
|
+
<!-- Button appended via input-append -->
|
|
437
|
+
<div class="input-groups">
|
|
438
|
+
<input type="text" placeholder="Search...">
|
|
439
|
+
<span class="input-append">
|
|
440
|
+
<button class="btn btn-primary">Search</button>
|
|
441
|
+
</span>
|
|
442
|
+
</div>
|
|
443
|
+
|
|
444
|
+
<!-- Button appended via btn-append -->
|
|
445
|
+
<div class="input-groups">
|
|
446
|
+
<input type="text" placeholder="Enter URL">
|
|
447
|
+
<div class="btn-append">
|
|
448
|
+
<button class="btn btn-primary-outline">Go</button>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Outline buttons in input groups automatically match the input border. Button scale transforms are disabled inside input groups for a clean inline look.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## Alerts and Notifications
|
|
458
|
+
|
|
459
|
+
```html
|
|
460
|
+
<div class="alert">Default alert</div>
|
|
461
|
+
<div class="alert alert-blue">Info alert</div>
|
|
462
|
+
<div class="alert alert-green">Success alert</div>
|
|
463
|
+
<div class="alert alert-red">Error alert</div>
|
|
464
|
+
<div class="alert alert-yellow">Warning alert</div>
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Aliases: `alert` = `tools-alert`, `message` = `tools-message`.
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## Common Patterns
|
|
472
|
+
|
|
473
|
+
- **Equal-height cards** — `equal-height` on `units-row`
|
|
474
|
+
- **Gap between children** — `gap-xs`, `gap-sm`, `gap`, `gap-lg`, `gap-xl`
|
|
475
|
+
- **Prevent orphans** — `no-orphan` on paragraphs, `text-balance` on headings
|
|
476
|
+
- **Card-style links** — `no-link-style` on a container to suppress link color/underline
|
|
477
|
+
- **Sticky elements** — `sticky` class + `top: 0` inline style
|
|
478
|
+
- **Display utilities** — `display-flex`, `display-grid`, `display-block`, `display-none`
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Accessibility (WCAG 2.1 AA) — Built Into Every Component
|
|
483
|
+
|
|
484
|
+
ply is built for Section 508 / WCAG 2.1 AA compliance. Every interactive element ships with accessibility features — no extra configuration needed.
|
|
485
|
+
|
|
486
|
+
### What ply provides automatically
|
|
487
|
+
|
|
488
|
+
- **Focus indicators** — All interactive elements (buttons, links, inputs, nav items, dropdowns, `<summary>`, `<dialog>`) get `:focus-visible` with a 2px solid blue outline and 2px offset. Keyboard users see clear focus rings; mouse users don't. (WCAG 2.4.7 Focus Visible)
|
|
489
|
+
- **Color contrast** — All default color pairings meet 4.5:1 contrast in both light and dark modes. `btn-primary` delivers 4.56:1 contrast (white on blue). The entire brand palette is tuned for AA. (WCAG 1.4.3 Contrast Minimum)
|
|
490
|
+
- **Non-text contrast** — Focus indicators, button borders, and input borders meet 3:1 ratio. (WCAG 1.4.11)
|
|
491
|
+
- **High contrast mode** — `@media (prefers-contrast: more)` strengthens text and link colors beyond AA minimums. (WCAG 1.4.6 Contrast Enhanced)
|
|
492
|
+
- **Reduced motion** — `@media (prefers-reduced-motion: reduce)` disables all CSS animations and transitions. (WCAG 2.3.3)
|
|
493
|
+
- **Dark mode contrast** — `prefers-color-scheme: dark` maintains WCAG AA contrast. (WCAG 1.4.3)
|
|
494
|
+
- **Skip link** — `.skip-link` as the first focusable element lets keyboard users bypass navigation. (WCAG 2.4.1)
|
|
495
|
+
- **Screen reader support** — `.sr-only` hides content visually while keeping it accessible to assistive technology. (WCAG 1.3.1)
|
|
496
|
+
- **Keyboard operability** — All ply interactive elements are natively keyboard-operable via semantic HTML. No custom JS required. (WCAG 2.1.1)
|
|
497
|
+
- **Zero JavaScript** — No ARIA state management failures from framework code. (WCAG 4.1.2)
|
|
498
|
+
- **Sortable table headers** — `th.sortable` gets `:focus-visible` outlines
|
|
499
|
+
- **Pagination** — Focus-visible outlines on all page links, `aria-current="page"` supported
|
|
500
|
+
- **RTL support** — `dir="rtl"` for Arabic, Hebrew, and other RTL languages
|
|
501
|
+
|
|
502
|
+
### WCAG compliance summary
|
|
503
|
+
|
|
504
|
+
| WCAG Criterion | How ply addresses it |
|
|
505
|
+
|---|---|
|
|
506
|
+
| 1.3.1 Info and Relationships | Semantic HTML auto-styling encourages `<nav>`, `<main>`, `<aside>`, `<table>`, `<details>`, headings. `.sr-only` exposes supplemental info. |
|
|
507
|
+
| 1.4.3 Contrast (Minimum) | All default colors meet 4.5:1. `btn-primary` = 4.56:1. Dark mode maintains AA. |
|
|
508
|
+
| 1.4.6 Contrast (Enhanced) | `prefers-contrast: more` strengthens colors beyond AA. |
|
|
509
|
+
| 1.4.11 Non-Text Contrast | Focus indicators, button borders, input borders all meet 3:1. |
|
|
510
|
+
| 2.1.1 Keyboard | All interactive elements keyboard-operable via native HTML. |
|
|
511
|
+
| 2.3.3 Animation | `prefers-reduced-motion: reduce` disables all animations. |
|
|
512
|
+
| 2.4.1 Bypass Blocks | `.skip-link` for keyboard bypass navigation. |
|
|
513
|
+
| 2.4.7 Focus Visible | `:focus-visible` on every interactive element. |
|
|
514
|
+
| 4.1.2 Name, Role, Value | Zero JS = no ARIA state failures. Native HTML exposes correct roles. |
|
|
515
|
+
|
|
516
|
+
### What needs application-level work
|
|
517
|
+
|
|
518
|
+
- **Alt text** — Add `alt` to every `<img>`; `alt=""` for decorative. (1.1.1)
|
|
519
|
+
- **Heading hierarchy** — One `<h1>` per page, then h2-h6 in sequence. (1.3.1)
|
|
520
|
+
- **Custom widget ARIA** — JS-powered dropdowns, modals, tabs need `aria-expanded`, `aria-controls`, `role`. (4.1.2)
|
|
521
|
+
- **Custom color contrast** — If you override `--ply-*` variables, verify 4.5:1 for text, 3:1 for large text/UI. (1.4.3)
|
|
522
|
+
- **Keyboard for custom widgets** — Custom JS components must be keyboard-operable. (2.1.1)
|
|
523
|
+
- **Page title** — Every page needs `<title>`. (2.4.2)
|
|
524
|
+
- **Language** — Set `<html lang="en">`. (3.1.1)
|
|
525
|
+
- **Form labels** — Use `<label>` for all inputs. (3.3.2)
|
|
526
|
+
- **Error identification** — Identify errors in text, not just color. (3.3.1)
|
|
527
|
+
|
|
528
|
+
### AI agent guidance for accessible markup
|
|
529
|
+
|
|
530
|
+
When generating ply markup, always:
|
|
531
|
+
1. Use semantic elements for landmarks — `<header>`, `<nav>`, `<main>`, `<aside>`, `<footer>`
|
|
532
|
+
2. Add `aria-label` to custom widgets without visible text labels
|
|
533
|
+
3. Use `<form role="search">` for search forms
|
|
534
|
+
4. Include `.skip-link` as the first focusable element
|
|
535
|
+
5. Add `alt` text to all `<img>` elements
|
|
536
|
+
6. Maintain heading hierarchy (h1 > h2 > h3, no skipping)
|
|
537
|
+
7. Label all form inputs with `<label>`
|
|
538
|
+
8. Set `lang` on `<html>`
|
|
539
|
+
9. Pair color with text for status indicators
|
|
540
|
+
10. Prefer native elements over ARIA — `<button>` over `<div role="button">`, `<dialog>` over `<div role="dialog">`
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
## Focus Management & Keyboard Patterns
|
|
545
|
+
|
|
546
|
+
ply provides `:focus-visible` outlines on all native interactive elements. For custom widgets, manage focus yourself.
|
|
547
|
+
|
|
548
|
+
### Focus Order Strategy
|
|
549
|
+
|
|
550
|
+
1. **Use semantic HTML first** — `<button>`, `<a>`, `<input>`, `<select>`, `<details>`, `<dialog>` are already in tab order.
|
|
551
|
+
2. **`tabindex="0"`** — Add to custom interactive elements to include in tab order. ply's `:focus-visible` applies automatically.
|
|
552
|
+
3. **`tabindex="-1"`** — For programmatic focus targets (modal containers, error summaries). NOT in tab order.
|
|
553
|
+
4. **Never use `tabindex` > 0** — Overrides natural DOM order.
|
|
554
|
+
|
|
555
|
+
```html
|
|
556
|
+
<!-- Native — already focusable, ply styles focus -->
|
|
557
|
+
<button class="btn">Save</button>
|
|
558
|
+
<a href="/settings">Settings</a>
|
|
559
|
+
|
|
560
|
+
<!-- Custom interactive element — add tabindex="0" -->
|
|
561
|
+
<div role="button" tabindex="0"
|
|
562
|
+
onclick="doAction()"
|
|
563
|
+
onkeydown="if(event.key==='Enter'||event.key===' ')doAction()">
|
|
564
|
+
Custom Action
|
|
565
|
+
</div>
|
|
566
|
+
|
|
567
|
+
<!-- Programmatic focus target — tabindex="-1" -->
|
|
568
|
+
<div id="error-summary" tabindex="-1" role="alert">
|
|
569
|
+
Please fix the errors below.
|
|
570
|
+
</div>
|
|
571
|
+
<script>
|
|
572
|
+
document.getElementById('error-summary').focus();
|
|
573
|
+
</script>
|
|
574
|
+
|
|
575
|
+
<!-- Screen-reader-only content -->
|
|
576
|
+
<span class="sr-only">3 notifications</span>
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### Arrow Key Navigation (Roving Tabindex)
|
|
580
|
+
|
|
581
|
+
For grouped controls (tabs, toolbars, menu items): one item has `tabindex="0"` (active), the rest have `tabindex="-1"`. Arrow keys move focus within the group.
|
|
582
|
+
|
|
583
|
+
```html
|
|
584
|
+
<div role="tablist" aria-label="Settings">
|
|
585
|
+
<button role="tab" aria-selected="true" tabindex="0" aria-controls="panel-general">General</button>
|
|
586
|
+
<button role="tab" aria-selected="false" tabindex="-1" aria-controls="panel-security">Security</button>
|
|
587
|
+
<button role="tab" aria-selected="false" tabindex="-1" aria-controls="panel-notifications">Notifications</button>
|
|
588
|
+
</div>
|
|
589
|
+
<div role="tabpanel" id="panel-general" aria-labelledby="tab-general">
|
|
590
|
+
General settings content.
|
|
591
|
+
</div>
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
```js
|
|
595
|
+
tablist.addEventListener('keydown', (e) => {
|
|
596
|
+
const tabs = [...tablist.querySelectorAll('[role="tab"]')];
|
|
597
|
+
const current = tabs.indexOf(document.activeElement);
|
|
598
|
+
let next;
|
|
599
|
+
if (e.key === 'ArrowRight') next = (current + 1) % tabs.length;
|
|
600
|
+
else if (e.key === 'ArrowLeft') next = (current - 1 + tabs.length) % tabs.length;
|
|
601
|
+
else if (e.key === 'Home') next = 0;
|
|
602
|
+
else if (e.key === 'End') next = tabs.length - 1;
|
|
603
|
+
else return;
|
|
604
|
+
e.preventDefault();
|
|
605
|
+
tabs[current].setAttribute('tabindex', '-1');
|
|
606
|
+
tabs[current].setAttribute('aria-selected', 'false');
|
|
607
|
+
tabs[next].setAttribute('tabindex', '0');
|
|
608
|
+
tabs[next].setAttribute('aria-selected', 'true');
|
|
609
|
+
tabs[next].focus();
|
|
610
|
+
tabs.forEach((tab, i) => {
|
|
611
|
+
document.getElementById(tab.getAttribute('aria-controls')).hidden = (i !== next);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
### ARIA Attributes Reference
|
|
617
|
+
|
|
618
|
+
| Attribute | When to use | Example |
|
|
619
|
+
|---|---|---|
|
|
620
|
+
| `aria-expanded` | Toggleable content (dropdowns, accordions, menus) | `<button aria-expanded="false" aria-controls="menu">Menu</button>` |
|
|
621
|
+
| `aria-controls` | Links trigger to controlled element | `<button aria-controls="dropdown-1">Options</button>` |
|
|
622
|
+
| `aria-selected` | Active item in tabs, listboxes | `<button role="tab" aria-selected="true">Tab 1</button>` |
|
|
623
|
+
| `aria-current="page"` | Current page in navigation | `<a href="/" aria-current="page">Home</a>` |
|
|
624
|
+
| `aria-live="polite"` | Dynamic content updates | `<div aria-live="polite">3 results found.</div>` |
|
|
625
|
+
| `aria-live="assertive"` | Urgent announcements | `<div aria-live="assertive" role="alert">Session expired.</div>` |
|
|
626
|
+
| `aria-label` | Elements without visible text | `<button aria-label="Close">×</button>` |
|
|
627
|
+
| `aria-labelledby` | Points to another element for label | `<div role="tabpanel" aria-labelledby="tab-1">` |
|
|
628
|
+
| `aria-describedby` | Additional description | `<input aria-describedby="password-hint">` |
|
|
629
|
+
|
|
630
|
+
### Live Regions for Dynamic Content
|
|
631
|
+
|
|
632
|
+
```html
|
|
633
|
+
<!-- Polite — announced after current speech -->
|
|
634
|
+
<div aria-live="polite" role="status" class="sr-only" id="search-status"></div>
|
|
635
|
+
<script>
|
|
636
|
+
document.getElementById('search-status').textContent = '12 results found.';
|
|
637
|
+
</script>
|
|
638
|
+
|
|
639
|
+
<!-- Assertive — announced immediately -->
|
|
640
|
+
<div aria-live="assertive" role="alert">
|
|
641
|
+
Your session has expired. Please log in again.
|
|
642
|
+
</div>
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
## Custom Widget Accessibility Patterns
|
|
648
|
+
|
|
649
|
+
When building custom interactive widgets beyond ply's built-in components, follow these patterns for WCAG 2.1 AA compliance.
|
|
650
|
+
|
|
651
|
+
### Drag-and-Drop with Full Keyboard Support
|
|
652
|
+
|
|
653
|
+
Drag-and-drop interfaces must be fully keyboard operable (WCAG 2.1.1). Use a listbox pattern with grab/move/drop states and an `aria-live` region for screen reader announcements.
|
|
654
|
+
|
|
655
|
+
```html
|
|
656
|
+
<!-- Live region for announcements -->
|
|
657
|
+
<div class="sr-only" aria-live="assertive" id="dnd-status"></div>
|
|
658
|
+
|
|
659
|
+
<!-- Visible keyboard instructions -->
|
|
660
|
+
<p class="text-secondary text-sm">
|
|
661
|
+
Keyboard: Tab to list, Space to grab, Up/Down to move, Enter to drop, Escape to cancel.
|
|
662
|
+
</p>
|
|
663
|
+
|
|
664
|
+
<!-- Sortable list with ARIA roles -->
|
|
665
|
+
<ul role="listbox" aria-label="Sortable tasks" id="sortable-list">
|
|
666
|
+
<li role="option" tabindex="0" aria-grabbed="false">Review pull request</li>
|
|
667
|
+
<li role="option" tabindex="-1" aria-grabbed="false">Update documentation</li>
|
|
668
|
+
<li role="option" tabindex="-1" aria-grabbed="false">Fix navigation bug</li>
|
|
669
|
+
</ul>
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
```js
|
|
673
|
+
const list = document.getElementById('sortable-list');
|
|
674
|
+
const status = document.getElementById('dnd-status');
|
|
675
|
+
let grabbed = null;
|
|
676
|
+
|
|
677
|
+
list.addEventListener('keydown', (e) => {
|
|
678
|
+
const item = e.target;
|
|
679
|
+
if (item.getAttribute('role') !== 'option') return;
|
|
680
|
+
|
|
681
|
+
switch (e.key) {
|
|
682
|
+
case ' ':
|
|
683
|
+
e.preventDefault();
|
|
684
|
+
if (!grabbed) {
|
|
685
|
+
grabbed = item;
|
|
686
|
+
item.setAttribute('aria-grabbed', 'true');
|
|
687
|
+
status.textContent = `Grabbed ${item.textContent}. Use arrow keys to move, Enter to drop, Escape to cancel.`;
|
|
688
|
+
}
|
|
689
|
+
break;
|
|
690
|
+
case 'ArrowDown':
|
|
691
|
+
e.preventDefault();
|
|
692
|
+
if (grabbed) {
|
|
693
|
+
const next = grabbed.nextElementSibling;
|
|
694
|
+
if (next) {
|
|
695
|
+
list.insertBefore(next, grabbed);
|
|
696
|
+
status.textContent = `Moved down to position ${[...list.children].indexOf(grabbed) + 1}.`;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
break;
|
|
700
|
+
case 'ArrowUp':
|
|
701
|
+
e.preventDefault();
|
|
702
|
+
if (grabbed) {
|
|
703
|
+
const prev = grabbed.previousElementSibling;
|
|
704
|
+
if (prev) {
|
|
705
|
+
list.insertBefore(grabbed, prev);
|
|
706
|
+
status.textContent = `Moved up to position ${[...list.children].indexOf(grabbed) + 1}.`;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
break;
|
|
710
|
+
case 'Enter':
|
|
711
|
+
if (grabbed) {
|
|
712
|
+
e.preventDefault();
|
|
713
|
+
grabbed.setAttribute('aria-grabbed', 'false');
|
|
714
|
+
status.textContent = `Dropped ${grabbed.textContent} at position ${[...list.children].indexOf(grabbed) + 1}.`;
|
|
715
|
+
grabbed = null;
|
|
716
|
+
}
|
|
717
|
+
break;
|
|
718
|
+
case 'Escape':
|
|
719
|
+
if (grabbed) {
|
|
720
|
+
e.preventDefault();
|
|
721
|
+
grabbed.setAttribute('aria-grabbed', 'false');
|
|
722
|
+
status.textContent = 'Reorder cancelled.';
|
|
723
|
+
grabbed = null;
|
|
724
|
+
}
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
**Accessibility features in this pattern:**
|
|
731
|
+
- `aria-grabbed` toggles between "true" (grabbed) and "false" (released) — announced by screen readers (WCAG 4.1.2)
|
|
732
|
+
- `aria-live="assertive"` region announces every move to screen readers
|
|
733
|
+
- Roving tabindex — only the focused item has `tabindex="0"`
|
|
734
|
+
- `role="listbox"` + `role="option"` — correct semantics for screen readers
|
|
735
|
+
- Visible keyboard instructions for sighted users
|
|
736
|
+
- `prefers-reduced-motion: reduce` — disable drag animations in CSS
|
|
737
|
+
- Full keyboard alternative: Space=grab, Arrows=move, Enter=drop, Escape=cancel (WCAG 2.1.1)
|
|
738
|
+
|
|
739
|
+
### Focus Trap for Modals
|
|
740
|
+
|
|
741
|
+
Prefer native `<dialog>` — ply styles it automatically and browsers handle focus trapping. For custom modals:
|
|
742
|
+
|
|
743
|
+
```js
|
|
744
|
+
function trapFocus(modal) {
|
|
745
|
+
const focusable = modal.querySelectorAll(
|
|
746
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
747
|
+
);
|
|
748
|
+
const first = focusable[0], last = focusable[focusable.length - 1];
|
|
749
|
+
modal.addEventListener('keydown', (e) => {
|
|
750
|
+
if (e.key !== 'Tab') return;
|
|
751
|
+
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
|
|
752
|
+
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
|
|
753
|
+
});
|
|
754
|
+
first?.focus();
|
|
755
|
+
}
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
### Focus Return After Close
|
|
759
|
+
|
|
760
|
+
Always restore focus to the element that triggered the widget:
|
|
761
|
+
|
|
762
|
+
```js
|
|
763
|
+
const trigger = document.activeElement;
|
|
764
|
+
modal.showModal();
|
|
765
|
+
modal.addEventListener('close', () => trigger.focus(), { once: true });
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
### Custom Widget Accessibility Checklist
|
|
769
|
+
|
|
770
|
+
| Requirement | WCAG Criterion | How to verify |
|
|
771
|
+
|---|---|---|
|
|
772
|
+
| Keyboard operable (no mouse required) | 2.1.1 Keyboard | Tab, Enter, Space, arrow keys all work |
|
|
773
|
+
| Focus indicator visible | 2.4.7 Focus Visible | ply provides `:focus-visible` — don't override `outline: none` |
|
|
774
|
+
| States announced to screen readers | 4.1.2 Name, Role, Value | `aria-grabbed`, `aria-expanded`, `aria-selected` update on interaction |
|
|
775
|
+
| Focus trapped in modals | 2.4.3 Focus Order | Tab does not leave an open dialog |
|
|
776
|
+
| Focus restored on close | 2.4.3 Focus Order | Closing returns focus to the trigger element |
|
|
777
|
+
| Touch targets >= 44x44px | 2.5.5 Target Size | Measure interactive elements |
|
|
778
|
+
| Alternative input available | 2.1.1 Keyboard | Drag-and-drop has keyboard fallback |
|
|
779
|
+
| Screen reader tested | 4.1.2 Name, Role, Value | Verify with VoiceOver (macOS) or NVDA (Windows) |
|
|
780
|
+
|
|
781
|
+
### Screen Reader Testing (VoiceOver on macOS)
|
|
782
|
+
|
|
783
|
+
| Action | Shortcut |
|
|
784
|
+
|---|---|
|
|
785
|
+
| Turn on/off VoiceOver | Cmd + F5 |
|
|
786
|
+
| Navigate next element | VO + Right Arrow (VO = Ctrl + Option) |
|
|
787
|
+
| Navigate previous | VO + Left Arrow |
|
|
788
|
+
| Activate element | VO + Space |
|
|
789
|
+
| Read current element | VO + F3 |
|
|
790
|
+
| Open rotor (landmarks, headings) | VO + U |
|
|
791
|
+
|
|
792
|
+
**Verify:** Every interactive element reachable by Tab/arrows. Buttons announce label + role. Expanded/collapsed state announced. Dynamic content announced via `aria-live`. Form inputs announce label + required + errors. Heading hierarchy logical.
|
|
793
|
+
|
|
794
|
+
---
|
|
795
|
+
|
|
796
|
+
## AI-Friendly Design
|
|
797
|
+
|
|
798
|
+
ply is built for AI code generation:
|
|
799
|
+
|
|
800
|
+
1. **Single-dash class names** read like English — `units-row`, `equal-height`, `padding`, `border-radius`
|
|
801
|
+
2. **No abbreviation guessing** — `padding-lg` not `pl-6`, `margin-top` not `mt-4`
|
|
802
|
+
3. **421 documented classes** in `ply-classes.json` — machine-readable reference
|
|
803
|
+
4. **Semantic HTML first** — auto-styled elements reduce class clutter
|
|
804
|
+
5. **AI-friendly aliases** — `btn-lg`/`btn-big`, `btn-sm`/`btn-small`, `alert`/`tools-alert` all work
|
|
805
|
+
|
|
806
|
+
### Alias Table
|
|
807
|
+
|
|
808
|
+
| Short | Long |
|
|
809
|
+
|---|---|
|
|
810
|
+
| `alert` | `tools-alert` |
|
|
811
|
+
| `alert-blue` | `tools-alert-blue` |
|
|
812
|
+
| `message` | `tools-message` |
|
|
813
|
+
| `btn-lg` | `btn-big` |
|
|
814
|
+
| `btn-sm` | `btn-small` |
|
|
815
|
+
| `btn-xs` | `btn-smaller` |
|
|
816
|
+
| `input-lg` | `input-big` |
|
|
817
|
+
| `input-sm` | `input-small` |
|
|
818
|
+
| `input-xs` | `input-smaller` |
|
|
819
|
+
| `li.active` | `li.on` (navbar) |
|
|
820
|
+
| `bg-black` | `background-black` |
|
|
821
|
+
| `bg-white` | `background-white` |
|
|
822
|
+
|
|
823
|
+
---
|
|
824
|
+
|
|
825
|
+
## Anti-Patterns
|
|
826
|
+
|
|
827
|
+
- **DON'T** use Tailwind, Bootstrap, or other frameworks with ply
|
|
828
|
+
- **DON'T** skip semantic HTML — use `<nav>`, `<code>`, `<table>`, `<details>`, `<dialog>` before `<div>`
|
|
829
|
+
- **DON'T** invent class names — only use classes from `ply-classes.json`
|
|
830
|
+
- **DON'T** use `role="button"` on links — use `<button>`
|
|
831
|
+
- **DON'T** put `unit-*` outside `units-row`
|
|
832
|
+
- **DON'T** hard-code colors — use `var(--ply-*)` custom properties
|
|
833
|
+
- **DON'T** forget the `.form` wrapper on forms
|
|
834
|
+
- **DON'T** override `outline: none` on interactive elements — it breaks focus visibility
|