feat: add focus-visible rings and refactor accordion chevron

Add global :focus-visible outline style and migrate form components
from :focus box-shadow to :focus-visible outline. Refactor accordion
chevron from CSS ::after pseudo-element to explicit span element.
This commit is contained in:
Florian Schroedl
2026-03-05 11:34:07 +01:00
parent aa3370565f
commit e3787363d2
4 changed files with 50 additions and 27 deletions

View File

@@ -26,7 +26,9 @@
base-attrs (cond-> (merge {:class classes} attrs) base-attrs (cond-> (merge {:class classes} attrs)
open (assoc :open true))] open (assoc :open true))]
[:details base-attrs [:details base-attrs
[:summary {:class "accordion-trigger"} title] [:summary {:class "accordion-trigger"}
[:span {:class "accordion-trigger-text"} title]
[:span {:class "accordion-chevron" :aria-hidden "true"}]]
(into [:div {:class "accordion-content"}] children)]) (into [:div {:class "accordion-content"}] children)])
:cljs :cljs
@@ -35,7 +37,9 @@
base-attrs (cond-> (merge {:class classes} attrs) base-attrs (cond-> (merge {:class classes} attrs)
open (assoc :open true))] open (assoc :open true))]
[:details base-attrs [:details base-attrs
[:summary {:class ["accordion-trigger"]} title] [:summary {:class ["accordion-trigger"]}
[:span {:class ["accordion-trigger-text"]} title]
[:span {:class ["accordion-chevron"] :aria-hidden "true"}]]
(into [:div {:class ["accordion-content"]}] children)]) (into [:div {:class ["accordion-content"]}] children)])
:clj :clj
@@ -44,5 +48,7 @@
base-attrs (cond-> (merge {:class classes} attrs) base-attrs (cond-> (merge {:class classes} attrs)
open (assoc :open true))] open (assoc :open true))]
[:details base-attrs [:details base-attrs
[:summary {:class "accordion-trigger"} title] [:summary {:class "accordion-trigger"}
[:span {:class "accordion-trigger-text"} title]
[:span {:class "accordion-chevron" :aria-hidden "true"}]]
(into [:div {:class "accordion-content"}] children)]))) (into [:div {:class "accordion-content"}] children)])))

View File

@@ -9,6 +9,10 @@
overflow: hidden; overflow: hidden;
} }
.accordion-trigger:focus-visible {
outline-offset: -2px;
}
.accordion + .accordion { .accordion + .accordion {
margin-top: -1px; margin-top: -1px;
border-top-left-radius: 0; border-top-left-radius: 0;
@@ -25,7 +29,6 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: var(--size-2); gap: var(--size-2);
width: 100%;
padding: var(--size-4); padding: var(--size-4);
font-weight: 500; font-weight: 500;
font-size: inherit; font-size: inherit;
@@ -50,15 +53,20 @@
background: var(--bg-1); background: var(--bg-1);
} }
.accordion-trigger::after { .accordion-trigger-text {
content: ""; flex: 1;
text-align: left;
}
.accordion-chevron {
display: block;
width: 1em; width: 1em;
height: 1em; height: 1em;
flex-shrink: 0; flex-shrink: 0;
background-color: currentColor; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2371717a' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); background-size: contain;
mask-size: contain; background-repeat: no-repeat;
mask-repeat: no-repeat; background-position: center;
transition: transform 150ms ease; transition: transform 150ms ease;
} }
@@ -66,7 +74,7 @@
border-bottom: var(--border-0); border-bottom: var(--border-0);
} }
.accordion[open] > .accordion-trigger::after { .accordion[open] > .accordion-trigger .accordion-chevron {
transform: rotate(180deg); transform: rotate(180deg);
} }

View File

@@ -63,6 +63,17 @@
margin: 0; margin: 0;
background: var(--bg-0); background: var(--bg-0);
color: var(--fg-0); color: var(--fg-0);
}
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
box-shadow: none;
}
:focus:not(:focus-visible) {
outline: none;
box-shadow: none;
}") }")
(defn collect-component-css (defn collect-component-css

View File

@@ -23,10 +23,10 @@
border-color: var(--danger); border-color: var(--danger);
} }
.form-field--error .form-input:focus, .form-field--error .form-input:focus-visible,
.form-field--error .form-textarea:focus, .form-field--error .form-textarea:focus-visible,
.form-field--error .form-select:focus { .form-field--error .form-select:focus-visible {
box-shadow: 0 0 0 2px color-mix(in srgb, var(--danger) 20%, transparent); outline-color: var(--danger);
} }
/* ── Label ─────────────────────────────────────────────────────── */ /* ── Label ─────────────────────────────────────────────────────── */
@@ -60,12 +60,11 @@
color: var(--fg-2); color: var(--fg-2);
} }
.form-input:focus, .form-input:focus-visible,
.form-textarea:focus, .form-textarea:focus-visible,
.form-select:focus { .form-select:focus-visible {
outline: none;
border-color: var(--accent); border-color: var(--accent);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 20%, transparent); outline-offset: -1px;
} }
.form-input:disabled, .form-input:disabled,
@@ -81,9 +80,9 @@
border-color: var(--danger); border-color: var(--danger);
} }
.form-input--error:focus, .form-input--error:focus-visible,
.form-textarea--error:focus { .form-textarea--error:focus-visible {
box-shadow: 0 0 0 2px color-mix(in srgb, var(--danger) 20%, transparent); outline-color: var(--danger);
} }
/* ── Textarea ──────────────────────────────────────────────────── */ /* ── Textarea ──────────────────────────────────────────────────── */
@@ -148,10 +147,9 @@
background-repeat: no-repeat; background-repeat: no-repeat;
} }
.form-checkbox:focus, .form-checkbox:focus-visible,
.form-radio:focus { .form-radio:focus-visible {
outline: none; outline-offset: 0;
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 20%, transparent);
} }
.form-checkbox:disabled, .form-checkbox:disabled,