Let a header that paints its own surface show through while sticky
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s

A section in the header set to #ffffff00 for the scrolled page still looked
white: the sticky header element paints its own white surface and a shadow
line underneath, so a transparent section showed that surface instead of the
page, and the line could be neither recoloured nor removed.

When a top-level header section defines a background of its own - normal,
hover or scrolled - the header element now gets `has-own-surface` and paints
nothing. A header without its own design keeps the white surface, so it does
not turn see-through over the content. A text colour alone is not a surface,
and a boxed column is not the whole header.

The check is a Twig function rather than a controller variable because four
paths render the header: page, archive, editor preview and area preview.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-15 07:15:31 +02:00
1 parent fb7cfa37d1
commit a4ee04533c
5 files changed
+85 -2

No files matched your search

+4
View File
@@ -409,6 +409,10 @@ body.consent-open{padding-bottom:320px}
Inline geoeffnet stand es sonst in der schmalen Spalte neben der Marke und schob den
ganzen Kopf auseinander - auf dem Telefon sah es aus, als fehle das Menue. */
.site-header{position:relative}
/* Gibt der Kopfbereich seine Flaeche selbst vor, malt der klebende Kopf keine zweite dahinter:
sonst zeigte eine durchsichtige Sektion Weiss statt Seite, und die Schattenlinie liesse sich
weder faerben noch abschalten. Linie und Schatten kommen dann aus Rahmen und Wirkung. */
.site-header.is-sticky.has-own-surface{background:transparent;box-shadow:none}
.site-header .menu-burger[open]>summary{border-radius:8px}
.site-header .menu-burger-body{position:absolute;left:0;right:0;top:100%;z-index:30;
padding:15px max(16px,calc((100% - var(--k-container,1180px)) / 2 + 16px)) 25px;
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
/**
* Ob der Kopfbereich seine Fläche selbst gestaltet.
*
* Der klebende Kopf malt eine weiße Fläche mit Schattenlinie, damit ein Kopf ohne eigene
* Gestaltung beim Scrollen nicht über dem Inhalt durchscheint. Gibt eine Sektion des Kopfbereichs
* aber selbst einen Hintergrund vor - normal, beim Überfahren oder beim Scrollen -, lag diese
* Fläche dahinter: eine durchsichtige Sektion zeigte Weiß statt Seite, und die Linie ließ sich
* weder färben noch abschalten. Dann trägt die Sektion die Fläche allein.
*
* Eine Funktion statt einer Variablen, weil vier Wege den Kopf ausgeben - Seite, Archiv,
* Editor-Vorschau und Bereichs-Vorschau - und keiner davon vergessen werden darf.
*/
final class HeaderExtension extends AbstractExtension
{
/** Die Eigenschaften, die eine Fläche ausmachen. Eine Textfarbe allein ist keine. */
private const SURFACE = ['backgroundColor', 'gradientFrom', 'gradientTo', 'backgroundImage'];
public function getFunctions(): array
{
return [new TwigFunction('kant_header_surface', self::ownSurface(...))];
}
/**
* Nur Sektionen der obersten Ebene zählen: eine Spalte mit weißem Kasten um das Logo macht
* noch keinen Kopf, und ohne Fläche dahinter schiene der Rest über dem Inhalt durch.
*/
public static function ownSurface(?array $document): bool
{
foreach ($document['blocks'] ?? [] as $block) {
if ('section' !== ($block['type'] ?? null)) {
continue;
}
foreach ($block['style'] ?? [] as $values) {
if (!is_array($values)) {
continue;
}
foreach ([$values, $values['hover'] ?? [], $values['scrolled'] ?? []] as $state) {
foreach (self::SURFACE as $key) {
if (is_array($state) && '' !== (string) ($state[$key] ?? '')) {
return true;
}
}
}
}
}
return false;
}
}
+1 -1
View File
@@ -26,7 +26,7 @@
<body class="kant{{ consentOpen ? ' consent-open' : '' }}">
<a class="skip-link" href="#main">Zum Inhalt springen</a>
{% if areas.header|default(null) %}
<header class="site-header{{ siteSettings.load().stickyHeader ? ' is-sticky' : '' }}{{ siteSettings.load().headerOverlay ? ' is-overlay' : '' }}">
<header class="site-header{{ siteSettings.load().stickyHeader ? ' is-sticky' : '' }}{{ siteSettings.load().headerOverlay ? ' is-overlay' : '' }}{{ kant_header_surface(areas.header) ? ' has-own-surface' : '' }}">
{% for block in areas.header.blocks %}{% include 'blocks/block.html.twig' with {block: block, postLists: {}, forms: {}, breadcrumbs: [], navigation: navigation|default({}), sections: sections|default({}), richText: true, richHeadings: areas.header.schemaVersion|default(1) >= 17, eagerImage: null} only %}{% endfor %}
</header>
{% endif %}
+1 -1
View File
@@ -77,7 +77,7 @@
{# Header and footer are block documents like any page, so they are built from elements
instead of being written into this template. #}
{% if areas.header|default(null) %}
<header class="site-header{{ siteSettings.load().stickyHeader ? ' is-sticky' : '' }}{{ siteSettings.load().headerOverlay ? ' is-overlay' : '' }}">
<header class="site-header{{ siteSettings.load().stickyHeader ? ' is-sticky' : '' }}{{ siteSettings.load().headerOverlay ? ' is-overlay' : '' }}{{ kant_header_surface(areas.header) ? ' has-own-surface' : '' }}">
{% for block in areas.header.blocks %}{% include 'blocks/block.html.twig' with {block: block, editing: editing|default(false), postLists: {}, forms: {}, breadcrumbs: [], navigation: navigation|default({}), sections: sections|default({}), richText: areas.header.schemaVersion|default(1) >= 12, richHeadings: areas.header.schemaVersion|default(1) >= 17, eagerImage: eagerImage|default(null), inHeader: true} only %}{% endfor %}
</header>
{% endif %}
+23
View File
@@ -810,6 +810,29 @@ final class CmsTest extends TestCase
self::assertStringContainsString('class="site-header is-sticky"', $this->request('/')->getContent());
}
/**
* Ein Kopfbereich mit eigener Fläche bekommt keine zweite dahinter.
*
* Beim Nachbau der Tagespflege war die Sektion beim Scrollen auf #ffffff00 gestellt und blieb
* trotzdem weiß: der klebende Kopf malte seine eigene Fläche und Schattenlinie darunter.
*/
public function testAHeaderThatPaintsItsOwnSurfaceGetsNoSecondOne(): void
{
$settings = $this->site->load();
$this->site->save([...$settings, 'stickyHeader' => true, 'sameAs' => '', 'consentGroups' => $settings['consentGroups']]);
self::assertStringContainsString('class="site-header is-sticky"', $this->request('/')->getContent(), 'Ohne eigene Fläche bleibt die weiße des Kopfes.');
self::assertTrue(\App\Twig\HeaderExtension::ownSurface(['blocks' => [['id' => 's', 'type' => 'section', 'style' => ['desktop' => ['scrolled' => ['backgroundColor' => '#ffffff00']]]]]]));
self::assertTrue(\App\Twig\HeaderExtension::ownSurface(['blocks' => [['id' => 's', 'type' => 'section', 'style' => ['mobile' => ['backgroundColor' => '#294655']]]]]));
// Eine Textfarbe ist keine Fläche, und ein Kasten in einer Spalte macht noch keinen Kopf.
self::assertFalse(\App\Twig\HeaderExtension::ownSurface(['blocks' => [['id' => 's', 'type' => 'section', 'style' => ['desktop' => ['color' => '#294655']]]]]));
self::assertFalse(\App\Twig\HeaderExtension::ownSurface(['blocks' => [['id' => 's', 'type' => 'section', 'children' => [['id' => 'c', 'type' => 'column', 'style' => ['desktop' => ['backgroundColor' => '#ffffff']]]]]]]));
self::assertFalse(\App\Twig\HeaderExtension::ownSurface(null));
$css = (string) file_get_contents(__DIR__.'/../public/assets/site.css');
self::assertStringContainsString('.site-header.is-sticky.has-own-surface{background:transparent;box-shadow:none}', $css);
}
/** @return iterable<string, array{string, bool}> */
public static function imageLinks(): iterable
{