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 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. */ ganzen Kopf auseinander - auf dem Telefon sah es aus, als fehle das Menue. */
.site-header{position:relative} .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[open]>summary{border-radius:8px}
.site-header .menu-burger-body{position:absolute;left:0;right:0;top:100%;z-index:30; .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; 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' : '' }}"> <body class="kant{{ consentOpen ? ' consent-open' : '' }}">
<a class="skip-link" href="#main">Zum Inhalt springen</a> <a class="skip-link" href="#main">Zum Inhalt springen</a>
{% if areas.header|default(null) %} {% 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 %} {% 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> </header>
{% endif %} {% endif %}
+1 -1
View File
@@ -77,7 +77,7 @@
{# Header and footer are block documents like any page, so they are built from elements {# Header and footer are block documents like any page, so they are built from elements
instead of being written into this template. #} instead of being written into this template. #}
{% if areas.header|default(null) %} {% 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 %} {% 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> </header>
{% endif %} {% endif %}
+23
View File
@@ -810,6 +810,29 @@ final class CmsTest extends TestCase
self::assertStringContainsString('class="site-header is-sticky"', $this->request('/')->getContent()); 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}> */ /** @return iterable<string, array{string, bool}> */
public static function imageLinks(): iterable public static function imageLinks(): iterable
{ {