This is an automated email from the ASF dual-hosted git repository.

shahar1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow-site.git


The following commit(s) were added to refs/heads/main by this push:
     new a8d80462d8 Improve WCAG 2.1 AA accessibility of landing pages (#1545)
a8d80462d8 is described below

commit a8d80462d8839087545efae727f93fa39fdaa644
Author: Shahar Epstein <[email protected]>
AuthorDate: Thu May 28 08:01:53 2026 +0300

    Improve WCAG 2.1 AA accessibility of landing pages (#1545)
    
    Add skip link, semantic headings, accessible names for icon controls,
    search labeling, drawer state, reduced-motion handling, contrast fixes,
    and focus-visible styles on the Airflow landing pages.
    
    Co-authored-by: Cursor <[email protected]>
---
 landing-pages/site/assets/scss/_footer.scss        | 15 +++-
 landing-pages/site/assets/scss/main-custom.scss    | 89 ++++++++++++++++++++++
 landing-pages/site/layouts/_default/baseof.html    |  3 +-
 landing-pages/site/layouts/_partials/footer.html   |  2 +-
 landing-pages/site/layouts/_partials/navbar.html   |  6 +-
 landing-pages/site/layouts/index.html              | 21 ++---
 .../partials/buttons/button-with-arrow.html        |  4 +-
 landing-pages/site/layouts/partials/feature.html   |  2 +-
 landing-pages/site/layouts/partials/navbar.html    |  6 +-
 .../site/layouts/partials/text-with-icon.html      |  2 +-
 landing-pages/src/js/drawer.js                     |  4 +
 landing-pages/src/js/headerAnimation.js            | 10 ++-
 12 files changed, 140 insertions(+), 24 deletions(-)

diff --git a/landing-pages/site/assets/scss/_footer.scss 
b/landing-pages/site/assets/scss/_footer.scss
index 1eeeab9dbb..f42caafa92 100644
--- a/landing-pages/site/assets/scss/_footer.scss
+++ b/landing-pages/site/assets/scss/_footer.scss
@@ -73,6 +73,10 @@ footer {
       &--policy-item {
         cursor: pointer;
         transition: opacity 0.2s ease, color 0.2s ease;
+        display: inline-flex;
+        align-items: center;
+        min-height: 24px;
+        padding: 2px 0;
 
         &::before {
           content: "\00a0\00a0";
@@ -98,9 +102,18 @@ footer {
       &--disclaimer {
         display: block;
         max-width: 600px;
-        color: map-get($colors, very-light-pink) !important;
+        color: map-get($colors, white) !important;
         margin-top: 16px;
 
+        a {
+          color: #ddf0ff;
+          text-decoration: underline;
+
+          &:hover {
+            color: map-get($colors, white);
+          }
+        }
+
         @media (min-width: $fullhd) {
           max-width: 800px;
         }
diff --git a/landing-pages/site/assets/scss/main-custom.scss 
b/landing-pages/site/assets/scss/main-custom.scss
index ff0a4e42a2..7e2f6b72c5 100644
--- a/landing-pages/site/assets/scss/main-custom.scss
+++ b/landing-pages/site/assets/scss/main-custom.scss
@@ -60,6 +60,95 @@
 @import "survey";
 @import "suggest-change";
 
+/* Accessibility: .btn-filled uses #017cee (cerulean-blue) background with 
white text.
+   WCAG AA requires 4.5:1; #017cee gives ~4.1:1. Override the button 
background with
+   a slightly darker shade (#016bdb) that achieves 4.7:1 while staying 
brand-adjacent. */
+.btn-filled {
+  background-color: #016bdb;
+  border-color: #016bdb;
+
+  &:focus {
+    background-color: #017cee;
+    border-color: #017cee;
+  }
+
+  &:hover {
+    background-color: #017cee;
+    border-color: #017cee;
+  }
+}
+
+[data-bs-theme="dark"] .btn-filled {
+  background-color: #016bdb;
+  border-color: #016bdb;
+
+  &:focus {
+    background-color: #0188ff;
+    border-color: #0188ff;
+  }
+
+  &:hover {
+    background-color: #0188ff;
+    border-color: #0188ff;
+  }
+}
+
+/* Skip navigation link */
+.skip-link {
+  position: absolute;
+  top: -100%;
+  left: 0;
+  z-index: 9999;
+  padding: 0.75rem 1.25rem;
+  background: #017cee;
+  color: #fff;
+  font-weight: 600;
+  font-size: 1rem;
+  text-decoration: none;
+  border-radius: 0 0 4px 0;
+
+  &:focus {
+    top: 0;
+    outline: 3px solid #fff;
+    outline-offset: 2px;
+  }
+}
+
+/* Visually hidden utility (accessible label helper) */
+.visually-hidden {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0, 0, 0, 0);
+  white-space: nowrap;
+  border: 0;
+}
+
+/* Focus-visible enhancement for interactive elements */
+:focus-visible {
+  outline: 3px solid #017cee;
+  outline-offset: 2px;
+}
+
+/* Reduced motion: disable non-essential transitions */
+@media (prefers-reduced-motion: reduce) {
+  *,
+  *::before,
+  *::after {
+    animation-duration: 0.01ms !important;
+    animation-iteration-count: 1 !important;
+    transition-duration: 0.01ms !important;
+    scroll-behavior: auto !important;
+  }
+
+  #header-canvas canvas {
+    display: none;
+  }
+}
+
 /* Theme toggle pill (sun/moon) */
 .theme-toggle-pill {
   display: inline-flex;
diff --git a/landing-pages/site/layouts/_default/baseof.html 
b/landing-pages/site/layouts/_default/baseof.html
index e5a63fcce5..5dc1d76e49 100644
--- a/landing-pages/site/layouts/_default/baseof.html
+++ b/landing-pages/site/layouts/_default/baseof.html
@@ -23,13 +23,14 @@
     {{ partial "head.html" . }}
 </head>
 <body class="td-{{ .Kind }}">
+<a href="#main-content" class="skip-link">Skip to main content</a>
 <header>
     {{ partial "navbar.html" . }}
 </header>
 <div class="container-fluid td-default">
     {{ block "animation-header" . }}{{ end }}
     <div class="home-page-layout base-layout">
-        <main role="main" class="td-main container">
+        <main id="main-content" role="main" class="td-main container">
             {{ block "main" . }}{{ end }}
         </main>
         <div class="base-layout--button">
diff --git a/landing-pages/site/layouts/_partials/footer.html 
b/landing-pages/site/layouts/_partials/footer.html
index c0f09bedd6..e830493d72 100644
--- a/landing-pages/site/layouts/_partials/footer.html
+++ b/landing-pages/site/layouts/_partials/footer.html
@@ -62,7 +62,7 @@
             {{ with $links }}
                 {{ with index . "media"}}
                     {{ range . }}
-                        <a class="footer-section__media-section--link icon-{{ 
replace .iconName ".svg" "" | lower }}" target="_blank" href="{{ .url }}">
+                        <a class="footer-section__media-section--link icon-{{ 
replace .iconName ".svg" "" | lower }}" target="_blank" rel="noopener 
noreferrer" href="{{ .url }}" aria-label="{{ .name }} (opens in new tab)">
                             {{ with resources.Get (print "icons/" .iconName) 
}}{{ .Content | safeHTML }}{{ end }}
                         </a>
                     {{ end }}
diff --git a/landing-pages/site/layouts/_partials/navbar.html 
b/landing-pages/site/layouts/_partials/navbar.html
index fa904245f9..c66a42b5cc 100644
--- a/landing-pages/site/layouts/_partials/navbar.html
+++ b/landing-pages/site/layouts/_partials/navbar.html
@@ -18,10 +18,10 @@
 */}}
 
 {{ $cover := .HasShortcode "blocks/cover" }}
