Skip to content

Commit a8e2bdf

Browse files
authored
Merge pull request #7 from Foscat/polished
chore: polish package metadata and README for public release Updates interaction-state styling and release-facing documentation/metadata in preparation for a public release, with a focus on accessibility and state rendering changes in interactive-surface.css. Changes: Refactors hover/focus/active styling to use inset “state layers” (instead of host-level filter) and updates disabled/ARIA-disabled semantics. Updates README and CHANGELOG to reflect the intended shipped behavior and release notes. Removes obsolete wiki setup guidance and tweaks the PR template formatting.
2 parents 2686c80 + 397ffb8 commit a8e2bdf

7 files changed

Lines changed: 140 additions & 66 deletions

File tree

.github/pull_request_template.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## Summary
1+
## Summary
22

33
Describe the change and why it is needed.
44

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,36 @@
22

33
All notable changes to this package are documented in this file.
44

5+
## 1.1.1 - 2026-04-24
6+
7+
### Improved
8+
9+
- Refined interaction-state rendering to improve accessibility across varied site themes and token sets
10+
- Replaced whole-element filter-based state styling with safer state-layer treatment
11+
- Improved disabled-state behavior to better support real-world semantics and integration patterns
12+
- Strengthened high-contrast handling for `prefers-contrast: more`
13+
14+
### Changed
15+
16+
- Native disabled controls (`:disabled`) and `.is-disabled` now use stricter non-interactive behavior
17+
- `[aria-disabled="true"]` receives the same visual disabled appearance and sets `pointer-events: none`; keyboard focus is still reachable so the focus ring remains visible
18+
- Touch feedback now uses a tokenized tap highlight instead of fully removing native tap indication
19+
20+
### Fixed
21+
22+
- Fixed a bug where certain state combinations could cause unintended visual results with specific token sets
23+
- Reduced the risk of contrast loss caused by whole-element brightness and contrast filters
24+
- Reduced the chance of child text and icons being unintentionally affected by state styling
25+
- Softened disabled-state degradation to avoid overly washed-out presentation
26+
527
## 1.1.0 - 2026-04-09
628

729
- Standardized color usage to functional notation by converting remaining hex fallbacks in `interactive-surface.css` to `rgb(...)`.
830
- Added a color-format guard script (`npm run check:no-hex-colors`) to fail builds when hex literals are introduced in `interactive-surface.css`.
931
- Integrated the color-format guard into `prepublishOnly` so release builds validate RGB/HSL notation before publish.
1032
- Promoted `index.html` as the primary demo/customization app, with `example.html` retained as a backward-compatible export alias.
1133
- Updated docs and wiki guidance for the new color standard, release checklist updates, and demo customization workflow.
34+
- Updated the README demo section to better highlight the live demo and customization workflow.
1235

1336
## 1.0.2 - 2026-04-02
1437

README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# Interactive Surface
1+
# Interactive Surface
22

33
Interactive Surface is a framework-agnostic CSS interaction primitive for buttons, cards, icon controls, and similar click targets.
44

5-
It provides consistent hover, focus-visible, active, press, and disabled behavior with token-driven theming and accessibility guardrails.
5+
It provides consistent hover, focus-visible, active, press, and disabled behavior with token-driven theming, accessibility guardrails, and minimal integration friction.
66

77
## Documentation
88

@@ -28,8 +28,9 @@ Community and governance docs:
2828
## Package
2929

3030
- Package name: `interactive-surface-css`
31-
- Style entry: `interactive-surface.css`
32-
- JS entry: `index.js` (imports CSS)
31+
- Primary stylesheet: `interactive-surface.css`
32+
- JavaScript entry: `index.js` (imports CSS for bundler-friendly usage)
33+
- Live demo: `https://foscat.github.io/Interactive-Surface-CSS/`
3334

3435
Install:
3536

@@ -49,6 +50,8 @@ Or import CSS directly:
4950
import "interactive-surface-css/interactive-surface.css";
5051
```
5152

53+
Note: The JavaScript entry imports CSS, so it should be used in bundlers or toolchains that support CSS imports. If you want the most portable, framework-agnostic path, import `interactive-surface-css/interactive-surface.css` directly. The package supports both approaches to accommodate different project setups and preferences.
54+
5255
Webpack:
5356

5457
1. Install loaders:
@@ -101,9 +104,9 @@ CDN:
101104
</button>
102105
```
103106

