diff --git a/assets/icons/close.svg b/assets/icons/close.svg
new file mode 100644
index 0000000..26a0e5e
--- /dev/null
+++ b/assets/icons/close.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/search.svg b/assets/icons/search.svg
new file mode 100644
index 0000000..4664f85
--- /dev/null
+++ b/assets/icons/search.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/logo-text.svg b/assets/images/logo-text.svg
new file mode 100644
index 0000000..dd78e01
--- /dev/null
+++ b/assets/images/logo-text.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/components/header/header.component.yml b/src/components/header/header.component.yml
new file mode 100644
index 0000000..5f1ee15
--- /dev/null
+++ b/src/components/header/header.component.yml
@@ -0,0 +1,23 @@
+$schema: https://git.drupalcode.org/project/drupal/-/raw/10.1.x/core/modules/sdc/src/metadata.schema.json
+
+name: Header
+group: Components
+status: stable
+props:
+ type: object
+ properties:
+ header__show_search:
+ type: boolean
+ title: Show Search
+ description: Control visibility of the search form in the header.
+ data: true
+slots:
+ header__branding:
+ title: Branding
+ description: Branding content (logo, site name, etc).
+ header__menu:
+ title: Menu
+ description: Navigation menu content.
+ header__search:
+ title: Search
+ description: Search form content.
diff --git a/src/components/header/header.scss b/src/components/header/header.scss
new file mode 100644
index 0000000..df4fb82
--- /dev/null
+++ b/src/components/header/header.scss
@@ -0,0 +1,66 @@
+@use '../../foundation/utility/rem-calc' as *;
+@use '../../foundation/breakpoints/breakpoints' as *;
+
+.header {
+ display: flex;
+ width: 100%;
+ min-height: 5rem;
+ padding-inline: var(--spacing-xl) var(--spacing-lg);
+ justify-content: space-between;
+ align-items: center;
+ position: relative;
+ box-shadow: 0 4px 4px 0 rgba(197, 197, 197, 0.25);
+
+ @include breakpoint('medium') {
+ justify-content: flex-start;
+ align-items: stretch;
+ padding-inline: 0;
+ }
+}
+
+/* Branding */
+.header__branding {
+ display: flex;
+ align-items: center;
+
+ .logo {
+ width: auto;
+ height: 2.5rem;
+
+ @include breakpoint('medium') {
+ margin-left: var(--spacing-xl);
+ height: 3.5rem;
+ }
+ }
+
+ .logo__link {
+ height: 100%;
+ display: flex;
+ }
+}
+
+/* Main navigation */
+.header__menu {
+ @include breakpoint('medium') {
+ flex: 1;
+ display: flex;
+ justify-content: end;
+ margin-right: var(--spacing-xl);
+ }
+}
+
+.main-nav__menu-list-wrapper--level-0 {
+ .header & {
+ top: 100%;
+ padding-right: var(--spacing-lg);
+ }
+}
+
+/* Search form */
+.header__search {
+ display: none;
+
+ @include breakpoint('medium') {
+ display: block;
+ }
+}
diff --git a/src/components/header/header.stories.js b/src/components/header/header.stories.js
new file mode 100644
index 0000000..a032302
--- /dev/null
+++ b/src/components/header/header.stories.js
@@ -0,0 +1,51 @@
+// Markup.
+import template from './header.twig';
+
+// Data.
+import { props as mainMenuProps } from '../navigation/main/main.component.yml';
+import { props as searchProps } from '../search/search.component.yml';
+
+// JavaScript.
+import '../navigation/base/menu-toggle/menu-toggle';
+import '../navigation/main/main';
+import '../search/search';
+
+const mainMenuData = mainMenuProps.properties;
+const searchData = searchProps.properties;
+
+/**
+ * Storybook Definition.
+ */
+export default {
+ title: 'Components/Header',
+ argTypes: {
+ mainMenu: {
+ name: 'Menu Items',
+ control: { type: 'object' },
+ },
+ searchPlaceholder: {
+ name: 'Search Placeholder',
+ type: 'string',
+ },
+ showSearch: {
+ name: 'Show Search',
+ control: { type: 'boolean' },
+ },
+ },
+ args: {
+ mainMenu: mainMenuData.items.data,
+ searchLabel: searchData.search__label.data,
+ searchPlaceholder: searchData.search__placeholder.data,
+ showSearch: true,
+ },
+};
+
+export const Header = ({ mainMenu, searchPlaceholder, searchLabel, showSearch }) =>
+ template({
+ header__menu: mainMenu,
+ header__branding: '
',
+ header__show_search: showSearch,
+ header__search__placeholder: searchPlaceholder,
+ header__search__label: searchLabel,
+ });
+
diff --git a/src/components/header/header.twig b/src/components/header/header.twig
new file mode 100644
index 0000000..830b355
--- /dev/null
+++ b/src/components/header/header.twig
@@ -0,0 +1,46 @@
+{#
+ # Header Component Template
+ # Available Variables:
+ # - header__base_class: (string) base BEM class, default 'header'
+ # - header__search__placeholder: (string) placeholder text for search input
+ # - header__search__label: (string) label for search toggle button
+ # - header__menu_label: (string) label for mobile menu toggle
+ # - additional_attributes: (array) HTML attributes for the header element
+ # - search_form: (string) rendered search form markup
+ #
+ # Available Blocks (Slots):
+ # - header__branding
+ # - header__menu
+ #}
+
+{% set header__base_class = 'header' %}
+
+
+ {# Branding #}
+
+ {% block header__branding %}
+ {% include "@components/logo/logo.twig" with {
+ image_src: directory ? '/' ~ directory ~ '/assets/images/logo-text.svg' : 'logo-text.svg',
+ } %}
+ {% endblock %}
+
+ {# Main navigation #}
+
+ {% block header__menu %}
+ {% include "@components/navigation/main/main.twig" with {
+ items: header__menu,
+ } %}
+ {% endblock %}
+
+ {# Search form #}
+ {% if header__show_search %}
+
+ {% block header__search %}
+ {% include '@components/search/search.twig' with {
+ search__placeholder: header__search__placeholder,
+ search__label: header__search__label,
+ } %}
+ {% endblock %}
+
+ {% endif %}
+
diff --git a/src/components/logo/logo.twig b/src/components/logo/logo.twig
index d7c567e..f38febe 100644
--- a/src/components/logo/logo.twig
+++ b/src/components/logo/logo.twig
@@ -20,8 +20,8 @@
{% block link__content %}
{% include "@components/image/responsive-image.twig" with {
output_image_tag: true,
- image__src: directory ? '/' ~ directory ~ '/assets/images/logo.png' : 'logo.png',
- image__alt: 'Logo',
+ image__src: image_src|default(directory ? '/' ~ directory ~ '/assets/images/logo.png' : 'logo.png'),
+ image__alt: image_alt|default('Logo'),
responsive_image_blockname: 'logo',
} %}
{% endblock %}
diff --git a/src/components/search/search.component.yml b/src/components/search/search.component.yml
new file mode 100644
index 0000000..efe835d
--- /dev/null
+++ b/src/components/search/search.component.yml
@@ -0,0 +1,23 @@
+$schema: https://git.drupalcode.org/project/drupal/-/raw/10.1.x/core/modules/sdc/src/metadata.schema.json
+
+name: HeaderSearch
+group: Components
+status: stable
+props:
+ type: object
+ properties:
+ search__placeholder:
+ type: string
+ title: Search Placeholder
+ description: Placeholder text for search input.
+ data: Search
+ search__label:
+ type: string
+ title: Search Label
+ description: Label for the search toggle button.
+ data: Search
+slots:
+ # Search form markup provided by parent
+ search_form:
+ title: Search Form
+ description: Rendered search form markup.
diff --git a/src/components/search/search.js b/src/components/search/search.js
new file mode 100644
index 0000000..eaa78cc
--- /dev/null
+++ b/src/components/search/search.js
@@ -0,0 +1,69 @@
+// Search component behavior for header
+Drupal.behaviors.headerSearch = {
+ attach(context) {
+ // Helper to close search form
+ const closeSearch = () => {
+ document.querySelectorAll('.header.search-active').forEach((header) => {
+ header.querySelectorAll('.search__toggle').forEach((t) => t.setAttribute('aria-expanded', 'false'));
+ header.querySelector('.search__form__wrapper')?.setAttribute('aria-hidden', 'true');
+ header.classList.remove('search-active');
+ });
+ };
+
+ // Helper to handle search form location
+ const handleSearchLocation = () => {
+ const isMobile = window.innerWidth < 1040;
+ const searchForm = document.querySelector('.search__form');
+ const desktopContainer = document.querySelector('.search__form__wrapper');
+ const mobileContainer = document.querySelector('.header__menu > nav > div');
+
+ if (!searchForm || !mobileContainer || !desktopContainer) return;
+
+ if (isMobile) {
+ if (searchForm.parentElement !== mobileContainer) {
+ mobileContainer.prepend(searchForm);
+ }
+ } else {
+ if (searchForm.parentElement !== desktopContainer) {
+ desktopContainer.prepend(searchForm);
+ }
+ }
+ };
+
+ // Initial calls
+ handleSearchLocation();
+
+ // Search Toggles
+ context.querySelectorAll('.search__toggle').forEach((toggle) => {
+ if (toggle.dataset.jsProcessed) return;
+ toggle.dataset.jsProcessed = true;
+ toggle.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const header = toggle.closest('.header');
+ const isOpening = !header.classList.contains('search-active');
+ closeSearch();
+ if (isOpening) {
+ header.classList.add('search-active');
+ toggle.setAttribute('aria-expanded', 'true');
+ header.querySelector('input[type="search"]')?.focus();
+ header.querySelector('.search__form__wrapper')?.setAttribute('aria-hidden', 'false');
+ }
+ });
+ });
+
+ // Global listeners.
+ if (!window.headerSearchInitialized) {
+ window.headerSearchInitialized = true;
+ const mediaQuery = window.matchMedia('(min-width: 1040px)');
+ const handleBreakpointChange = () => {
+ handleSearchLocation();
+ closeSearch();
+ };
+
+ mediaQuery.addEventListener('change', handleBreakpointChange);
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') closeSearch();
+ });
+ }
+ }
+};
diff --git a/src/components/search/search.scss b/src/components/search/search.scss
new file mode 100644
index 0000000..374005b
--- /dev/null
+++ b/src/components/search/search.scss
@@ -0,0 +1,174 @@
+@use '../../foundation/utility/rem-calc' as *;
+@use '../../foundation/breakpoints/breakpoints' as *;
+
+.search {
+ display: block;
+ height: 100%;
+}
+
+/* Search form */
+.search__form__wrapper {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ width: 100%;
+ max-height: 0;
+ overflow: hidden;
+ visibility: hidden;
+ z-index: 1;
+ transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), visibility 0.4s;
+
+ @include breakpoint('medium') {
+ top: 0;
+ right: 0;
+ left: auto;
+ height: 100%;
+ max-height: none;
+ max-width: 0;
+ transition: max-width 0.4s cubic-bezier(0.4, 0, 0.2, 1), visibility 0.4s;
+ }
+
+ &[aria-hidden='false'] {
+ max-height: rem-calc(100);
+ visibility: visible;
+
+ @include breakpoint('medium') {
+ max-width: rem-calc(560);
+ }
+ }
+}
+
+.search__form {
+ display: flex;
+ width: 100%;
+ height: initial;
+ align-items: center;
+ padding-block: var(--spacing-lg) var(--spacing-xl);
+ padding-inline: 0;
+ background-color: var(--color-white);
+
+ @include breakpoint('medium') {
+ padding-block: 0;
+ position: absolute;
+ right: 0;
+ width: auto;
+ height: 100%;
+ padding-block: var(--spacing-lg);
+ padding-inline: var(--spacing-xl) calc(var(--spacing-lg));
+ background-color: var(--color-primary-darker);
+ }
+}
+
+.search__form__input {
+ flex: 1;
+ border: none;
+ min-height: 2.25rem;
+ color: var(--color-text-body);
+ font-size: var(--font-size-body);
+ background-color: transparent;
+ margin-right: var(--spacing-lg);
+ padding-left: var(--spacing-lg);
+ border-bottom: 1px solid var(--color-primary-darker);
+
+ @include breakpoint('medium') {
+ border-bottom: 1px solid var(--color-white);
+ color: var(--color-white);
+ }
+}
+
+.search__form__input::placeholder {
+ opacity: 1;
+ color: var(--color-primary-dark);
+
+ @include breakpoint('medium') {
+ color: var(--color-white);
+ }
+}
+
+.search__form__input::-ms-input-placeholder {
+ color: var(--color-primary-dark);
+
+ @include breakpoint('medium') {
+ color: var(--color-white);
+ }
+}
+
+.search__form__actions {
+ display: flex;
+ align-items: center;
+
+ button {
+ cursor: pointer;
+ border: none;
+ width: 2.25rem;
+ height: 2.25rem;
+ padding: var(--spacing-md);
+ background-color: transparent;
+
+ @include breakpoint('medium') {
+ display: flex;
+ }
+
+ &.search__toggle {
+ display: none;
+
+ @include breakpoint('medium') {
+ display: flex;
+ }
+ }
+
+ svg {
+ width: 100%;
+ height: 100%;
+ padding: 0;
+ user-select: none;
+ color: var(--color-primary-dark);
+
+ @include breakpoint('medium') {
+ color: var(--color-white);
+ }
+ }
+ }
+}
+
+/* Search toggle button */
+.search__toggle {
+ cursor: pointer;
+ font-weight: 700;
+ text-decoration: none;
+ text-transform: uppercase;
+ color: var(--color-primary-dark);
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-lg);
+ height: 100%;
+ background-color: transparent;
+ border: none;
+ padding: 0 var(--spacing-md);
+ margin-inline: auto var(--spacing-md);
+
+ @include breakpoint('medium') {
+ background-color: var(--color-primary-dark);
+ padding: 0 var(--spacing-xl);
+ color: var(--color-white);
+ margin-inline: 0;
+ }
+
+ span {
+ display: none;
+
+ @include breakpoint('medium') {
+ display: block;
+ }
+ }
+
+ svg {
+ width: rem-calc(24);
+ height: rem-calc(24);
+ user-select: none;
+
+ &.icon--close {
+ display: none;
+ }
+ }
+}
diff --git a/src/components/search/search.stories.js b/src/components/search/search.stories.js
new file mode 100644
index 0000000..b290f33
--- /dev/null
+++ b/src/components/search/search.stories.js
@@ -0,0 +1,27 @@
+import template from './search.twig';
+import { props } from './search.component.yml';
+
+import './search';
+
+/**
+ * Storybook definition for Search component.
+ */
+export default {
+ title: 'Component/Search',
+ argTypes: {
+ header__search_placeholder: {
+ name: 'Placeholder',
+ control: { type: 'text' },
+ },
+ header__search_label: {
+ name: 'Search Button Label',
+ control: { type: 'text' },
+ },
+ },
+ args: {
+ search__placeholder: props.properties.search__placeholder.data,
+ search__label: props.properties.search__label.data,
+ },
+};
+
+export const Search = (args) => template(args);
diff --git a/src/components/search/search.twig b/src/components/search/search.twig
new file mode 100644
index 0000000..5f9ecea
--- /dev/null
+++ b/src/components/search/search.twig
@@ -0,0 +1,45 @@
+{#
+ # Search Component Template
+ # Available Variables:
+ # - search__placeholder: (string) placeholder text for the search input
+ # - search__label: (string) label for the toggle button
+ #}
+
+{% set search__base_class = 'search' %}
+{% set search__label = search__label|default('Search') %}
+{% set search__placeholder = search__placeholder|default('Search') %}
+
+{% set search_form %}
+
+ {% block header__search %}
+
+ {% endblock %}
+
+{% endset %}
+
+
+
+ {{ search_form }}
+