Framework-agnostic CSS for reliable hover, focus, press, selected, current, loading, disabled, and motion affordances on interactive controls. Use it alone or as the interaction layer beside your existing layout and theme system.
Version 1.5.0 is a release candidate in this repository until its npm release is published. Existing 1.x imports, selectors, data hooks, ARIA hooks, and tokens remain supported.
The package targets Node.js 20+ for npm installs and local validation. CI proves the minimum Node 20 lane and the preferred Node 22 lane before release.
- Live standalone demo
- Interface Systems Lab — integrated proof with all three CSS libraries
- GitHub Wiki
- Installation guide
- API reference
- Token reference
- Accessibility guide
Each package remains independently useful. Use one library, use two compatible libraries, or use all three according to the layers your application needs.
| Library | Owns | Does not own |
|---|---|---|
interactive-surface-css |
Interaction states, focus visibility, state precedence, and interaction motion | Page layout or an application theme |
ui-style-kit-css |
Theme paint, modes, component appearance, and visual tokens | Page layout or interaction behavior |
layout-style-css |
Page structure, layout recipes, and geometry | Theme paint or interaction behavior |
Install the package:
npm install interactive-surface-cssImport the portable, direct CSS preset. It includes the neutral standalone presentation and the complete state layer:
import "interactive-surface-css/standalone-preset.css";Add the base class to a native control:
<button class="interactive-surface variant-primary" type="button">
Save changes
</button>For a no-build page, pin the release:
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/interactive-surface-css@1.5.0/standalone-preset.css"
/>The equivalent unpkg URL is https://unpkg.com/interactive-surface-css@1.5.0/standalone-preset.css. To follow future releases deliberately, use https://cdn.jsdelivr.net/npm/interactive-surface-css@latest/standalone-preset.css — unpinned opt-in.
Prefer native elements, then reflect persistent application state through ARIA:
<!-- Action -->
<button class="interactive-surface variant-primary" type="button">
Publish
</button>
<!-- Toggle: update aria-pressed when the value changes -->
<button class="interactive-surface" type="button" aria-pressed="true">
Pinned
</button>
<!-- Current navigation item: any non-false aria-current value is supported -->
<a class="interactive-surface" href="/account" aria-current="page">Account</a>
<!-- Selected item in a composite widget -->
<button class="interactive-surface" role="tab" aria-selected="true">
Details
</button>
<!-- Loading: update the accessible label or nearby status text as needed -->
<button class="interactive-surface" type="button" aria-busy="true">
Saving…
</button>
<!-- Prefer native disabled when the control cannot activate -->
<button class="interactive-surface" type="button" disabled>Unavailable</button>
<!-- Native file inputs opt in on the host; the selector button follows the same tokens -->
<input class="interactive-surface variant-subtle" type="file" />
<!-- Variant and level hooks are safe for renderers and companion bridges -->
<button
class="interactive-surface"
type="button"
data-surface-variant="accent"
data-surface-level="2"
>
Continue
</button>
<!-- Icon-only controls need an accessible name; the icon itself is decorative -->
<button
class="interactive-surface icon-only"
type="button"
aria-label="Open settings"
>
<svg aria-hidden="true" data-icon-role="dark" viewBox="0 0 24 24">…</svg>
</button>Native disabled is preferred because the browser suppresses focus and activation. CSS can only communicate disabled-looking state: consumers must suppress activation for aria-disabled="true" and .is-disabled controls in their event handling.
| Import | Presentation | Best for |
|---|---|---|
import "interactive-surface-css/standalone-preset.css"; |
State core plus neutral paint, variants, levels, icon sizing, and preset token defaults | New standalone integrations |
import "interactive-surface-css/state-core.css"; |
Interaction mechanics and neutral core token fallbacks only | Existing design systems that already own paint and geometry |
import "interactive-surface-css/interactive-surface.css"; |
Complete standalone compatibility bundle | Existing direct-CSS 1.x consumers |
import "interactive-surface-css"; |
JavaScript entry that imports the complete compatibility bundle | Existing bundlers configured for CSS imports |
standalone-preset.css and interactive-surface.css are generated from the same authored modules and are behaviorally equivalent in 1.5.0. The compatibility paths remain stable; no 1.x migration is required.
The package main and module fields preserve the CommonJS and ESM entries; both load interactive-surface.css. The style, unpkg, and jsdelivr fields also resolve to that complete compatibility bundle.
| Kind | Public hooks |
|---|---|
| Base | .interactive-surface |
| Size | .size-sm, default medium, .size-lg |
| Transient | pointer :hover, keyboard :focus-visible, :active |
| Persistent | .is-active, aria-pressed="true", aria-pressed="mixed", any non-false aria-current, aria-selected="true" |
| Loading | aria-busy="true", .is-loading |
| Disabled | native :disabled, aria-disabled="true", .is-disabled |
| Variant | .variant-primary, .variant-secondary, .variant-accent, .variant-subtle, .variant-warning, .variant-danger, or matching data-surface-variant values |
| Level | data-surface-level="1", "2", or "3" |
| Icon | .icon-only; child data-icon-role="light", "dark", "accessibility", or the legacy role classes |
| Native subcontrol | input[type="file"].interactive-surface::file-selector-button inherits surface paint and hover/active feedback |
Disabled > busy/loading > transient :active > pressed/selected/current > :hover > base. The :focus-visible ring is orthogonal to that precedence, so keyboard focus remains visible without replacing the active interaction state. The state layer preserves static meaning under reduced motion and uses system-color affordances in forced-colors mode. Interaction lift uses the individual translate property, so consumer-owned transform, scale, and rotate declarations can coexist.
For selector details and responsibilities, see the complete API reference.
state-core.css provides neutral defaults for state-layer color and opacity, focus rings, motion, persistent states, disabled state, and loading state. standalone-preset.css additionally supplies base paint, lift and shadows, variants, surface levels, icon roles, and the 44 × 44px icon-control geometry.
.save-action {
--interactive-surface-focus-ring-color: rgb(0 95 115);
--interactive-surface-state-layer-color: rgb(0 45 55);
--interactive-surface-state-layer-hover-opacity: 0.1;
--interactive-surface-transition-property:
translate, box-shadow, outline-color;
--interactive-surface-transition-duration: 120ms;
--interactive-surface-transition-easing: cubic-bezier(0.2, 0, 0.2, 1);
--interactive-surface-transition-delay: 0ms;
}The transition defaults are translate, box-shadow, outline-color, the established default motion duration, the standard easing curve, and zero delay. Existing --interactive-surface-motion-*, --interactive-surface-ease-*, --motion-*, and --ease-* fallbacks remain supported. All public custom properties use the --interactive-surface-* namespace. See the token ownership tables and full reference.
The CSS package provides visible keyboard focus, persistent-state treatment, disabled precedence, reduced-motion behavior, higher-contrast behavior, forced-colors affordances, hover gating for capable pointers, and standalone icon target sizing. The :focus-visible ring remains orthogonal across every focusable non-disabled interaction state.
Applications still own semantics and behavior:
- Use
<button>for actions and<a href>for navigation. - Update
aria-pressed,aria-current,aria-selected, andaria-busywhen application state changes. - Prefer native
disabled. If a custom control usesaria-disabled="true"or.is-disabled, suppress pointer, keyboard, and programmatic activation. - Give icon-only controls an accessible name, normally with
aria-label, and hide decorative SVG content from assistive technology. - Implement the keyboard model for composite widgets such as tabs; CSS does not add runtime behavior.
Read the accessibility guide for complete examples.
When UI Style Kit owns visual paint, import its opt-in bridge and the state-only core:
import "ui-style-kit-css/with-bridge.css";
import "interactive-surface-css/state-core.css";The bridge maps active UI Style Kit theme and mode values into the --interactive-surface-* contract. Interactive Surface keeps ownership of focus, hover, pressed, selected, current, loading, disabled, and motion behavior. UI Style Kit remains optional: this package declares no runtime or peer dependency on it, and the documented import order keeps its paint layer before state-core.css.
Use the established order so paint, interaction, and structure retain clear ownership:
import "ui-style-kit-css/with-bridge.css";
import "interactive-surface-css/state-core.css";
import "layout-style-css/bridge.css";
import "layout-style-css";The Interface Systems Lab is the canonical integrated example. Each package remains optional: use one library, use two compatible libraries, or use all three.
- Browser behavior: current Chromium, Firefox, and WebKit are covered by the full Playwright gate; forced-colors and platform-specific behavior are tested where supported.
- Testing and quality guide
- Changelog
- Contributing
- Code of Conduct
- Security policy
- MIT License