104-
Demo page: `index.html`
107+
Live demo: [Interactive Surface Demo](https://foscat.github.io/Interactive-Surface-CSS/)
105108

106-
`index.html` is a practical customization playground for this library:
109+
The demo page is a practical customization playground for this library:
107110

108111
- It provides guided token editing controls instead of freehand CSS typing.
109112
- It supports importing and exporting token CSS so teams can reuse exact values.

interactive-surface.css

Lines changed: 102 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,25 @@
4242
var(--shadow-active, 0 6px 18px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.2))
4343
);
4444

45+
/*
46+
* Legacy brightness tokens are preserved, but state rendering now uses
47+
* a ::before state-layer pseudo-element instead of host-level filters so
48+
* text and icons are less likely to lose contrast in consumer themes.
49+
* The state layer is decoupled from box-shadow so shadow tokens set to
50+
* `none` (e.g. in prefers-contrast: more) cannot invalidate the
51+
* box-shadow declaration.
52+
*/
4553
--_is-darken-hover: var(--interactive-surface-darken-hover, var(--surface-darken-hover, 0.96));
4654
--_is-darken-active: var(--interactive-surface-darken-active, var(--surface-darken-active, 0.98));
55+
--_is-state-layer-hover-opacity: var(
56+
--interactive-surface-state-layer-hover-opacity,
57+
calc(1 - var(--_is-darken-hover))
58+
);
59+
--_is-state-layer-active-opacity: var(
60+
--interactive-surface-state-layer-active-opacity,
61+
calc(1 - var(--_is-darken-active))
62+
);
63+
--_is-state-layer-color: var(--interactive-surface-state-layer-color, rgb(0 0 0));
4764

4865
--_is-motion-default: var(--interactive-surface-motion-default, var(--motion-default, 140ms));
4966
--_is-motion-press: var(--interactive-surface-motion-press, var(--motion-press, 60ms));
@@ -69,8 +86,10 @@
6986
--_is-focus-ring-width: var(--interactive-surface-focus-ring-width, 2px);
7087
--_is-focus-ring-offset: var(--interactive-surface-focus-ring-offset, 2px);
7188
--_is-disabled-opacity: var(--interactive-surface-disabled-opacity, 0.72);
89+
--_is-tap-highlight: var(--interactive-surface-tap-highlight-color, rgb(11 99 246 / 0.18));
7290

7391
position: relative;
92+
isolation: isolate;
7493
box-sizing: border-box;
7594
border: var(--_is-border-width) solid var(--_is-border-color);
7695
border-radius: var(--_is-radius);
@@ -82,28 +101,32 @@
82101
-webkit-appearance: none;
83102
appearance: none;
84103
touch-action: manipulation;
85-
-webkit-tap-highlight-color: transparent;
104+
-webkit-tap-highlight-color: var(--_is-tap-highlight);
105+
background-clip: padding-box;
86106
transform: translateY(var(--_is-lift-base)) translateZ(0) !important;
87107
box-shadow: var(--_is-shadow-base);
88108
transition:
89109
transform var(--_is-motion-default) var(--_is-ease-standard),
90110
box-shadow var(--_is-motion-default) var(--_is-ease-standard),
91-
filter var(--_is-motion-default) linear,
92111
background-color var(--_is-motion-default) linear,
93-
color var(--_is-motion-default) linear;
112+
border-color var(--_is-motion-default) linear,
113+
color var(--_is-motion-default) linear,
114+
outline-color var(--_is-motion-default) linear;
94115
will-change: auto;
95116
}
96117

