Skip to content

Commit df895be

Browse files
author
m.buchhorn-roth
committed
a11y: WCAG 2.2 - skip link, aria-labels, reduced-motion, landmarks, focus rings, page title
1 parent 66884ec commit df895be

3 files changed

Lines changed: 85 additions & 26 deletions

File tree

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<head>
55
<meta charset="UTF-8" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>CASSA - Digitaler Wissensassistent für die Rentenversicherung | Sopra Steria</title>
7+
<title>CASSA RELIEF — KI-gestützte E-AKTE für die Grundsicherung | Sopra Steria</title>
88
<link rel="preconnect" href="https://fonts.googleapis.com">
99
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1010
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">

src/App.tsx

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { motion, useScroll, useTransform, AnimatePresence } from "framer-motion"
1+
import { motion, useScroll, useTransform, AnimatePresence, useReducedMotion } from "framer-motion"
22
import { Button } from "@/components/ui/button"
33
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
44
import { Badge } from "@/components/ui/badge"
@@ -65,6 +65,7 @@ function App() {
6565

6666
const { scrollYProgress } = useScroll()
6767
const heroOpacity = useTransform(scrollYProgress, [0, 0.15], [1, 0.3])
68+
const prefersReducedMotion = useReducedMotion()
6869

6970
useEffect(() => {
7071
const timer = setTimeout(() => setShowIntroGuide(false), 5000)
@@ -100,7 +101,12 @@ function App() {
100101
}, [isMuted])
101102

102103
const scrollToSection = (id: string) => {
103-
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' })
104+
const el = document.getElementById(id)
105+
if (!el) return
106+
el.scrollIntoView({ behavior: prefersReducedMotion ? 'auto' : 'smooth' })
107+
// Move focus to the section for keyboard/screen-reader users
108+
if (!el.hasAttribute('tabindex')) el.setAttribute('tabindex', '-1')
109+
el.focus({ preventScroll: true })
104110
}
105111

106112
// ── Side navigation sections ──
@@ -389,6 +395,13 @@ function App() {
389395

390396
return (
391397
<div className="min-h-screen bg-background relative">
398+
{/* ── WCAG: Skip-to-content ── */}
399+
<a
400+
href="#main-content"
401+
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-[9999] focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded-lg focus:shadow-xl focus:outline-2 focus:outline-offset-2 focus:outline-primary-foreground"
402+
>
403+
Zum Hauptinhalt springen
404+
</a>
392405
<AnimatePresence>
393406
{showIntroGuide && (
394407
<motion.div
@@ -400,10 +413,10 @@ function App() {
400413
<Card className="shadow-2xl border-2 border-accent">
401414
<CardContent className="p-6 flex items-center gap-4">
402415
<motion.div
403-
animate={{ y: [0, 10, 0] }}
416+
animate={prefersReducedMotion ? {} : { y: [0, 10, 0] }}
404417
transition={{ duration: 1.5, repeat: Infinity }}
405418
>
406-
<ArrowDown className="h-6 w-6 text-accent" />
419+
<ArrowDown className="h-6 w-6 text-accent" aria-hidden="true" />
407420
</motion.div>
408421
<div>
409422
<p className="font-semibold text-foreground">Scrollen Sie, um mehr zu erfahren</p>
@@ -423,7 +436,7 @@ function App() {
423436
<Separator orientation="vertical" className="h-8 hidden md:block" />
424437
<span className="text-sm font-medium text-muted-foreground hidden md:block">CASSA · RELIEF</span>
425438
</div>
426-
<div className="flex items-center gap-4">
439+
<nav aria-label="Hauptnavigation" className="flex items-center gap-4">
427440
<Button variant="ghost" size="sm" onClick={() => scrollToSection('fall-becker')} className="hidden md:flex">
428441
Fall Becker
429442
</Button>
@@ -445,15 +458,15 @@ function App() {
445458
<Button asChild size="sm" className="bg-accent hover:bg-accent/90 text-accent-foreground">
446459
<a href="https://www.soprasteria.de/products/cassa" target="_blank" rel="noopener noreferrer">
447460
Mehr erfahren
448-
<ArrowRight className="ml-2 h-4 w-4" />
461+
<ArrowRight className="ml-2 h-4 w-4" aria-hidden="true" />
449462
</a>
450463
</Button>
451-
</div>
464+
</nav>
452465
</div>
453466
</header>
454467

455468
{/* ── SIDE NAVIGATION (desktop only, top-right below header) ── */}
456-
<nav className="hidden xl:block fixed right-6 top-20 z-50">
469+
<nav aria-label="Seitennavigation" className="hidden xl:block fixed right-6 top-20 z-50">
457470
<div className="bg-card/95 backdrop-blur-md border border-border rounded-xl shadow-lg px-4 py-5 max-h-[calc(100vh-6rem)] overflow-y-auto">
458471
{(() => {
459472
let lastGroup = ''
@@ -471,8 +484,9 @@ function App() {
471484
)}
472485
<button
473486
onClick={() => scrollToSection(section.id)}
487+
aria-current={activeSection === section.id ? 'location' : undefined}
474488
className={`
475-
flex items-center gap-2.5 w-full text-left text-sm px-2.5 py-2 rounded-lg transition-all duration-200
489+
flex items-center gap-2.5 w-full text-left text-sm px-2.5 py-2 rounded-lg transition-all duration-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary
476490
${activeSection === section.id
477491
? 'bg-primary/15 text-primary font-semibold border-l-3 border-primary pl-3'
478492
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'}
@@ -495,8 +509,15 @@ function App() {
495509
</div>
496510
</nav>
497511

512+
<main>
513+
498514
{/* ── HERO ── */}
499-
<motion.section style={{ opacity: heroOpacity }} className="hero-pattern py-32 md:py-40 relative overflow-hidden">
515+
<motion.section
516+
id="main-content"
517+
aria-label="Einleitung"
518+
style={{ opacity: heroOpacity }}
519+
className="hero-pattern py-32 md:py-40 relative overflow-hidden"
520+
>
500521
<AnimatedBackground />
501522
<div className="container mx-auto px-6 max-w-7xl relative z-10">
502523
<motion.div
@@ -528,7 +549,7 @@ function App() {
528549
className="bg-accent hover:bg-accent/90 text-accent-foreground text-lg px-10 h-14 shadow-lg hover:shadow-xl transition-shadow"
529550
>
530551
<a href="https://www.soprasteria.de/products/cassa" target="_blank" rel="noopener noreferrer">
531-
<BrainCircuit className="mr-2 h-5 w-5" />
552+
<BrainCircuit className="mr-2 h-5 w-5" aria-hidden="true" />
532553
CASSA entdecken
533554
</a>
534555
</Button>
@@ -539,7 +560,7 @@ function App() {
539560
onClick={() => scrollToSection('challenges')}
540561
>
541562
Herausforderungen verstehen
542-
<ArrowDown className="ml-2 h-5 w-5" />
563+
<ArrowDown className="ml-2 h-5 w-5" aria-hidden="true" />
543564
</Button>
544565
</div>
545566
</motion.div>
@@ -726,9 +747,11 @@ function App() {
726747
<div className="flex items-center gap-4">
727748
<button
728749
onClick={toggleNarration}
729-
className="flex-shrink-0 w-14 h-14 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors shadow-lg"
750+
aria-label={isPlayingNarration ? 'Narration pausieren' : 'Narration abspielen'}
751+
aria-pressed={isPlayingNarration}
752+
className="flex-shrink-0 w-14 h-14 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors shadow-lg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
730753
>
731-
{isPlayingNarration ? <Pause className="h-6 w-6" /> : <Play className="h-6 w-6 ml-0.5" />}
754+
{isPlayingNarration ? <Pause className="h-6 w-6" aria-hidden="true" /> : <Play className="h-6 w-6 ml-0.5" aria-hidden="true" />}
732755
</button>
733756
<div className="flex-1 min-w-0">
734757
<p className="font-semibold text-foreground">Fall Becker — Narration</p>
@@ -738,9 +761,11 @@ function App() {
738761
</div>
739762
<button
740763
onClick={toggleMute}
741-
className="flex-shrink-0 p-2 rounded-lg text-muted-foreground hover:text-foreground transition-colors"
764+
aria-label={isMuted ? 'Ton einschalten' : 'Ton ausschalten'}
765+
aria-pressed={isMuted}
766+
className="flex-shrink-0 p-2 rounded-lg text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
742767
>
743-
{isMuted ? <VolumeX className="h-5 w-5" /> : <Volume2 className="h-5 w-5" />}
768+
{isMuted ? <VolumeX className="h-5 w-5" aria-hidden="true" /> : <Volume2 className="h-5 w-5" aria-hidden="true" />}
744769
</button>
745770
</div>
746771
</CardContent>
@@ -1049,17 +1074,21 @@ function App() {
10491074
<div className="absolute bottom-5 right-5 z-30 flex items-center gap-3">
10501075
<button
10511076
onClick={toggleNarration}
1052-
className="flex items-center gap-2 px-4 py-2.5 rounded-full bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-all hover:scale-105 backdrop-blur-sm"
1077+
aria-label={isPlayingNarration ? 'Narration pausieren' : 'Fall Becker Narration abspielen'}
1078+
aria-pressed={isPlayingNarration}
1079+
className="flex items-center gap-2 px-4 py-2.5 rounded-full bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-all hover:scale-105 backdrop-blur-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-foreground"
10531080
>
1054-
{isPlayingNarration ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4 ml-0.5" />}
1081+
{isPlayingNarration ? <Pause className="h-4 w-4" aria-hidden="true" /> : <Play className="h-4 w-4 ml-0.5" aria-hidden="true" />}
10551082
<span className="text-sm font-medium">{isPlayingNarration ? 'Pause' : 'Fall Becker anhören'}</span>
10561083
</button>
10571084
{isPlayingNarration && (
10581085
<button
10591086
onClick={toggleMute}
1060-
className="p-2 rounded-full bg-slate-800/80 text-slate-300 hover:bg-slate-700/80 hover:text-white transition-colors backdrop-blur-sm"
1087+
aria-label={isMuted ? 'Ton einschalten' : 'Ton ausschalten'}
1088+
aria-pressed={isMuted}
1089+
className="p-2 rounded-full bg-slate-800/80 text-slate-300 hover:bg-slate-700/80 hover:text-white transition-colors backdrop-blur-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
10611090
>
1062-
{isMuted ? <VolumeX className="h-4 w-4" /> : <Volume2 className="h-4 w-4" />}
1091+
{isMuted ? <VolumeX className="h-4 w-4" aria-hidden="true" /> : <Volume2 className="h-4 w-4" aria-hidden="true" />}
10631092
</button>
10641093
)}
10651094
</div>
@@ -1231,7 +1260,7 @@ function App() {
12311260
<CardTitle className="text-base">
12321261
<a href={api.url} target="_blank" rel="noopener noreferrer" className="hover:underline inline-flex items-center gap-1">
12331262
{api.name}
1234-
<ExternalLink className="h-3 w-3 opacity-50" />
1263+
<ExternalLink className="h-3 w-3 opacity-50" aria-hidden="true" />
12351264
</a>
12361265
</CardTitle>
12371266
</div>
@@ -1500,7 +1529,7 @@ function App() {
15001529
<CardTitle className="text-lg leading-tight">
15011530
<a href={card.url} target="_blank" rel="noopener noreferrer" className="hover:underline inline-flex items-center gap-1.5">
15021531
{card.title}
1503-
<ExternalLink className="h-3.5 w-3.5 opacity-40" />
1532+
<ExternalLink className="h-3.5 w-3.5 opacity-40" aria-hidden="true" />
15041533
</a>
15051534
</CardTitle>
15061535
</div>
@@ -1629,7 +1658,7 @@ function App() {
16291658
<h4 className="font-semibold text-sm mb-1">
16301659
<a href={tool.url} target="_blank" rel="noopener noreferrer" className="hover:underline inline-flex items-center gap-1">
16311660
{tool.name}
1632-
<ExternalLink className="h-3 w-3 opacity-40" />
1661+
<ExternalLink className="h-3 w-3 opacity-40" aria-hidden="true" />
16331662
</a>
16341663
</h4>
16351664
<p className="text-xs text-muted-foreground">{tool.desc}</p>
@@ -1944,7 +1973,7 @@ function App() {
19441973
<CardTitle className="text-lg leading-tight">
19451974
<a href={model.url} target="_blank" rel="noopener noreferrer" className="hover:underline inline-flex items-center gap-1.5">
19461975
{model.name}
1947-
<ExternalLink className="h-3.5 w-3.5 opacity-40" />
1976+
<ExternalLink className="h-3.5 w-3.5 opacity-40" aria-hidden="true" />
19481977
</a>
19491978
</CardTitle>
19501979
<p className="text-xs text-muted-foreground font-medium mt-1">{model.role}</p>
@@ -2078,7 +2107,7 @@ function App() {
20782107
{std.url ? (
20792108
<a href={std.url} target="_blank" rel="noopener noreferrer" className="hover:underline inline-flex items-center gap-1">
20802109
{std.name}
2081-
<ExternalLink className="h-3 w-3 opacity-40" />
2110+
<ExternalLink className="h-3 w-3 opacity-40" aria-hidden="true" />
20822111
</a>
20832112
) : std.name}
20842113
</div>
@@ -2174,6 +2203,8 @@ function App() {
21742203
</div>
21752204
</div>
21762205
</footer>
2206+
2207+
</main>
21772208
</div>
21782209
)
21792210
}

src/index.css

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,32 @@
8282
background-image:
8383
repeating-linear-gradient(0deg, oklch(0.88 0.005 60 / 0.5) 0px, transparent 1px, transparent 40px),
8484
repeating-linear-gradient(90deg, oklch(0.88 0.005 60 / 0.5) 0px, transparent 1px, transparent 40px);
85+
}
86+
87+
/* ── WCAG 2.2 — prefers-reduced-motion ── */
88+
@media (prefers-reduced-motion: reduce) {
89+
/* Disable all CSS transitions and animations */
90+
*,
91+
*::before,
92+
*::after {
93+
animation-duration: 0.01ms !important;
94+
animation-iteration-count: 1 !important;
95+
transition-duration: 0.01ms !important;
96+
scroll-behavior: auto !important;
97+
}
98+
99+
/* Framer-motion outputs inline transform/opacity transitions;
100+
set transition to instant so whileInView fade-ins still work
101+
but don't animate */
102+
[style*="transform"],
103+
[style*="opacity"] {
104+
transition: none !important;
105+
}
106+
}
107+
108+
/* ── WCAG 2.2 — focus-visible ring ── */
109+
:focus-visible {
110+
outline: 2px solid var(--primary);
111+
outline-offset: 2px;
112+
border-radius: 4px;
85113
}

0 commit comments

Comments
 (0)