-<nav class="js-navbar-scroll navbar">
+<nav class="js-navbar-scroll navbar" aria-label="Main navigation">
 
     <div class="navbar__icon-container">
-        <a href="{{ .Site.Home.RelPermalink }}">
+        <a href="{{ .Site.Home.RelPermalink }}" aria-label="Apache Airflow 
home">
             {{ with resources.Get "icons/airflow-logo.svg" }}{{ .Content | 
safeHTML }}{{ end }}
         </a>
     </div>
@@ -34,7 +34,7 @@
     </div>
     {{ end -}}
     <div class="no-desktop navbar__drawer-container">
-        <button class="navbar__toggle-button" id="navbar-toggle-button">
+        <button class="navbar__toggle-button" id="navbar-toggle-button" 
aria-label="Open navigation menu" aria-expanded="false" 
aria-controls="navbar-drawer">
             {{ with resources.Get "icons/hamburger-icon.svg" }}
                 <div id="hamburger-icon" class="navbar__toggle-button--icon 
visible">
                     {{ .Content | safeHTML }}
diff --git a/landing-pages/site/layouts/index.html 
b/landing-pages/site/layouts/index.html
index 01d1fbe931..5c5ea75aaf 100644
--- a/landing-pages/site/layouts/index.html
+++ b/landing-pages/site/layouts/index.html
@@ -21,11 +21,11 @@
     <section id="header">
         <div id="header-canvas">
             <div class="text-area">
-                <h2 class="text-area--header" id="header-title">Apache 
Airflow&#174;</h2>
-                <h5 class="text-area--subheader" id="header-lead">
+                <h1 class="text-area--header" id="header-title">Apache 
Airflow&#174;</h1>
+                <p class="text-area--subheader" id="header-lead">
                     Apache Airflow&#174; is a platform created by the 