97-
@media (hover: hover) and (pointer: fine) {
98-
.interactive-surface {
99-
will-change: transform, box-shadow;
100-
}
118+
/* 2a) State-layer pseudo-element */
101119

102-
.interactive-surface:hover {
103-
transform: translateY(var(--_is-lift-hover)) translateZ(0) !important;
104-
box-shadow: var(--_is-shadow-hover);
105-
filter: brightness(var(--_is-darken-hover));
106-
}
120+
.interactive-surface::before {
121+
content: "";
122+
position: absolute;
123+
inset: 0;
124+
border-radius: inherit;
125+
background-color: var(--_is-state-layer-color);
126+
opacity: 0;
127+
pointer-events: none;
128+
z-index: -1;
129+
transition: opacity var(--_is-motion-default) var(--_is-ease-standard);
107130
}
108131

109132
/* 3) Keyboard focus visibility */
@@ -119,33 +142,63 @@
119142
}
120143
}
121144

122-
.interactive-surface:focus-visible {
145+
.interactive-surface:not(.is-disabled, :disabled, [aria-disabled="true"]):focus-visible {
123146
transform: translateY(var(--_is-lift-hover)) translateZ(0) !important;
124147
box-shadow: var(--_is-shadow-hover);
125-
filter: brightness(var(--_is-darken-hover));
148+
outline: var(--_is-focus-ring-width) solid var(--_is-focus-ring-color);
149+
outline-offset: var(--_is-focus-ring-offset);
150+
}
151+
152+
.interactive-surface:not(.is-disabled, :disabled, [aria-disabled="true"]):focus-visible::before {
153+
opacity: var(--_is-state-layer-hover-opacity);
154+
}
155+
156+
.interactive-surface[aria-disabled="true"]:focus-visible {
126157
outline: var(--_is-focus-ring-width) solid var(--_is-focus-ring-color);
127158
outline-offset: var(--_is-focus-ring-offset);
128159
}
129160

130161
/* 4) Active and toggled states */
131162

132-
.interactive-surface.is-active,
133-
.interactive-surface[aria-pressed="true"] {
163+
.interactive-surface.is-active:not(.is-disabled, [aria-disabled="true"]),
164+
.interactive-surface[aria-pressed="true"]:not(.is-disabled, :disabled, [aria-disabled="true"]) {
134165
transform: translateY(var(--_is-lift-active)) translateZ(0) !important;
135166
box-shadow: var(--_is-shadow-active);
136-
filter: brightness(var(--_is-darken-active));
137167
}
138168

139-
/* 5) Press feedback */
169+
.interactive-surface.is-active:not(.is-disabled, [aria-disabled="true"])::before,
170+
.interactive-surface[aria-pressed="true"]:not(.is-disabled, :disabled, [aria-disabled="true"])::before {
171+
opacity: var(--_is-state-layer-active-opacity);
172+
}
173+
174+
/* 5) Pointer hover and press feedback */
175+
176+
@media (hover: hover) and (pointer: fine) {
177+
.interactive-surface {
178+
will-change: transform, box-shadow;
179+
}
180+
181+
.interactive-surface:not(.is-disabled, :disabled, [aria-disabled="true"]):hover {
182+
transform: translateY(var(--_is-lift-hover)) translateZ(0) !important;
183+
box-shadow: var(--_is-shadow-hover);
184+
}
185+
186+
.interactive-surface:not(.is-disabled, :disabled, [aria-disabled="true"]):hover::before {
187+
opacity: var(--_is-state-layer-hover-opacity);
188+
}
189+
}
140190

141-
.interactive-surface:active {
191+
.interactive-surface:not(.is-disabled, :disabled, [aria-disabled="true"]):active {
142192
transform: translateY(var(--_is-lift-base)) translateZ(0) !important;
143193
box-shadow: var(--_is-shadow-base);
144-
filter: brightness(1);
145194
transition-duration: var(--_is-motion-press);
146195
transition-timing-function: var(--_is-ease-press);
147196
}
148197

198+
.interactive-surface:not(.is-disabled, :disabled, [aria-disabled="true"]):active::before {
199+
opacity: 0;
200+
}
201+
149202
/* 6) Size modifiers */
150203

151204
.interactive-surface.size-sm {
@@ -271,17 +324,23 @@
271324
/* 9) Disabled states */
272325

273326
.interactive-surface.is-disabled,
274-
.interactive-surface:disabled,
275-
.interactive-surface[aria-disabled="true"] {
327+
.interactive-surface:disabled {
276328
cursor: not-allowed;
277329
pointer-events: none;
278330
transform: none !important;
279331
box-shadow: none;
280-
filter: saturate(0.6) contrast(0.9) brightness(0.92);
281332
opacity: var(--_is-disabled-opacity);
282333
transition: none;
283334
}
284335

336+
.interactive-surface[aria-disabled="true"] {
337+
cursor: not-allowed;
338+
pointer-events: none;
339+
transform: none !important;
340+
box-shadow: none;
341+
opacity: var(--_is-disabled-opacity);
342+
}
343+
285344
/* Guardrail: this class owns motion on the host element. */
286345
.interactive-surface,
287346
.interactive-surface:hover,
@@ -301,14 +360,25 @@
301360
transition: none;
302361
}
303362

363+
.interactive-surface::before {
364+
transition: none;
365+
}
366+
304367
.interactive-surface:hover,
305368
.interactive-surface:focus-visible,
306369
.interactive-surface:active,
307370
.interactive-surface.is-active,
308371
.interactive-surface[aria-pressed="true"] {
309372
transform: none !important;
310373
box-shadow: none;
311-
filter: none;
374+
}
375+
376+
.interactive-surface:hover::before,
377+
.interactive-surface:focus-visible::before,
378+
.interactive-surface:active::before,
379+
.interactive-surface.is-active::before,
380+
.interactive-surface[aria-pressed="true"]::before {
381+
opacity: 0;
312382
}
313383
}
314384

@@ -319,6 +389,10 @@
319389
--interactive-surface-darken-hover: 1;
320390
--interactive-surface-darken-active: 1;
321391
--interactive-surface-focus-ring-width: 3px;
392+
--interactive-surface-border-width: 2px;
393+
--interactive-surface-shadow-base: none;
394+
--interactive-surface-shadow-hover: none;
395+
--interactive-surface-shadow-active: none;
322396
}
323397
}
324398

@@ -329,7 +403,10 @@
329403
color: ButtonText;
330404
border-color: ButtonText;
331405
box-shadow: none;
332-
filter: none;
406+
}
407+
408+
.interactive-surface::before {
409+
display: none;
333410
}
334411

