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/PLY.md
ADDED
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
# PLY — AI-Ready CSS Framework
|
|
2
|
+
|
|
3
|
+
ply is a ratio-based, flexbox CSS framework with dark mode, accessibility defaults, and a small footprint (18.5KB gzip full, ~15KB core). 457 utility classes, 100+ CSS custom properties, 13 auto-styled semantic elements.
|
|
4
|
+
|
|
5
|
+
**Differentiators:** Small bundle, AI-parseable class system, accessible out of the box, dark mode built-in.
|
|
6
|
+
|
|
7
|
+
## Before Writing Custom CSS — Search ply-classes.json
|
|
8
|
+
|
|
9
|
+
**`ply-classes.json`** is the complete searchable reference. Before writing any custom CSS, search it first:
|
|
10
|
+
|
|
11
|
+
- **`classes`** — Every ply class (457) with category, description, and usage examples. Search here before inventing a class name or writing a custom style.
|
|
12
|
+
- **`customProperties`** — All `--ply-*` CSS variables organized by category (background, text, borders, interactive, forms, code, tables, buttons, navigation, elevation, brand, palette). Each entry includes light and dark mode values. Use these instead of hardcoding colors.
|
|
13
|
+
- **`semanticElements`** — Every HTML element ply auto-styles (`<dialog>`, `<details>`, `<table>`, `<code>`, `<kbd>`, `<mark>`, `<progress>`, `<meter>`, headings, form controls) with styling details and usage tips. Check here before building a custom component.
|
|
14
|
+
|
|
15
|
+
The JSON is the source of truth. If a class, variable, or semantic element already does what you need, use it instead of writing custom CSS.
|
|
16
|
+
|
|
17
|
+
## Custom Themes
|
|
18
|
+
|
|
19
|
+
Create a custom theme by defining a `data-theme` value and overriding `--ply-*` custom properties. Every ply component respects these variables, so one block themes the entire app.
|
|
20
|
+
|
|
21
|
+
```css
|
|
22
|
+
[data-theme="brand"] {
|
|
23
|
+
/* Colors */
|
|
24
|
+
--ply-bg-body: #fefce8;
|
|
25
|
+
--ply-bg-surface: #fef9c3;
|
|
26
|
+
--ply-bg-muted: #fef08a;
|
|
27
|
+
--ply-color-body: #1a1a1a;
|
|
28
|
+
--ply-color-headings: #78350f;
|
|
29
|
+
--ply-border-color: #fbbf24;
|
|
30
|
+
--ply-color-accent: #b45309;
|
|
31
|
+
--ply-btn-default-bg: #b45309;
|
|
32
|
+
--ply-btn-default-bg-hover: #92400e;
|
|
33
|
+
--ply-btn-default-bg-active: #7c2d12;
|
|
34
|
+
--ply-btn-secondary-bg: #78350f;
|
|
35
|
+
--ply-btn-border-radius: 0.5rem;
|
|
36
|
+
--ply-nav-bg: #fef3c7;
|
|
37
|
+
--ply-nav-border: #f59e0b;
|
|
38
|
+
|
|
39
|
+
/* Typography (optional) */
|
|
40
|
+
--ply-font-body: "Palatino Linotype", Palatino, Georgia, serif;
|
|
41
|
+
--ply-font-heading: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
42
|
+
--ply-font-mono: "Fira Code", "Source Code Pro", monospace;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```html
|
|
47
|
+
<html data-theme="brand">
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Setting a custom `data-theme` value prevents auto dark mode from interfering with the theme. The `--ply-font-body`, `--ply-font-heading`, and `--ply-font-mono` properties let you override typography alongside colors.
|
|
51
|
+
|
|
52
|
+
See `customProperties` in `ply-classes.json` for the full list of overridable variables.
|
|
53
|
+
|
|
54
|
+
### Custom Theme Browser Compatibility
|
|
55
|
+
|
|
56
|
+
ply uses `color-mix()` to auto-compute hover/active button states from your base color. This works in all modern browsers (Chrome 111+, Firefox 113+, Safari 16.4+, Edge 111+). On older browsers (pre-2023), `color-mix()` is ignored and the fallback hex values from ply's default theme are used instead. For custom themes targeting legacy browsers, also set `--ply-btn-default-bg-hover`, `--ply-btn-default-bg-active`, `--ply-btn-secondary-bg-hover`, and `--ply-btn-secondary-bg-active` explicitly. In modern browsers, `color-mix()` overrides these fallbacks automatically.
|
|
57
|
+
|
|
58
|
+
## Philosophy: Start Semantic
|
|
59
|
+
|
|
60
|
+
ply automatically styles semantic HTML elements — tables, code blocks, blockquotes, navs, details/summary, dialogs, progress bars, meters, forms, and more. Before reaching for a `<div>` with a custom class, check if a semantic element already does what you need. Custom styling is fine when you need it, but start with what HTML and ply give you for free.
|
|
61
|
+
|
|
62
|
+
```html
|
|
63
|
+
<!-- Start here — ply styles these automatically -->
|
|
64
|
+
<nav> <table> <code> <blockquote> <details> <dialog> <progress>
|
|
65
|
+
|
|
66
|
+
<!-- Then reach for ply classes when you need layout or variants -->
|
|
67
|
+
<div class="units-row"> <div class="alert alert-blue"> <button class="btn">
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Quick Start
|
|
71
|
+
|
|
72
|
+
### npm + Sass (recommended)
|
|
73
|
+
|
|
74
|
+
For any real project, install ply and work with the SCSS source. This gives you the full color palette, spacing variables, mixins, and the ability to customize everything at the Sass level.
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
npm install ply-css
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```scss
|
|
81
|
+
// Import all of ply
|
|
82
|
+
@use "ply-css/src/scss/ply" as *;
|
|
83
|
+
|
|
84
|
+
// Or import just what you need
|
|
85
|
+
@use "ply-css/src/scss/components/colors" as colors;
|
|
86
|
+
@use "ply-css/src/scss/components/variables" as variables;
|
|
87
|
+
@use "ply-css/src/scss/components/mixins" as mixins;
|
|
88
|
+
|
|
89
|
+
// Now you can use ply's Sass variables and mixins
|
|
90
|
+
.my-component {
|
|
91
|
+
color: colors.$color-blue;
|
|
92
|
+
background: colors.$color-blue-pastel;
|
|
93
|
+
@include mixins.border-bottom-radius(variables.$border-radius);
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The SCSS source lives in `src/scss/`. Key files:
|
|
98
|
+
- `components/_colors.scss` — Full color palette (brand colors, dark/light/pastel variants, neutral scale)
|
|
99
|
+
- `components/_variables.scss` — Spacing, font sizes, breakpoints, border radius
|
|
100
|
+
- `components/_mixins.scss` — Button generator, clearfix, gradients, arrows, animations
|
|
101
|
+
|
|
102
|
+
### CDN (prototyping only)
|
|
103
|
+
|
|
104
|
+
For quick demos or prototypes, drop in the CSS directly:
|
|
105
|
+
|
|
106
|
+
```html
|
|
107
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ply-css@1/dist/css/ply.min.css">
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Or the lean core bundle (no labels, dropdowns, loaders, print styles):
|
|
111
|
+
|
|
112
|
+
```html
|
|
113
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ply-css@1/dist/css/ply-core.min.css">
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Note: The CDN approach gives you ply's classes and dark mode, but you won't have access to Sass variables, the full color palette, or mixins for custom components.
|
|
117
|
+
|
|
118
|
+
## Icons
|
|
119
|
+
|
|
120
|
+
ply does not include icons. [Feather Icons](https://feathericons.com) is the recommended icon library — it's lightweight, clean, and pairs well with ply's aesthetic. Any icon library works.
|
|
121
|
+
|
|
122
|
+
```html
|
|
123
|
+
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
|
124
|
+
<script>feather.replace();</script>
|
|
125
|
+
|
|
126
|
+
<!-- Usage -->
|
|
127
|
+
<i data-feather="check"></i> Saved
|
|
128
|
+
<button class="btn btn-blue"><i data-feather="send"></i> Send</button>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Dark Mode
|
|
132
|
+
|
|
133
|
+
ply auto-detects `prefers-color-scheme: dark` only when no `data-theme` attribute is set on `<html>`. This means custom themes (e.g., `data-theme="warm"`) won't have dark styles applied over them. To manually set a mode:
|
|
134
|
+
|
|
135
|
+
```html
|
|
136
|
+
<!-- Auto (OS preference) — no data-theme attribute -->
|
|
137
|
+
<html>
|
|
138
|
+
|
|
139
|
+
<!-- Explicit light mode -->
|
|
140
|
+
<html data-theme="light">
|
|
141
|
+
|
|
142
|
+
<!-- Explicit dark mode -->
|
|
143
|
+
<html data-theme="dark">
|
|
144
|
+
|
|
145
|
+
<!-- Custom theme (no auto dark mode interference) -->
|
|
146
|
+
<html data-theme="warm">
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Toggle with JavaScript:
|
|
150
|
+
```js
|
|
151
|
+
document.documentElement.dataset.theme =
|
|
152
|
+
document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Container Queries
|
|
158
|
+
|
|
159
|
+
Container queries let units respond to their parent's width instead of the viewport. This is useful for reusable components (cards, widgets, sidebars) that need to adapt based on the space they're placed in, not the screen size.
|
|
160
|
+
|
|
161
|
+
**Wrapper:** Add `container-query` to the parent element to enable container queries.
|
|
162
|
+
|
|
163
|
+
**Container prefixes** mirror viewport prefixes with a `container-` prefix:
|
|
164
|
+
|
|
165
|
+
| Container prefix | Breakpoint | Mirrors |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| `container-phone-unit-*` | 480px | `phone-unit-*` |
|
|
168
|
+
| `container-large-phone-unit-*` | 650px | `large-phone-unit-*` |
|
|
169
|
+
| `container-tablet-unit-*` | 767px | `tablet-unit-*` |
|
|
170
|
+
| `container-small-desktop-unit-*` | 1024px | `small-desktop-unit-*` |
|
|
171
|
+
|
|
172
|
+
All 21 unit sizes are available (100, 90, 88, 80, 75, 70, 66, 65, 62, 60, 50, 40, 38, 35, 33, 30, 25, 20, 12, 10, auto).
|
|
173
|
+
|
|
174
|
+
Container query classes use `@container` rules instead of `@media` — they fire based on the `.container-query` element's inline size. Mix viewport and container prefixes freely on the same element.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Class Reference — See ply-classes.json
|
|
179
|
+
|
|
180
|
+
All classes, CSS custom properties, and semantic element styles are documented in **`ply-classes.json`**. Search it for class names, categories, descriptions, and usage examples. The JSON is the source of truth — do not invent class names that aren't in it.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Usage Rules
|
|
185
|
+
|
|
186
|
+
1. **ply is standalone** — Do NOT use Tailwind, Bootstrap, or other CSS frameworks alongside ply. Remove them first.
|
|
187
|
+
2. **Always wrap units in `units-row`** — `unit-*` classes must be direct children of `units-row`.
|
|
188
|
+
3. **Use `<button>` for buttons, not `<a>`** — Links are for navigation, buttons for actions.
|
|
189
|
+
4. **Wrap forms in `.form`** for styled inputs — Without the wrapper, inputs get minimal styling.
|
|
190
|
+
5. **Use semantic HTML first** — ply automatically styles `<code>`, `<pre>`, `<kbd>`, `<blockquote>`, `<mark>`, `<table>`, `<details>`, `<summary>`, `<dialog>`, `<progress>`, `<meter>`, `<nav>`, `<hr>`, and heading tags. Use the native element before creating custom classes.
|
|
191
|
+
6. **Only use classes documented here** — Do NOT invent utility classes (e.g. `.color-gray-60` does not exist). If ply doesn't have a class for something, use a CSS custom property or write a small custom rule.
|
|
192
|
+
7. **Use `units-container` for page width** — Centers content at 1200px max-width.
|
|
193
|
+
8. **Add responsive classes for mobile** — At minimum use `tablet-unit-100` to stack on tablets.
|
|
194
|
+
9. **Use CSS custom properties for theming** — All colors, backgrounds, and borders are customizable via `--ply-*` variables. Do not hard-code colors that break dark mode.
|
|
195
|
+
10. **Use single-dash class names** — `navbar-centered`, `display-flex`, `margin-top-extra`.
|
|
196
|
+
|
|
197
|
+
### Button Hierarchy
|
|
198
|
+
|
|
199
|
+
- **`btn-primary`** — Primary call-to-action. Blue by default, themed via `--ply-btn-default-bg`. WCAG AA compliant (4.56:1 contrast on white).
|
|
200
|
+
- **`btn-secondary`** (or plain `btn`) — Secondary actions. Dark gray by default, themed via `--ply-btn-secondary-bg`.
|
|
201
|
+
- **`btn-primary-outline`** / **`btn-secondary-outline`** — Outlined variants. Transparent bg, border + text from the respective theme color. Fills on hover.
|
|
202
|
+
- **`btn-ghost`** — Ghost button. Text-only with subtle hover tint.
|
|
203
|
+
- **`btn-blue`**, **`btn-red`**, **`btn-green`**, **`btn-yellow`** — Static color buttons with hardcoded hex values. Immune to theming. Use for color-coded actions (e.g., delete = red, success = green).
|
|
204
|
+
|
|
205
|
+
```html
|
|
206
|
+
<!-- Primary + Secondary pair -->
|
|
207
|
+
<button class="btn btn-primary">Save</button>
|
|
208
|
+
<button class="btn btn-secondary">Cancel</button>
|
|
209
|
+
|
|
210
|
+
<!-- Themed: --ply-btn-default-bg controls primary + links -->
|
|
211
|
+
<html data-theme="warm">
|
|
212
|
+
<style>[data-theme="warm"] { --ply-btn-default-bg: #92400e; }</style>
|
|
213
|
+
|
|
214
|
+
<!-- Static: btn-blue stays blue regardless of theme -->
|
|
215
|
+
<button class="btn btn-blue">Always Blue</button>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Icon Buttons
|
|
219
|
+
|
|
220
|
+
- **`btn-icon`** — Icon-only button modifier. Equal padding for a square aspect ratio. Always add `aria-label` (no visible text).
|
|
221
|
+
- Combine with `btn-ghost` for toolbar-style icon buttons. Combine with size modifiers (`btn-sm`, `btn-xs`) for smaller icons.
|
|
222
|
+
- For icon + text buttons, use a regular `btn` with an inline SVG — no `btn-icon` needed.
|
|
223
|
+
|
|
224
|
+
## Common Patterns
|
|
225
|
+
|
|
226
|
+
### Responsive Collapsible Header (CSS-Only)
|
|
227
|
+
|
|
228
|
+
Use `<details>`/`<summary>` as a hamburger toggle for mobile. No JavaScript required. The nav shows inline on desktop and collapses behind a toggle on mobile.
|
|
229
|
+
|
|
230
|
+
```html
|
|
231
|
+
<header class="sticky" style="top: 0; z-index: 100;">
|
|
232
|
+
<nav class="navbar" aria-label="Main navigation">
|
|
233
|
+
<!-- Desktop nav — hidden on mobile -->
|
|
234
|
+
<ul class="phone-hide">
|
|
235
|
+
<li class="active"><a href="#">Home</a></li>
|
|
236
|
+
<li><a href="#">About</a></li>
|
|
237
|
+
<li><a href="#">Services</a></li>
|
|
238
|
+
<li><a href="#">Contact</a></li>
|
|
239
|
+
</ul>
|
|
240
|
+
<!-- Mobile hamburger — hidden on desktop -->
|
|
241
|
+
<details class="tablet-hide desktop-hide">
|
|
242
|
+
<summary aria-label="Menu">☰ Menu</summary>
|
|
243
|
+
<ul>
|
|
244
|
+
<li class="active"><a href="#">Home</a></li>
|
|
245
|
+
<li><a href="#">About</a></li>
|
|
246
|
+
<li><a href="#">Services</a></li>
|
|
247
|
+
<li><a href="#">Contact</a></li>
|
|
248
|
+
</ul>
|
|
249
|
+
</details>
|
|
250
|
+
</nav>
|
|
251
|
+
</header>
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**How it works:**
|
|
255
|
+
- `sticky` + `top: 0` keeps the header pinned on scroll.
|
|
256
|
+
- `phone-hide` hides the desktop `<ul>` on small screens (≤ 767px).
|
|
257
|
+
- `tablet-hide desktop-hide` shows the `<details>` toggle only on mobile.
|
|
258
|
+
- `<details>`/`<summary>` is natively keyboard-accessible — Enter/Space toggles it, no JS needed.
|
|
259
|
+
- Start mobile-first: design the collapsed state first, then override with wider breakpoints.
|
|
260
|
+
|
|
261
|
+
**Always include the viewport meta tag** in your `<head>` for responsive behavior:
|
|
262
|
+
```html
|
|
263
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
See `snippets/responsive-header.html` for a full working example.
|
|
267
|
+
|
|
268
|
+
### Borders
|
|
269
|
+
|
|
270
|
+
- **`border`** — 1px solid border (all sides). Uses `var(--ply-border-color)`.
|
|
271
|
+
- **`border-top`**, **`border-right`**, **`border-bottom`**, **`border-left`** — Single-side borders.
|
|
272
|
+
- **`border-thick`** — 3px solid border (all sides). Also `border-top-thick`, `border-right-thick`, `border-bottom-thick`, `border-left-thick`.
|
|
273
|
+
- **`no-border`** — Remove all borders. Also `no-border-top`, `no-border-right`, `no-border-bottom`, `no-border-left`.
|
|
274
|
+
- **`border-radius`** — Default border radius. `border-radius-lg` (0.75rem), `border-radius-xl` (1.5rem), `circle` (100%).
|
|
275
|
+
|
|
276
|
+
### Other Common Patterns
|
|
277
|
+
|
|
278
|
+
- **Equal-height cards** — Add `equal-height` to `units-row` so all children stretch to the tallest
|
|
279
|
+
- **Gap between flex/grid children** — Use `gap-xs`, `gap-sm`, `gap`, `gap-lg`, `gap-xl` instead of margin hacks
|
|
280
|
+
- **Prevent orphaned words** — Use `no-orphan` on paragraphs, `text-balance` on headings
|
|
281
|
+
- **Card-style links** — Use `no-link-style` on a container to suppress link color/underline on all `<a>` inside
|
|
282
|
+
- **Navbar variants** — Default is a thin border. Use `navbar-thick`, `navbar-borderless`, or `navbar-border-blue/green/red/yellow`
|
|
283
|
+
- **Text color hierarchy** — `text-primary`, `text-secondary`, `text-tertiary` (all theme-aware)
|
|
284
|
+
|
|
285
|
+
## Accessibility (WCAG 2.1 AA)
|
|
286
|
+
|
|
287
|
+
ply is built for Section 508 / WCAG 2.1 AA compliance out of the box:
|
|
288
|
+
|
|
289
|
+
- **Focus indicators** — All interactive elements (buttons, links, inputs, nav items, dropdowns) use `:focus-visible` with a 2px blue outline. Keyboard users see clear focus rings; mouse users don't.
|
|
290
|
+
- **High contrast mode** — `@media (prefers-contrast: more)` is supported. Text colors and link colors become stronger for users who need more contrast.
|
|
291
|
+
- **Reduced motion** — `@media (prefers-reduced-motion: reduce)` disables animations and transitions.
|
|
292
|
+
- **Dark mode** — `prefers-color-scheme: dark` is respected automatically when no `data-theme` attribute is set. Theme-aware colors maintain WCAG AA contrast in both modes.
|
|
293
|
+
- **Skip link** — Use `.skip-link` as the first focusable element to let keyboard users skip past navigation.
|
|
294
|
+
- **Screen reader support** — `.sr-only` hides content visually while keeping it accessible to assistive technology.
|
|
295
|
+
- **Sortable table headers** — `th.sortable` elements have `:focus-visible` outlines for keyboard users.
|
|
296
|
+
- **Pagination** — Focus-visible outlines on all page links, disabled states prevent interaction, `aria-current="page"` supported on active items.
|
|
297
|
+
- **Dialog patterns** — Dialog open/close respects `prefers-reduced-motion` (no animation when reduced motion is preferred).
|
|
298
|
+
- **Multi-step forms** — Step transitions respect `prefers-reduced-motion`.
|
|
299
|
+
- **RTL support** — `dir="rtl"` enables proper internationalization with automatic layout mirroring for Arabic, Hebrew, and other RTL languages.
|
|
300
|
+
|
|
301
|
+
```html
|
|
302
|
+
<!-- Skip link — first element inside <body> -->
|
|
303
|
+
<a href="#main" class="skip-link">Skip to main content</a>
|
|
304
|
+
<nav class="navbar">...</nav>
|
|
305
|
+
<main id="main">...</main>
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Title II / WCAG 2.1 AA Compliance
|
|
309
|
+
|
|
310
|
+
ply targets ADA Title II compliance (28 CFR Part 35) by meeting WCAG 2.1 Level AA success criteria at the framework level. As of June 2024, state and local government web content must conform to WCAG 2.1 AA. ply provides a compliant foundation so that applications built on it start closer to full conformance.
|
|
311
|
+
|
|
312
|
+
### WCAG criteria ply satisfies at the framework level
|
|
313
|
+
|
|
314
|
+
| WCAG Criterion | How ply addresses it |
|
|
315
|
+
|----------------|---------------------|
|
|
316
|
+
| **1.3.1 Info and Relationships** | Semantic HTML auto-styling encourages correct use of `<nav>`, `<main>`, `<aside>`, `<table>`, `<details>`, headings, and form elements. `.sr-only` exposes supplemental info to assistive technology. |
|
|
317
|
+
| **1.4.3 Contrast (Minimum)** | All default color pairings (text on background, button labels, link colors) meet 4.5:1 contrast in both light and dark modes. The brand palette levels are tuned for AA. |
|
|
318
|
+
| **1.4.6 Contrast (Enhanced)** | `@media (prefers-contrast: more)` strengthens text and link colors beyond AA minimums. |
|
|
319
|
+
| **1.4.11 Non-Text Contrast** | Focus indicators, button borders, and input borders all meet the 3:1 non-text contrast ratio. |
|
|
320
|
+
| **2.1.1 Keyboard** | All ply interactive elements (buttons, links, inputs, nav items, `<details>`, `<dialog>`) are natively keyboard-operable. No custom JS required. |
|
|
321
|
+
| **2.3.3 Animation from Interactions** | `@media (prefers-reduced-motion: reduce)` disables all CSS animations and transitions. |
|
|
322
|
+
| **2.4.1 Bypass Blocks** | `.skip-link` lets keyboard users jump past navigation to main content. |
|
|
323
|
+
| **2.4.7 Focus Visible** | `:focus-visible` outlines appear on every interactive element. Mouse users do not see them; keyboard users always do. |
|
|
324
|
+
| **4.1.2 Name, Role, Value** | ply ships zero JavaScript. All interactivity comes from native HTML elements (`<button>`, `<a>`, `<input>`, `<dialog>`, `<details>`), which expose correct roles and states automatically. No ARIA state management failures from framework code. |
|
|
325
|
+
|
|
326
|
+
### WCAG criteria that require application-level work
|
|
327
|
+
|
|
328
|
+
These criteria depend on content and custom code — ply cannot enforce them automatically:
|
|
329
|
+
|
|
330
|
+
| WCAG Criterion | What you need to do |
|
|
331
|
+
|----------------|---------------------|
|
|
332
|
+
| **1.1.1 Non-text Content** | Add `alt` attributes to all `<img>` elements. Use `alt=""` for purely decorative images. Provide text alternatives for `<svg>` icons (`aria-label` or `<title>`). |
|
|
333
|
+
| **1.3.1 Info and Relationships** (content) | Use one `<h1>` per page. Follow heading hierarchy (h1 > h2 > h3) without skipping levels. Use `<label>` elements for form inputs. Group related form fields with `<fieldset>` and `<legend>`. |
|
|
334
|
+
| **1.3.2 Meaningful Sequence** | Ensure DOM order matches visual order. ply's `reverse-direction` class reverses visual order but not DOM order — use it only when the visual reorder is cosmetic. |
|
|
335
|
+
| **1.4.1 Use of Color** | Do not rely on color alone to convey information. Pair colored status indicators (`.success`, `.error`) with text labels or icons. |
|
|
336
|
+
| **1.4.3 Contrast (Minimum)** (custom colors) | If you override `--ply-*` variables or add custom colors, verify 4.5:1 contrast for normal text and 3:1 for large text (18px bold / 24px regular) and UI components. |
|
|
337
|
+
| **2.1.1 Keyboard** (custom widgets) | Custom JavaScript components (tabs, carousels, drag-and-drop, custom dropdowns) must be fully operable with keyboard alone. Manage `tabindex`, arrow key navigation, and Escape to close. |
|
|
338
|
+
| **2.4.2 Page Titled** | Every page needs a descriptive `<title>` element. |
|
|
339
|
+
| **2.4.4 Link Purpose** | Link text should describe the destination. Avoid "click here" — use descriptive text or add `aria-label` when the visible text is ambiguous. |
|
|
340
|
+
| **2.4.6 Headings and Labels** | Headings and form labels must describe their content or purpose. ply styles them, but you write the text. |
|
|
341
|
+
| **3.1.1 Language of Page** | Set `lang` attribute on `<html>` (e.g., `<html lang="en">`). |
|
|
342
|
+
| **3.3.1 Error Identification** | When form validation fails, identify the error in text (not just color). Use `input-error` alongside a visible error message. |
|
|
343
|
+
| **3.3.2 Labels or Instructions** | Provide visible labels for form inputs. ply's `.form` wrapper styles `<label>` elements — use them. |
|
|
344
|
+
| **4.1.2 Name, Role, Value** (custom widgets) | Custom interactive widgets (JS-powered dropdowns, modals, tab panels) need ARIA attributes: `aria-expanded`, `aria-controls`, `aria-selected`, `role="tablist"`, etc. |
|
|
345
|
+
|
|
346
|
+
### AI agent guidance for accessible markup
|
|
347
|
+
|
|
348
|
+
When generating ply markup, follow these practices to produce Title II compliant output:
|
|
349
|
+
|
|
350
|
+
1. **Use semantic elements for landmarks** — `<header>`, `<nav>`, `<main>`, `<aside>`, `<footer>`, `<section>`, `<article>`. Never use `<div>` for structural roles.
|
|
351
|
+
2. **Add `aria-label` to custom widgets** — Any interactive element without a visible text label needs `aria-label` or `aria-labelledby`.
|
|
352
|
+
3. **Use `<form role="search">` for search forms** — This creates a search landmark for screen reader users.
|
|
353
|
+
4. **Include `.skip-link` in page templates** — Add `<a href="#main" class="skip-link">Skip to main content</a>` as the first focusable element inside `<body>`.
|
|
354
|
+
5. **Add `alt` text to all images** — Describe the image content. Use `alt=""` only for decorative images.
|
|
355
|
+
6. **Maintain heading hierarchy** — Start with `<h1>`, follow with `<h2>`, then `<h3>`. Never skip levels. Use `.h1`-`.h6` classes for visual sizing when the semantic level differs.
|
|
356
|
+
7. **Label all form inputs** — Wrap inputs in `<label>` or use `for`/`id` pairing. Add `<fieldset>` and `<legend>` for radio/checkbox groups.
|
|
357
|
+
8. **Set `lang` on `<html>`** — Always include `<html lang="en">` (or the appropriate language code).
|
|
358
|
+
9. **Pair color with text** — When using `.success` or `.error` classes, include a text label (not just color) to convey meaning.
|
|
359
|
+
10. **Use native elements over ARIA** — Prefer `<button>` over `<div role="button">`, `<dialog>` over `<div role="dialog">`, `<details>` over custom accordion JS. Native elements handle keyboard and ARIA automatically.
|
|
360
|
+
|
|
361
|
+
## Focus Management & Keyboard Patterns
|
|
362
|
+
|
|
363
|
+
ply provides `:focus-visible` outlines on all native interactive elements. For custom widgets built with JavaScript, you need to manage focus order, keyboard navigation, and screen reader announcements yourself.
|
|
364
|
+
|
|
365
|
+
### Focus Order Strategy
|
|
366
|
+
|
|
367
|
+
1. **Use semantic HTML first** — Native `<button>`, `<a>`, `<input>`, `<select>`, `<details>`, and `<dialog>` are already in the tab order. No extra work needed.
|
|
368
|
+
2. **`tabindex="0"`** — Add to custom interactive elements (e.g., a `<div>` that acts as a button) to include them in the natural tab order. ply's `:focus-visible` outline will apply automatically.
|
|
369
|
+
3. **`tabindex="-1"`** — Use for elements that should receive focus programmatically (e.g., a modal container after opening, an error message after form validation) but should NOT be in the tab order.
|
|
370
|
+
4. **Never use `tabindex` > 0** — It overrides natural DOM order and creates confusing navigation.
|
|
371
|
+
|
|
372
|
+
```html
|
|
373
|
+
<!-- Native elements — already focusable, no tabindex needed -->
|
|
374
|
+
<button class="btn">Save</button>
|
|
375
|
+
<a href="/settings">Settings</a>
|
|
376
|
+
|
|
377
|
+
<!-- Custom interactive element — add tabindex="0" -->
|
|
378
|
+
<div role="button" tabindex="0" onclick="doAction()" onkeydown="if(event.key==='Enter'||event.key===' ')doAction()">
|
|
379
|
+
Custom Action
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
<!-- Programmatic focus target — tabindex="-1" -->
|
|
383
|
+
<div id="error-summary" tabindex="-1" role="alert">
|
|
384
|
+
Please fix the errors below.
|
|
385
|
+
</div>
|
|
386
|
+
<script>
|
|
387
|
+
// After form validation fails, move focus to the error summary
|
|
388
|
+
document.getElementById('error-summary').focus();
|
|
389
|
+
</script>
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Arrow Key Navigation (Roving Tabindex)
|
|
393
|
+
|
|
394
|
+
For grouped controls like tabs, toolbars, or menu items, use roving tabindex: only one item in the group has `tabindex="0"` (the active one), the rest have `tabindex="-1"`. Arrow keys move focus within the group.
|
|
395
|
+
|
|
396
|
+
```html
|
|
397
|
+
<!-- Roving tabindex — only the active tab is in the tab order -->
|
|
398
|
+
<div role="tablist" aria-label="Settings">
|
|
399
|
+
<button role="tab" aria-selected="true" tabindex="0" aria-controls="panel-general" id="tab-general">General</button>
|
|
400
|
+
<button role="tab" aria-selected="false" tabindex="-1" aria-controls="panel-security" id="tab-security">Security</button>
|
|
401
|
+
<button role="tab" aria-selected="false" tabindex="-1" aria-controls="panel-notifications" id="tab-notifications">Notifications</button>
|
|
402
|
+
</div>
|
|
403
|
+
<div role="tabpanel" id="panel-general" aria-labelledby="tab-general">
|
|
404
|
+
<p>General settings content.</p>
|
|
405
|
+
</div>
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
```js
|
|
409
|
+
// Arrow key handler for roving tabindex
|
|
410
|
+
tablist.addEventListener('keydown', (e) => {
|
|
411
|
+
const tabs = [...tablist.querySelectorAll('[role="tab"]')];
|
|
412
|
+
const current = tabs.indexOf(document.activeElement);
|
|
413
|
+
let next;
|
|
414
|
+
if (e.key === 'ArrowRight') next = (current + 1) % tabs.length;
|
|
415
|
+
else if (e.key === 'ArrowLeft') next = (current - 1 + tabs.length) % tabs.length;
|
|
416
|
+
else if (e.key === 'Home') next = 0;
|
|
417
|
+
else if (e.key === 'End') next = tabs.length - 1;
|
|
418
|
+
else return;
|
|
419
|
+
e.preventDefault();
|
|
420
|
+
tabs[current].setAttribute('tabindex', '-1');
|
|
421
|
+
tabs[current].setAttribute('aria-selected', 'false');
|
|
422
|
+
tabs[next].setAttribute('tabindex', '0');
|
|
423
|
+
tabs[next].setAttribute('aria-selected', 'true');
|
|
424
|
+
tabs[next].focus();
|
|
425
|
+
// Show corresponding panel
|
|
426
|
+
tabs.forEach((tab, i) => {
|
|
427
|
+
document.getElementById(tab.getAttribute('aria-controls')).hidden = (i !== next);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### ARIA Attributes Reference
|
|
433
|
+
|
|
434
|
+
| Attribute | When to use | Example |
|
|
435
|
+
|-----------|-------------|---------|
|
|
436
|
+
| `aria-expanded` | Toggleable content (dropdowns, accordions, hamburger menus) | `<button aria-expanded="false" aria-controls="menu">Menu</button>` |
|
|
437
|
+
| `aria-controls` | Links a trigger to the element it controls | `<button aria-controls="dropdown-1">Options</button>` |
|
|
438
|
+
| `aria-selected` | Active item in tabs, listboxes | `<button role="tab" aria-selected="true">Tab 1</button>` |
|
|
439
|
+
| `aria-current="page"` | Current page in navigation | `<a href="/" aria-current="page">Home</a>` |
|
|
440
|
+
| `aria-live="polite"` | Dynamic content that updates (notifications, status messages) | `<div aria-live="polite">3 results found.</div>` |
|
|
441
|
+
| `aria-live="assertive"` | Urgent announcements (errors) | `<div aria-live="assertive" role="alert">Session expired.</div>` |
|
|
442
|
+
| `role="status"` | Non-urgent status updates | `<div role="status">Saving...</div>` |
|
|
443
|
+
| `aria-label` | Labels for elements without visible text | `<button aria-label="Close">×</button>` |
|
|
444
|
+
| `aria-labelledby` | Points to another element for its label | `<div role="tabpanel" aria-labelledby="tab-1">` |
|
|
445
|
+
| `aria-describedby` | Additional description for an element | `<input aria-describedby="password-hint">` |
|
|
446
|
+
|
|
447
|
+
### Live Regions for Dynamic Content
|
|
448
|
+
|
|
449
|
+
When content updates without a page reload (e.g., search results, form validation, notifications), screen readers need to be told about the change.
|
|
450
|
+
|
|
451
|
+
```html
|
|
452
|
+
<!-- Status message — announced politely after current speech -->
|
|
453
|
+
<div aria-live="polite" role="status" class="sr-only" id="search-status"></div>
|
|
454
|
+
<script>
|
|
455
|
+
document.getElementById('search-status').textContent = '12 results found.';
|
|
456
|
+
</script>
|
|
457
|
+
|
|
458
|
+
<!-- Error alert — announced immediately -->
|
|
459
|
+
<div aria-live="assertive" role="alert">
|
|
460
|
+
Your session has expired. Please log in again.
|
|
461
|
+
</div>
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### Fieldset & Legend for Grouped Controls
|
|
465
|
+
|
|
466
|
+
Group related radio buttons and checkboxes with `<fieldset>` and `<legend>`. This tells screen readers the group label.
|
|
467
|
+
|
|
468
|
+
```html
|
|
469
|
+
<form class="form">
|
|
470
|
+
<fieldset>
|
|
471
|
+
<legend>Notification preferences</legend>
|
|
472
|
+
<label><input type="checkbox" name="notify" value="email"> Email</label>
|
|
473
|
+
<label><input type="checkbox" name="notify" value="sms"> SMS</label>
|
|
474
|
+
<label><input type="checkbox" name="notify" value="push"> Push notification</label>
|
|
475
|
+
</fieldset>
|
|
476
|
+
<fieldset>
|
|
477
|
+
<legend>Frequency</legend>
|
|
478
|
+
<label><input type="radio" name="freq" value="immediate"> Immediate</label>
|
|
479
|
+
<label><input type="radio" name="freq" value="daily"> Daily digest</label>
|
|
480
|
+
<label><input type="radio" name="freq" value="weekly"> Weekly digest</label>
|
|
481
|
+
</fieldset>
|
|
482
|
+
</form>
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Screen Reader Testing Quick Reference
|
|
486
|
+
|
|
487
|
+
Test with VoiceOver (macOS) to verify your custom widgets are accessible:
|
|
488
|
+
|
|
489
|
+
| Action | Shortcut |
|
|
490
|
+
|--------|----------|
|
|
491
|
+
| Turn on/off VoiceOver | Cmd + F5 |
|
|
492
|
+
| Navigate next element | VO + Right Arrow (VO = Ctrl + Option) |
|
|
493
|
+
| Navigate previous element | VO + Left Arrow |
|
|
494
|
+
| Activate (click) element | VO + Space |
|
|
495
|
+
| Read current element | VO + F3 |
|
|
496
|
+
| Open rotor (landmarks, headings, links) | VO + U |
|
|
497
|
+
| Enter/exit groups | VO + Shift + Down/Up |
|
|
498
|
+
|
|
499
|
+
**What to check:**
|
|
500
|
+
- Every interactive element is reachable with Tab or arrow keys
|
|
501
|
+
- Buttons and links announce their label and role
|
|
502
|
+
- Expanded/collapsed state is announced when toggling (`aria-expanded`)
|
|
503
|
+
- Dynamic content changes are announced (`aria-live`)
|
|
504
|
+
- Form inputs announce their label, required state, and error messages
|
|
505
|
+
- Heading hierarchy is logical (use rotor > Headings to verify)
|
|
506
|
+
|
|
507
|
+
## Custom Widget Accessibility Patterns
|
|
508
|
+
|
|
509
|
+
When building custom interactive widgets beyond ply's built-in components, follow these patterns to maintain WCAG 2.1 AA compliance.
|
|
510
|
+
|
|
511
|
+
### Drag-and-Drop Keyboard Pattern
|
|
512
|
+
|
|
513
|
+
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 to announce changes:
|
|
514
|
+
|
|
515
|
+
```html
|
|
516
|
+
<div class="sr-only" aria-live="assertive" id="dnd-status"></div>
|
|
517
|
+
<p class="text-secondary text-sm">Keyboard: Tab to list, Space to grab, ↑↓ to move, Enter to drop, Escape to cancel.</p>
|
|
518
|
+
<ul role="listbox" aria-label="Sortable list" id="sortable-list">
|
|
519
|
+
<li role="option" tabindex="0" aria-grabbed="false" data-index="0">Item 1</li>
|
|
520
|
+
<li role="option" tabindex="-1" aria-grabbed="false" data-index="1">Item 2</li>
|
|
521
|
+
<li role="option" tabindex="-1" aria-grabbed="false" data-index="2">Item 3</li>
|
|
522
|
+
</ul>
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
```js
|
|
526
|
+
const list = document.getElementById('sortable-list');
|
|
527
|
+
const status = document.getElementById('dnd-status');
|
|
528
|
+
let grabbed = null;
|
|
529
|
+
|
|
530
|
+
list.addEventListener('keydown', (e) => {
|
|
531
|
+
const item = e.target;
|
|
532
|
+
if (item.role !== 'option') return;
|
|
533
|
+
|
|
534
|
+
if (e.key === ' ' && !grabbed) {
|
|
535
|
+
e.preventDefault();
|
|
536
|
+
grabbed = item;
|
|
537
|
+
item.setAttribute('aria-grabbed', 'true');
|
|
538
|
+
status.textContent = `Grabbed ${item.textContent}. Use arrow keys to move, Enter to drop.`;
|
|
539
|
+
} else if (e.key === 'ArrowDown' && grabbed) {
|
|
540
|
+
e.preventDefault();
|
|
541
|
+
const next = grabbed.nextElementSibling;
|
|
542
|
+
if (next) { list.insertBefore(next, grabbed); status.textContent = `Moved down to position ${[...list.children].indexOf(grabbed) + 1}.`; }
|
|
543
|
+
} else if (e.key === 'ArrowUp' && grabbed) {
|
|
544
|
+
e.preventDefault();
|
|
545
|
+
const prev = grabbed.previousElementSibling;
|
|
546
|
+
if (prev) { list.insertBefore(grabbed, prev); status.textContent = `Moved up to position ${[...list.children].indexOf(grabbed) + 1}.`; }
|
|
547
|
+
} else if (e.key === 'Enter' && grabbed) {
|
|
548
|
+
e.preventDefault();
|
|
549
|
+
grabbed.setAttribute('aria-grabbed', 'false');
|
|
550
|
+
status.textContent = `Dropped ${grabbed.textContent} at position ${[...list.children].indexOf(grabbed) + 1}.`;
|
|
551
|
+
grabbed = null;
|
|
552
|
+
} else if (e.key === 'Escape' && grabbed) {
|
|
553
|
+
e.preventDefault();
|
|
554
|
+
grabbed.setAttribute('aria-grabbed', 'false');
|
|
555
|
+
status.textContent = 'Reorder cancelled.';
|
|
556
|
+
grabbed = null;
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
See `snippets/accessible-drag-and-drop.html` for a complete working example.
|
|
562
|
+
|
|
563
|
+
### Focus Trap for Modals
|
|
564
|
+
|
|
565
|
+
Prefer native `<dialog>` — ply styles it automatically and the browser handles focus trapping. For custom modals, trap Tab/Shift+Tab and set `aria-modal="true"`:
|
|
566
|
+
|
|
567
|
+
```js
|
|
568
|
+
function trapFocus(modal) {
|
|
569
|
+
const focusable = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
570
|
+
const first = focusable[0], last = focusable[focusable.length - 1];
|
|
571
|
+
modal.addEventListener('keydown', (e) => {
|
|
572
|
+
if (e.key !== 'Tab') return;
|
|
573
|
+
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
|
|
574
|
+
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
|
|
575
|
+
});
|
|
576
|
+
first?.focus();
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### Focus Return After Close
|
|
581
|
+
|
|
582
|
+
Always restore focus to the element that triggered the widget:
|
|
583
|
+
|
|
584
|
+
```js
|
|
585
|
+
const trigger = document.activeElement; // Store before opening
|
|
586
|
+
modal.showModal();
|
|
587
|
+
modal.addEventListener('close', () => trigger.focus(), { once: true });
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### Custom Widget Checklist
|
|
591
|
+
|
|
592
|
+
| Requirement | WCAG | How to verify |
|
|
593
|
+
|---|---|---|
|
|
594
|
+
| Keyboard operable (no mouse required) | 2.1.1 | Tab, Enter, Space, arrows all work |
|
|
595
|
+
| Focus indicator visible | 2.4.7 | ply provides `:focus-visible` — don't override `outline: none` |
|
|
596
|
+
| States announced to screen readers | 4.1.2 | `aria-grabbed`, `aria-expanded`, `aria-selected` update on interaction |
|
|
597
|
+
| Focus trapped in modals | 2.4.3 | Tab does not leave an open dialog |
|
|
598
|
+
| Focus restored on close | 2.4.3 | Closing returns focus to the trigger element |
|
|
599
|
+
| Touch targets ≥ 44×44px | 2.5.5 | Measure interactive elements |
|
|
600
|
+
| Alternative input available | 2.1.1 | Drag-and-drop has keyboard fallback |
|
|
601
|
+
| Screen reader tested | 4.1.2 | Verify with VoiceOver (macOS) or NVDA (Windows) |
|
|
602
|
+
|
|
603
|
+
## Anti-Patterns
|
|
604
|
+
|
|
605
|
+
- **DON'T** skip semantic HTML — Before adding `<div class="something">`, check if a semantic element works. ply styles `<nav>`, `<code>`, `<table>`, `<details>`, `<dialog>`, `<blockquote>`, etc. automatically.
|
|
606
|
+
- **DON'T** use Tailwind, Bootstrap, or other frameworks with ply — They will conflict.
|
|
607
|
+
- **DON'T** create custom classes for elements ply already styles — Use `<code>` not `.code-example`, use `<blockquote>` not `.quote-block`, etc.
|
|
608
|
+
- **DON'T** invent ply class names — Only use classes from this reference (e.g. `.color-gray-60` does not exist — use `text-secondary` or `text-tertiary` instead).
|
|
609
|
+
- **DON'T** use `role="button"` on links — Use actual `<button>` elements.
|
|
610
|
+
- **DON'T** put `unit-*` classes outside a `units-row` — They won't work correctly.
|
|
611
|
+
- **DON'T** use inline styles for layout — Use the grid system instead.
|
|
612
|
+
- **DON'T** forget the `.form` wrapper — Without it, form elements won't be styled.
|
|
613
|
+
- **DON'T** hard-code colors — Use `var(--ply-color-*)` custom properties so dark mode works correctly.
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
## Copy-Paste Snippets — See `snippets/`
|
|
618
|
+
|
|
619
|
+
Ready-to-use HTML examples are in the `snippets/` directory:
|
|
620
|
+
|
|
621
|
+
| File | Description |
|
|
622
|
+
|------|-------------|
|
|
623
|
+
| `starter-page.html` | Minimal ply page with CDN link |
|
|
624
|
+
| `two-column-layout.html` | Sidebar + main content |
|
|
625
|
+
| `card.html` | Card with border and button |
|
|
626
|
+
| `contact-form.html` | Styled form with validation |
|
|
627
|
+
| `navbar-page.html` | Navbar + page content |
|
|
628
|
+
| `dashboard.html` | Dashboard with stats cards |
|
|
629
|
+
| `notifications.html` | Alert variants |
|
|
630
|
+
| `data-table.html` | Styled data table |
|
|
631
|
+
| `login-page.html` | Centered login form |
|
|
632
|
+
| `pricing-cards.html` | Pricing tier cards |
|
|
633
|
+
| `custom-theme.html` | Custom theme example |
|
|
634
|
+
| `responsive-header.html` | CSS-only collapsible responsive header |
|
|
635
|
+
| `accessible-drag-and-drop.html` | Keyboard-accessible sortable list with ARIA live announcements |
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
## Bundles
|
|
640
|
+
|
|
641
|
+
| Bundle | Includes | Size (gzip) |
|
|
642
|
+
|--------|----------|-------------|
|
|
643
|
+
| `ply.min.css` | Everything | 18.5KB |
|
|
644
|
+
| `ply-core.min.css` | Grid, buttons, forms, nav, alerts, tables, typography, essential helpers | ~17KB |
|
|
645
|
+
| `ply-essentials.min.css` | Grid, helpers, alignments, blocks only | ~6KB |
|
|
646
|
+
| `ply-helpers.min.css` | Helper utilities only | ~4KB |
|