From 33794cbb4c289f911dea6160578650bc2d2798ed Mon Sep 17 00:00:00 2001
From: Jayden Yoon ZK <19250644+JaydenYoonZK@users.noreply.github.com>
Date: Mon, 13 Jul 2026 00:03:10 +0800
Subject: [PATCH] Release the accessible button component
---
.github/workflows/ci.yml | 30 ++++
CHANGELOG.md | 32 ++++
CONTRIBUTING.md | 14 ++
JayButton.css | 303 +++++++++++++++++++-------------------
README.md | 99 ++++++++++---
SECURITY.md | 5 +
assets/button-preview.svg | 30 ++++
demo.css | 192 ++++++++++++++++++++++++
index.html | 105 +++++++------
package.json | 21 +++
test/button.test.mjs | 48 ++++++
11 files changed, 655 insertions(+), 224 deletions(-)
create mode 100644 .github/workflows/ci.yml
create mode 100644 CHANGELOG.md
create mode 100644 CONTRIBUTING.md
create mode 100644 SECURITY.md
create mode 100644 assets/button-preview.svg
create mode 100644 demo.css
create mode 100644 package.json
create mode 100644 test/button.test.mjs
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..6bfc707
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,30 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+concurrency:
+ group: ci-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ name: Node ${{ matrix.node }}
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ node: [20, 22, 24]
+
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions/setup-node@v6
+ with:
+ node-version: ${{ matrix.node }}
+ - run: npm test
+ - run: npm run check
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..a366f42
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,32 @@
+# Changelog
+
+All notable changes to this project are documented here.
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
+
+## [2.0.0] - 2026-07-12
+
+### Added
+
+- A two-layer component API that keeps the edge and ground shadow fixed while the cap moves.
+- JaydenART green and brown variants with documented CSS custom properties.
+- Keyboard focus, disabled, reduced-motion, forced-colors, and touch interaction states.
+- A responsive, dependency-free live demo with a strict Content Security Policy.
+- Deterministic contract tests and CI on Node.js 20, 22, and 24.
+- Contributing, security, and release documentation.
+
+### Changed
+
+- The live demo uses semantic HTML, local stylesheets, responsive layout, and stable press geometry.
+- The README documents the component structure, customization API, accessibility behavior, and development checks.
+
+### Fixed
+
+- Pressing the modern component no longer moves the base or changes layout dimensions.
+- Keyboard focus is visible instead of being removed.
+- The demo no longer depends on an external background image or inline JavaScript.
+
+### Compatibility
+
+- The original `.JayBrown` single-element class remains available for existing markup.
+
+[2.0.0]: https://github.com/JaydenART/JaydenART_Realistic_3D_CSS_Button/releases/tag/v2.0.0
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..64a4c88
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,14 @@
+# Contributing
+
+Focused improvements are welcome.
+
+## Before Opening a Pull Request
+
+1. Keep `JayButton.css` dependency-free.
+2. Preserve native button semantics and keyboard behavior.
+3. Avoid layout shifts in hover, focus, active, and disabled states.
+4. Keep reduced-motion and forced-colors behavior working.
+5. Run `npm test` and `npm run check`.
+6. Update the README and changelog when public behavior changes.
+
+Please explain the problem, the implementation choice, and how you verified the result. Screenshots are useful for rendering changes, but they should accompany a reproducible description.
diff --git a/JayButton.css b/JayButton.css
index f197b06..176a0ad 100644
--- a/JayButton.css
+++ b/JayButton.css
@@ -1,179 +1,176 @@
-/* -- Body Background -- */
-body {
- margin: 0;
- height: 100vh;
- background-image: radial-gradient(ellipse at center, rgba(0,0,0,0) 0%, rgba(19,19,19,0.35) 100%), url("https://www.JaydenYoon.com/images/JaydenYoonZK_Brownish_White_Background_Small.jpg");
-}
+/* JaydenART Realistic 3D CSS Button v2.0.0 */
+.jay-button {
+ --jay-cap: #abcf37;
+ --jay-cap-hover: #b9dc49;
+ --jay-edge: #718a23;
+ --jay-ink: #10110d;
+ --jay-focus: #ffffff;
+ --jay-travel: 5px;
+ --jay-radius: 14px;
-/* -- Center Everything -- */
-.Center {
- margin: 0;
+ position: relative;
+ isolation: isolate;
+ display: inline-grid;
+ min-inline-size: 10rem;
+ padding: 0 0 8px;
+ border: 0;
+ border-radius: var(--jay-radius);
+ color: var(--jay-ink);
+ background: transparent;
+ font: 700 1rem/1 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ cursor: pointer;
+ touch-action: manipulation;
+ -webkit-tap-highlight-color: transparent;
+ appearance: none;
+}
+
+.jay-button::before {
+ content: "";
position: absolute;
- top: 50%;
- left: 50%;
- -ms-transform: translate(-50%, -50%);
- transform: translate(-50%, -50%);
+ z-index: -1;
+ inset: 5px 0 3px;
+ border-radius: var(--jay-radius);
+ background: var(--jay-edge);
+ box-shadow: inset 0 -1px 0 rgb(0 0 0 / 0.32);
}
+.jay-button::after {
+ content: "";
+ position: absolute;
+ z-index: -2;
+ inset: auto 9% -5px;
+ height: 16px;
+ border-radius: 50%;
+ background: rgb(0 0 0 / 0.34);
+ filter: blur(7px);
+}
-/* -- Center Button -- */
-.CenterButton {
- display: flex;
- align-items: center;
- justify-content: center;
+.jay-button__cap {
+ position: relative;
+ display: block;
+ padding: 1rem 1.55rem;
+ border: 1px solid rgb(0 0 0 / 0.2);
+ border-radius: var(--jay-radius);
+ background: var(--jay-cap);
+ box-shadow:
+ inset 0 1px 0 rgb(255 255 255 / 0.5),
+ inset 0 -2px 0 rgb(0 0 0 / 0.13),
+ 0 2px 3px rgb(0 0 0 / 0.16);
+ transform: translateY(0);
+ transition: transform 130ms ease, background-color 130ms ease, box-shadow 130ms ease;
}
-/* -- JaydenART Button : Normal -- */
-.JayBrown {
- display: inline-block;
- -webkit-box-sizing: content-box;
- -moz-box-sizing: content-box;
- box-sizing: content-box;
- margin: 0 0 15px;
- padding: 8px 20px 10px;
- border: 1px solid rgba(127,70,34,1);
- -webkit-border-radius: 10px;
- border-radius: 10px;
- font: normal normal bold 14px/1 "Coda", Helvetica, sans-serif;
- color: rgb(255, 255, 255);
- text-align: center;
- -o-text-overflow: clip;
- text-overflow: clip;
- background: -webkit-linear-gradient(90deg, rgba(255,255,255,0) 0, rgba(255,255,255,0) 1%, rgba(206,133,88,0.99) 100%), rgba(114,71,42,1);
- background: -moz-linear-gradient(0deg, rgba(255,255,255,0) 0, rgba(255,255,255,0) 1%, rgba(206,133,88,0.99) 100%), rgba(114,71,42,1);
- background: linear-gradient(0deg, rgba(255,255,255,0) 0, rgba(255,255,255,0) 1%, rgba(206,133,88,0.99) 100%), rgba(114,71,42,1);
- background-position: 50% 50%;
- -webkit-background-origin: padding-box;
- background-origin: padding-box;
- -webkit-background-clip: border-box;
- background-clip: border-box;
- -webkit-background-size: auto auto;
- background-size: auto auto;
- -webkit-box-shadow: 0 0 5px 1px rgba(239,159,105,0.6) inset, 0 1.5px 0 0 rgba(229,151,98,1) inset, 0 6px 0 0 rgba(89,50,24,1) , 0 8px 3px 1px rgba(0,0,0,0.99);
- box-shadow: 0 0 5px 1px rgba(239,159,105,0.6) inset, 0 1.5px 0 0 rgba(229,151,98,1) inset, 0 6px 0 0 rgba(89,50,24,1) , 0 8px 3px 1px rgba(0,0,0,0.99);
- text-shadow: 0 -1px 1px rgba(45,22,7,1);
- outline: none;
-}
-
-
-/* -- JaydenART Button : Hover -- */
-.JayBrown:hover {
- cursor: pointer;
- background: -webkit-linear-gradient(-90deg, rgba(255,255,255,0) 0, rgba(211,140,99,0.7) 100%), rgba(137,89,57,1);
- background: -moz-linear-gradient(180deg, rgba(255,255,255,0) 0, rgba(211,140,99,0.7) 100%), rgba(137,89,57,1);
- background: linear-gradient(180deg, rgba(255,255,255,0) 0, rgba(211,140,99,0.7) 100%), rgba(137,89,57,1);
- background-position: 50% 50%;
- -webkit-background-origin: padding-box;
- background-origin: padding-box;
- -webkit-background-clip: border-box;
- background-clip: border-box;
- -webkit-background-size: auto auto;
- background-size: auto auto;
- -webkit-box-shadow: 0 0 5px 1px rgba(239,159,105,0.6) inset, 0 1.5px 0 0 rgba(234,161,112,0.96) inset, 0 6px 0 0 rgba(89,50,24,1) , 0 8px 3px 1px rgba(0,0,0,0.99);
- box-shadow: 0 0 5px 1px rgba(239,159,105,0.6) inset, 0 1.5px 0 0 rgba(234,161,112,0.96) inset, 0 6px 0 0 rgba(89,50,24,1) , 0 8px 3px 1px rgba(0,0,0,0.99);
- text-shadow: 0 -1px 1px rgb(30,45,77);
- outline: none;
-}
-
-
-/* -- JaydenART Button : Active -- */
-.JayBrown:active {
- margin: 5px 0 10px;
- background: -webkit-linear-gradient(90deg, rgba(255,255,255,0) 0, rgba(255,255,255,0) 1%, rgba(206,133,88,0.99) 100%), rgba(114,71,42,1);
- background: -moz-linear-gradient(0deg, rgba(255,255,255,0) 0, rgba(255,255,255,0) 1%, rgba(206,133,88,0.99) 100%), rgba(114,71,42,1);
- background: linear-gradient(0deg, rgba(255,255,255,0) 0, rgba(255,255,255,0) 1%, rgba(206,133,88,0.99) 100%), rgba(114,71,42,1);
- background-position: 50% 50%;
- -webkit-background-origin: padding-box;
- background-origin: padding-box;
- -webkit-background-clip: border-box;
- background-clip: border-box;
- -webkit-background-size: auto auto;
- background-size: auto auto;
- -webkit-box-shadow: 0 1px 10px 1px rgba(239,159,105,0.6) inset, 0 1.5px 0 0 rgba(234,171,129,1) inset, 0 2px 0 0 rgba(89,50,24,1) , 0 3px 3px 1px rgba(0,0,0,0.99);
- box-shadow: 0 1px 10px 1px rgba(239,159,105,0.6) inset, 0 1.5px 0 0 rgba(234,171,129,1) inset, 0 2px 0 0 rgba(89,50,24,1) , 0 3px 3px 1px rgba(0,0,0,0.99);
- outline: none;
+@media (hover: hover) and (pointer: fine) {
+ .jay-button:hover .jay-button__cap {
+ background: var(--jay-cap-hover);
+ transform: translateY(-1px);
+ box-shadow:
+ inset 0 1px 0 rgb(255 255 255 / 0.58),
+ inset 0 -2px 0 rgb(0 0 0 / 0.13),
+ 0 3px 5px rgb(0 0 0 / 0.18);
+ }
}
+.jay-button:active .jay-button__cap {
+ transform: translateY(var(--jay-travel));
+ box-shadow:
+ inset 0 2px 3px rgb(0 0 0 / 0.16),
+ inset 0 -1px 0 rgb(255 255 255 / 0.28);
+}
-/* -- Hidden Line Break -- */
-span {
- white-space: pre;
+.jay-button:focus-visible {
+ outline: 3px solid var(--jay-focus);
+ outline-offset: 5px;
}
-span:after {
- content: ' ';
+.jay-button:disabled {
+ cursor: not-allowed;
+ opacity: 0.48;
}
-span.line-break {
- display: block;
+.jay-button:disabled .jay-button__cap {
+ transform: none;
}
-span.line-break:after {
- content: none;
+.jay-button[data-tone="brown"] {
+ --jay-cap: #6c5b54;
+ --jay-cap-hover: #7b6961;
+ --jay-edge: #382f2b;
+ --jay-ink: #ffffff;
}
+/* Backward-compatible single-element class from the original release. */
+.JayBrown {
+ display: inline-block;
+ min-inline-size: 10rem;
+ padding: 1rem 1.55rem;
+ border: 1px solid #3b312d;
+ border-radius: 14px;
+ color: #ffffff;
+ background: #6c5b54;
+ box-shadow:
+ inset 0 1px 0 rgb(255 255 255 / 0.22),
+ inset 0 -2px 0 rgb(0 0 0 / 0.2),
+ 0 6px 0 #382f2b,
+ 0 11px 14px rgb(0 0 0 / 0.28);
+ font: 700 1rem/1 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ cursor: pointer;
+ transform: translateY(0);
+ transition: transform 130ms ease, background-color 130ms ease, box-shadow 130ms ease;
+ touch-action: manipulation;
+ -webkit-tap-highlight-color: transparent;
+ appearance: none;
+}
-/* -- Copyright Texts -- */
-.Copyright a {
- text-decoration: none;
- color: #C79E5B;
+@media (hover: hover) and (pointer: fine) {
+ .JayBrown:hover {
+ background: #7b6961;
+ transform: translateY(-1px);
+ }
}
+.JayBrown:active {
+ transform: translateY(4px);
+ box-shadow:
+ inset 0 2px 3px rgb(0 0 0 / 0.18),
+ 0 2px 0 #382f2b,
+ 0 6px 9px rgb(0 0 0 / 0.26);
+}
-/* -- Superscript Registered Trademark Symbol -- */
-sup {
- position: relative;
- font-size: 40%;
- line-height: 0;
- vertical-align: baseline;
- top: -1.1em;
- left: 0.3em;
-}
-
-
-/* -- Center Copyright Texts & Word Breaks -- */
-.CenterCopyright {
- position: fixed;
- left: 0;
- bottom: 0;
- padding-top: 10px;
- padding-bottom: 10px;
- width: 100%;
-
- text-align: center;
- align-items: center;
- justify-content: center;
- text-align: center;
-
- color: #e0e0e0;
- font: bold 15px helvetica;
- text-shadow: 0px -1.5px 0px rgba(0, 0, 0, 1),0px 1px 1.5px rgba(255, 255, 255, 0.25);
-
- background-color: #333742;
- -webkit-box-shadow: 0px 10px 3px 11px rgba(0,0,0,0.6);
- box-shadow: 0px 10px 3px 11px rgba(0,0,0,0.6);
-
-/* Word Break */
- word-wrap: break-word;
- word-break: break-word;
- overflow-wrap: break-word;
- -ms-word-break: break-word;
-
-/* None Standard For Webkit */
- -webkit-hyphens: none;
- moz-hyphens: none;
- -ms-hyphens: none;
- hyphens: none;
-}
-
-
-/* -- All Rights Reserved Same Line -- */
-.AllRightsReserved {
- display: inline-block;
+.JayBrown:focus-visible {
+ outline: 3px solid #abcf37;
+ outline-offset: 5px;
}
+@media (prefers-reduced-motion: reduce) {
+ .jay-button__cap,
+ .JayBrown {
+ transition-duration: 0.01ms;
+ }
+
+ .jay-button:hover .jay-button__cap,
+ .jay-button:active .jay-button__cap,
+ .JayBrown:hover,
+ .JayBrown:active {
+ transform: none;
+ }
+}
-/* -- Text Highlight + Shadow Removed -- */
-::-moz-selection {
-
+@media (forced-colors: active) {
+ .jay-button,
+ .JayBrown {
+ forced-color-adjust: none;
+ }
+
+ .jay-button__cap,
+ .JayBrown {
+ border-color: ButtonText;
+ color: ButtonText;
+ background: ButtonFace;
+ }
+
+ .jay-button::before {
+ background: ButtonText;
+ }
+}
diff --git a/README.md b/README.md
index 0c9612e..9af8908 100644
--- a/README.md
+++ b/README.md
@@ -1,37 +1,100 @@
-# JaydenART® - Realistic 3D CSS Button
-A button that actually presses. 100% CSS, no JavaScript, no images.
+# JaydenART® Realistic 3D CSS Button
-
+
A button that actually presses. Pure CSS, no JavaScript, no images, and no dependencies.
-$~$ + +