community to programmatically author, schedule and monitor
                     workflows.
-                </h5>
+                </p>
                 <a href="/docs/stable/start.html" id="header-button">
                     {{ partial "buttons/button-filled" (dict "text" "Install 
Airflow") }}
                 </a>
@@ -36,7 +36,7 @@
 
 {{ define "main" }}
     <div>
-        <h4 class="page-header home-section-header">Principles</h4>
+        <h2 class="page-header home-section-header">Principles</h2>
         <div class="text-with-icon-list">
             {{ partial "text-with-icon" (dict "logo_path" 
"icons/scalable-icon.svg" "header" "Scalable" "text" "Apache Airflow&#174; has 
a modular architecture and uses a message queue to orchestrate an arbitrary 
number of workers. Airflowâ„¢ is ready to scale to infinity.") }}
             {{ partial "text-with-icon" (dict "logo_path" 
"icons/dynamic-icon.svg" "header" "Dynamic" "text" "Apache Airflow&#174; 
pipelines are defined in Python, allowing for dynamic pipeline generation. This 
allows for writing code that instantiates pipelines dynamically.") }}
@@ -45,7 +45,7 @@
         </div>
     </div>
     <div>
-        <h4 class="page-header home-section-header">Features</h4>
+        <h2 class="page-header home-section-header">Features</h2>
         <div class="features-list">
             {{ partial "feature" (dict "logo_path" 
"icons/pure-python-icon.svg" "header" "Pure Python" "text" "No more 
command-line or XML black-magic! Use standard Python features to create your 
workflows, including date time formats for scheduling and loops to dynamically 
generate tasks. This allows you to maintain full flexibility when building your 
workflows.") }}
             {{ partial "feature" (dict "logo_path" "icons/useful-ui-icon.svg" 
"header" "Useful UI" "text" "Monitor, schedule and manage your workflows via a 
robust and modern web application. No need to learn old, cron-like interfaces. 
You always have full insight into the status and logs of completed and ongoing 
tasks.") }}
@@ -56,7 +56,7 @@
 
     </div>
     <div>
-        <h4 class="page-header home-section-header">Integrations</h4>
+        <h2 class="page-header home-section-header">Integrations</h2>
         <div id="integrations">
             <script type="application/x-template" id="integration-template">
                 <a class="list-item" href="">
@@ -68,9 +68,10 @@
                     </div>
                 </a>
             </script>
-            <form class="search-form">
-                <input class="search-form__input" placeholder="Search" 
name="q" type="search" size="16"/>
-                <button class="search-form__button" type="submit">
+            <form class="search-form" role="search" aria-label="Search 
integrations">
+                <label for="integrations-search" 
class="visually-hidden">Search integrations</label>
+                <input id="integrations-search" class="search-form__input" 
placeholder="Search" name="q" type="search" size="16" aria-label="Search 
integrations"/>
+                <button class="search-form__button" type="submit" 
aria-label="Search">
                     {{ with resources.Get "icons/search-icon.svg" }}
                     {{ .Content | safeHTML }}
                     {{ end }}
@@ -92,7 +93,7 @@
         </div>
     </div>
     <div>
-    <h4 class="page-header home-section-header">From the Blog</h4>
+    <h2 class="page-header home-section-header">From the Blog</h2>
 
     <div class="blog-snippet-list">
         {{ range first 3 (where .Site.RegularPages "Section" "blog") }}
diff --git a/landing-pages/site/layouts/partials/buttons/button-with-arrow.html 
b/landing-pages/site/layouts/partials/buttons/button-with-arrow.html
index 257110b36c..8a1cea5fae 100644
--- a/landing-pages/site/layouts/partials/buttons/button-with-arrow.html
+++ b/landing-pages/site/layouts/partials/buttons/button-with-arrow.html
@@ -16,8 +16,8 @@
     specific language governing permissions and limitations
     under the License.
    */}}
-<button id="{{ .id }}" class="btn-hollow btn-brown btn-with-icon 
with-box-shadow {{ .class }}">
-    <svg xmlns="http://www.w3.org/2000/svg"; width="24" height="24" 
fill="black" viewBox="0 0 16 16">
+<button id="{{ .id }}" class="btn-hollow btn-brown btn-with-icon 
with-box-shadow {{ .class }}" aria-label="{{ with .ariaLabel }}{{ . }}{{ else 
}}Scroll to top{{ end }}">
+    <svg xmlns="http://www.w3.org/2000/svg"; width="24" height="24" 
fill="black" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
         <path d="m7.247 4.86-4.796 5.481c-.566.647-.106 1.659.753 
1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z"/>
     </svg>
 </button>
diff --git a/landing-pages/site/layouts/partials/feature.html 
b/landing-pages/site/layouts/partials/feature.html
index eddf6aa8a0..036025f107 100644
--- a/landing-pages/site/layouts/partials/feature.html
+++ b/landing-pages/site/layouts/partials/feature.html
@@ -24,7 +24,7 @@
         {{ end }}
     </div>
     <div class="feature-item--text-box">
-        <h5 class="feature-item--header">{{ .header }}</h5>
+        <h3 class="feature-item--header">{{ .header }}</h3>
         <p class="feature-item--text">{{ .text }}</p>
     </div>
 </div>
diff --git a/landing-pages/site/layouts/partials/navbar.html 
b/landing-pages/site/layouts/partials/navbar.html
index ca5ad81a57..401c00b9b2 100644
--- a/landing-pages/site/layouts/partials/navbar.html
+++ b/landing-pages/site/layouts/partials/navbar.html
@@ -18,10 +18,10 @@
 */}}
 
 {{ $cover := .HasShortcode "blocks/cover" }}
-<nav class="js-navbar-scroll navbar">
+<nav class="js-navbar-scroll navbar" aria-label="Main navigation">
 
     <div class="navbar__icon-container">
-        <a href="{{ .Site.Home.RelPermalink }}">
+        <a href="{{ .Site.Home.RelPermalink }}" aria-label="Apache Airflow 
home">
             {{ with resources.Get "icons/airflow-logo.svg" }}{{ .Content | 
safeHTML }}{{ end }}
         </a>
     </div>
@@ -34,7 +34,7 @@
     </div>
     {{ end -}}
     <div class="no-desktop navbar__drawer-container">
-        <button class="navbar__toggle-button" id="navbar-toggle-button">
+        <button class="navbar__toggle-button" id="navbar-toggle-button" 
aria-label="Open navigation menu" aria-expanded="false" 
aria-controls="navbar-drawer">
             {{ with resources.Get "icons/hamburger-icon.svg" }}
                 <div id="hamburger-icon" class="navbar__toggle-button--icon 
visible">
                     {{ .Content | safeHTML }}
diff --git a/landing-pages/site/layouts/partials/text-with-icon.html 
b/landing-pages/site/layouts/partials/text-with-icon.html
index efb6aaa4e8..09fb9e70a9 100644
--- a/landing-pages/site/layouts/partials/text-with-icon.html
+++ b/landing-pages/site/layouts/partials/text-with-icon.html
@@ -21,6 +21,6 @@
     {{ with resources.Get .logo_path }}
         {{ .Content | safeHTML }}
     {{ end }}
-    <h5 class="text-with-icon-item--header">{{ .header | markdownify }}</h5>
+    <h3 class="text-with-icon-item--header">{{ .header | markdownify }}</h3>
     <p class="text-with-icon-item--text">{{ .text | markdownify }}</p>
 </div>
diff --git a/landing-pages/src/js/drawer.js b/landing-pages/src/js/drawer.js
index 12c5a645fc..e8f9b1f521 100644
--- a/landing-pages/src/js/drawer.js
+++ b/landing-pages/src/js/drawer.js
@@ -27,6 +27,10 @@ const toggleDrawer = () => {
   drawer.classList.toggle("navbar__drawer--open");
   hamburgerIcon.classList.toggle("visible");
   closeIcon.classList.toggle("visible");
+
+  const isOpen = drawer.classList.contains("navbar__drawer--open");
+  toggleButton.setAttribute("aria-expanded", isOpen ? "true" : "false");
+  toggleButton.setAttribute("aria-label", isOpen ? "Close navigation menu" : 
"Open navigation menu");
 };
 
 if (toggleButton && !toggleButton.dataset.drawerInitialized) {
diff --git a/landing-pages/src/js/headerAnimation.js 
b/landing-pages/src/js/headerAnimation.js
index 6d3c15e2dd..0e880c2d4f 100644
--- a/landing-pages/src/js/headerAnimation.js
+++ b/landing-pages/src/js/headerAnimation.js
@@ -209,6 +209,8 @@ export function initHeaderAnimation() {
   const subtitle = document.querySelector("#header-lead");
   const button = document.querySelector("#header-button");
 
+  const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: 
reduce)").matches;
+
   documentReady(function() {
     new p5((sketch) => {
 
@@ -232,7 +234,13 @@ export function initHeaderAnimation() {
         ];
         logoArea = createLogoArea(sketch, canvas, title, subtitle, button);
         boxes = createBoxes(sketch, vw, vh, logoArea, colors);
-        sketch.createCanvas(vw, vh);
+        const p5Canvas = sketch.createCanvas(vw, vh);
+        p5Canvas.elt.setAttribute("aria-hidden", "true");
+        p5Canvas.elt.setAttribute("focusable", "false");
+
+        if (prefersReducedMotion) {
+          sketch.noLoop();
+        }
       };
 
       sketch.draw = () => {

Reply via email to