335412
.interactive-surface:hover,
@@ -339,7 +416,6 @@
339416
.interactive-surface:active {
340417
transform: none !important;
341418
box-shadow: none;
342-
filter: none;
343419
}
344420

345421
.interactive-surface:focus,

wiki/API-Reference.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Receives the same active styling as `.is-active`.
4949

5050
### `[aria-disabled="true"]`
5151

52-
Receives the same disabled styling as `.is-disabled`.
52+
Receives the same visual disabled styling as `.is-disabled` (reduced opacity, no transform or shadow, `cursor: not-allowed`, `pointer-events: none`). Unlike native `:disabled` controls, `[aria-disabled="true"]` elements can still receive keyboard focus, so the focus ring remains visible when the element is focused via Tab.
5353

5454
### `:disabled`
5555

wiki/Token-Reference.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ These are the primary public tokens:
2727
- `--interactive-surface-focus-ring-width`
2828
- `--interactive-surface-focus-ring-offset`
2929
- `--interactive-surface-disabled-opacity`
30+
- `--interactive-surface-tap-highlight-color`
3031

3132
## Legacy fallback tokens
3233

@@ -128,5 +129,8 @@ Use these carefully:
128129
- focus ring width
129130
- focus ring offset
130131
- disabled opacity
132+
- tap highlight color
131133

132134
Avoid reducing focus visibility below practical usability.
135+
136+
`--interactive-surface-tap-highlight-color` controls the native mobile tap flash color applied via `-webkit-tap-highlight-color`. The default is a semi-transparent blue (`rgb(11 99 246 / 0.18)`). Set to `transparent` to suppress the flash entirely (not recommended for touch-only contexts), or set to a brand color to align tap feedback with your theme.

wiki/WIKI_SETUP.md

Lines changed: 0 additions & 32 deletions
This file was deleted.

0 commit comments

Comments
 (0)