<header id="example-header--compact" class="cwf-header cwf-header--gold cwf-header--compact">
<div class="cwf-header__container">
<div class="cwf-header__title cwf-header__title--reverse">
<h1 class="cwf-header__department"> <a class="cwf-header__link" href="/">
Example Site
</a>
</h1>
</div>
<div class="cwf-header__controls"> <button class="cwf-header__toggle" aria-controls="cwf-header__search" aria-keyshortcuts="/">
<i class="fas fa-search cwf-header__icon"></i>
<span class="cwf-header__text">Search</span>
</button>
</div>
<div id="cwf-header__features" class="cwf-header__features">
<button class="cwf-header__exit" aria-controls="cwf-header__features">
Close
</button>
<form id="cwf-header__search" class="cwf-header__search" action="https://search.vcu.edu/s/search.html">
<input type="hidden" name="collection" value="vcu-meta" /> <input type="hidden" name="clive" value="vcu-ts" />
<input type="text" name="query" id="query" class="cwf-header__input" required />
<button type="submit" class="cwf-header__submit" aria-label="Submit">
<i class="fas fa-search"></i>
</button>
<label for="query" class="cwf-header__label">
Search
</label>
</form>
</div>
</div>
</header>
<header id="{{ id ?? 'cwf-header' }}"
class="cwf-header cwf-header--{{ theme }} {{
compact
? 'cwf-header--compact'
}}">
<div class="cwf-header__container">
<div class="cwf-header__title cwf-header__title--reverse">
<h1 class="cwf-header__department">
{%- if parent and parent.name and not parent.url %}
<a class="cwf-header__link cwf-header__link--multi-line"
href="{{ unit.url }}">
<span class="h2 cwf-header__parent">
{{ parent.name }}
</span>
<span>{{ unit.name }}</span>
</a>
{% else %}
<a class="cwf-header__link" href="{{ unit.url }}">
{{ unit.name }}
</a>
{% endif %}
</h1>
{%- if parent and parent.name and parent.url %}
<h2 class="cwf-header__parent">
<a class="cwf-header__link" href="{{ parent.url }}">
{{ parent.name }}
</a>
</h2>
{% endif -%}
</div>
{%- if quick_links or search %}
<div class="cwf-header__controls">
{%- if quick_links %}
<button class="cwf-header__toggle"
aria-controls="cwf-header__nav"
aria-keyshortcuts=".">
<i class="fas fa-ellipsis-v cwf-header__icon"></i>
<span class="cwf-header__text">Links</span>
</button>
{% endif %} {%- if search %}
<button class="cwf-header__toggle"
aria-controls="cwf-header__search"
aria-keyshortcuts="/">
<i class="fas fa-search cwf-header__icon"></i>
<span class="cwf-header__text">Search</span>
</button>
{% endif %}
</div>
<div id="cwf-header__features" class="cwf-header__features">
<button class="cwf-header__exit"
aria-controls="cwf-header__features">
Close
</button>
{%- if quick_links %}
<nav id="cwf-header__nav"
class="cwf-header__nav"
aria-label="Quick links">
{%- for quick_link in quick_links %}
<a href="{{ quick_link.url }}"
class="cwf-header__link">
{{ quick_link.name }}
</a>
{%- endfor %}
</nav>
{%- endif %} {%- if search %}
<form id="cwf-header__search"
class="cwf-header__search"
action="https://search.vcu.edu/s/search.html">
<input type="hidden"
name="collection"
value="vcu-meta" />
{%- if search.collection %}
<input type="hidden"
name="clive"
value="{{ search.collection }}" />
{% endif -%} {%- if search.scope %}
<input type="hidden"
name="scope"
value="{{ search.scope }}" />
{% endif -%}
<input type="text"
name="query"
id="query"
class="cwf-header__input"
required />
<button type="submit"
class="cwf-header__submit"
aria-label="Submit">
<i class="fas fa-search"></i>
</button>
<label for="query" class="cwf-header__label">
Search
</label>
</form>
{% endif %}
</div>
{% endif %}
</div>
</header>
{
"unit": {
"name": "Example Site",
"url": "/"
},
"compact": true,
"theme": "gold",
"search": {
"collection": "vcu-ts",
"scope": null
},
"id": "example-header--compact"
}
// Header component styles
@use "../../shared/animation";
@use "../../shared/media";
@use "../../shared/style";
@use "../../shared/theme";
@use "sass:map";
// Selector prefix
$prefix: "cwf" !default;
.#{$prefix}-header {
display: flex;
justify-content: center;
padding-top: 1rem;
padding-bottom: 1rem;
background-color: var(--cwf-header--background-color);
font-family: theme.font--sans-serif();
@include media.breakpoint {
min-height: 8rem;
padding-top: 2rem;
padding-bottom: 2rem;
}
}
// Header white colors
$background-color--white: style.color("white") !default;
$link__color--white: style.color("gray-darkest") !default;
$link--interact__color--white: style.color("black") !default;
$input__background-color--white: style.color("white-dark") !default;
$input--focus__background-color--white: style.color("white") !default;
$input__border-color--white: style.color("white-darkest") !default;
$input--hover__border-color--white: style.color("gray-lightest") !default;
$input--focus__border-color--white: style.color("gray-darkest") !default;
$input__color--white: style.color("gray-darkest") !default;
$submit__border-color--white: style.color("white-darkest") !default;
$submit--hover__border-color--white: style.color("gray-lightest") !default;
$submit--focus__border-color--white: style.color("gray-darkest") !default;
$label__color--white: style.color("gray-lightest") !default;
// Header gray colors
$background-color--gray: style.color("white-dark") !default;
$link__color--gray: style.color("gray-darkest") !default;
$link--interact__color--gray: style.color("black") !default;
$input__background-color--gray: style.color("white") !default;
$input--focus__background-color--gray: style.color("white") !default;
$input__border-color--gray: style.darken("white-darkest", 12.5%) !default;
$input--hover__border-color--gray: style.darken("gray-lightest", 21%) !default;
$input--focus__border-color--gray: style.color("gray-darkest") !default;
$input__color--gray: style.color("gray-darkest") !default;
$submit__border-color--gray: style.color("white-darkest") !default;
$submit--hover__border-color--gray: style.color("gray-lightest") !default;
$submit--focus__border-color--gray: style.color("gray-darkest") !default;
$label__color--gray: style.color("gray-lightest") !default;
// Header gold colors
$background-color--gold: style.color("gold") !default;
$link__color--gold: style.color("gray-darkest") !default;
$link--interact__color--gold: style.color("black") !default;
$input__background-color--gold: style.color("white") !default;
$input--focus__background-color--gold: style.color("white") !default;
$input__border-color--gold: style.darken("gold", 20.5%) !default;
$input--hover__border-color--gold: style.darken("gold", 30.5%) !default;
$input--focus__border-color--gold: style.color("gray-darkest") !default;
$input__color--gold: style.color("gray-darkest") !default;
$submit__border-color--gold: style.color("white-darkest") !default;
$submit--hover__border-color--gold: style.color("gray-lightest") !default;
$submit--focus__border-color--gold: style.color("gray-darkest") !default;
$label__color--gold: style.color("gray-lightest") !default;
// Header dark colors
$background-color--dark: style.color("gray-dark") !default;
$link__color--dark: style.color("white") !default;
$link--interact__color--dark: style.color("white-darkest") !default;
$input__background-color--dark: style.color("gray") !default;
$input--focus__background-color--dark: style.color("gray-light") !default;
$input__border-color--dark: style.color("gray-darkest") !default;
$input--hover__border-color--dark: style.color("black") !default;
$input--focus__border-color--dark: style.color("black") !default;
$input__color--dark: style.color("white") !default;
$submit__border-color--dark: style.color("gray-darkest") !default;
$submit--hover__border-color--dark: style.color("black") !default;
$submit--focus__border-color--dark: style.color("black") !default;
$label__color--dark: style.color("white-darkest") !default;
// Header themes
$themes: (
"white": (
"background-color": $background-color--white,
"link-color": $link__color--white,
"link-hover-focus-color": $link--interact__color--white,
"input-background-color": $input__background-color--white,
"input-focus-background-color": $input--focus__background-color--white,
"input-border-color": $input__border-color--white,
"input-hover-border-color": $input--hover__border-color--white,
"input-focus-border-color": $input--focus__border-color--white,
"input-color": $input__color--white,
"submit-border-color": $submit__border-color--white,
"submit-hover-border-color": $submit--hover__border-color--white,
"submit-focus-border-color": $submit--focus__border-color--white,
"label-color": $label__color--white
),
"gray": (
"background-color": $background-color--gray,
"link-color": $link__color--gray,
"link-hover-focus-color": $link--interact__color--gray,
"input-background-color": $input__background-color--gray,
"input-focus-background-color": $input--focus__background-color--gray,
"input-border-color": $input__border-color--gray,
"input-hover-border-color": $input--hover__border-color--gray,
"input-focus-border-color": $input--focus__border-color--gray,
"input-color": $input__color--gray,
"submit-border-color": $submit__border-color--gray,
"submit-hover-border-color": $submit--hover__border-color--gray,
"submit-focus-border-color": $submit--focus__border-color--gray,
"label-color": $label__color--gray
),
"gold": (
"background-color": $background-color--gold,
"link-color": $link__color--gold,
"link-hover-focus-color": $link--interact__color--gold,
"input-background-color": $input__background-color--gold,
"input-focus-background-color": $input--focus__background-color--gold,
"input-border-color": $input__border-color--gold,
"input-hover-border-color": $input--hover__border-color--gold,
"input-focus-border-color": $input--focus__border-color--gold,
"input-color": $input__color--gold,
"submit-border-color": $submit__border-color--gold,
"submit-hover-border-color": $submit--hover__border-color--gold,
"submit-focus-border-color": $submit--focus__border-color--gold,
"label-color": $label__color--gold
),
"dark": (
"background-color": $background-color--dark,
"link-color": $link__color--dark,
"link-hover-focus-color": $link--interact__color--dark,
"input-background-color": $input__background-color--dark,
"input-focus-background-color": $input--focus__background-color--dark,
"input-border-color": $input__border-color--dark,
"input-hover-border-color": $input--hover__border-color--dark,
"input-focus-border-color": $input--focus__border-color--dark,
"input-color": $input__color--dark,
"submit-border-color": $submit__border-color--dark,
"submit-hover-border-color": $submit--hover__border-color--dark,
"submit-focus-border-color": $submit--focus__border-color--dark,
"label-color": $label__color--dark
)
);
$theme--keys: "background-color", "link-color", "link-hover-focus-color",
"input-background-color", "input-focus-background-color",
"input-border-color", "input-hover-border-color", "input-focus-border-color",
"input-color", "submit-border-color", "submit-hover-border-color",
"submit-focus-border-color", "label-color", "theme";
@function theme--validate($instructions) {
@return map.keys($instructions) == $theme--keys;
}
@mixin theme($instructions) {
@if theme--validate($instructions) {
$theme: map.get($instructions, "theme");
$background-color: map.get($instructions, "background-color");
$link-color: map.get($instructions, "link-color");
$link-hover-focus-color: map.get(
$instructions,
"link-hover-focus-color"
);
$input-background-color: map.get(
$instructions,
"input-background-color"
);
$input-focus-background-color: map.get(
$instructions,
"input-focus-background-color"
);
$input-border-color: map.get($instructions, "input-border-color");
$input-hover-border-color: map.get(
$instructions,
"input-hover-border-color"
);
$input-focus-border-color: map.get(
$instructions,
"input-focus-border-color"
);
$input-color: map.get($instructions, "input-color");
$submit-border-color: map.get($instructions, "submit-border-color");
$submit-hover-border-color: map.get(
$instructions,
"submit-hover-border-color"
);
$submit-focus-border-color: map.get(
$instructions,
"submit-focus-border-color"
);
$label-color: map.get($instructions, "label-color");
.#{$prefix}-header--#{$theme} {
--cwf-header--background-color: #{$background-color};
--cwf-header__link--color: #{$link-color};
--cwf-header__link--hover-focus--color: #{$link-hover-focus-color};
--cwf-header__input--background-color: #{$input-background-color};
--cwf-header__input--focus--background-color: #{$input-focus-background-color};
--cwf-header__input--border-color: #{$input-border-color};
--cwf-header__input--hover--border-color: #{$input-hover-border-color};
--cwf-header__input--focus--border-color: #{$input-focus-border-color};
--cwf-header__input--color: #{$input-color};
--cwf-header__submit--border-color: #{$submit-border-color};
--cwf-header__submit--hover--border-color: #{$submit-hover-border-color};
--cwf-header__submit--focus--border-color: #{$submit-focus-border-color};
--cwf-header__label--color: #{$label-color};
}
} @else {
@warn "Invalid header theme instructions provided!";
}
}
@mixin theme--official($theme) {
$colors: map.get($themes, $theme);
$instructions: map.merge(
$colors,
(
"theme": $theme
)
);
@include theme($instructions);
}
@each $theme, $colors in $themes {
@include theme--official($theme);
}
.#{$prefix}-header--compact {
min-height: 4rem;
padding-top: 1rem;
padding-bottom: 1rem;
}
.#{$prefix}-header__container {
display: flex;
align-items: center;
justify-content: center;
padding-left: 1rem;
padding-right: 1rem;
@include theme.contain;
@include media.breakpoint {
justify-content: space-between;
}
}
.#{$prefix}-header__title {
display: flex;
flex-direction: column;
align-items: center;
@include media.breakpoint {
align-items: flex-start;
}
}
.#{$prefix}-header__title--reverse {
flex-direction: column-reverse;
}
.#{$prefix}-header__department {
font-size: 1.1667rem;
font-weight: 500;
@include media.breakpoint {
font-size: 2rem;
}
}
.#{$prefix}-header--compact .#{$prefix}-header__department {
@include media.breakpoint {
font-size: 1.1667rem;
}
}
.#{$prefix}-header__department,
.#{$prefix}-header__parent {
margin-top: 0;
margin-bottom: 0;
padding-top: 0;
}
.#{$prefix}-header__parent {
margin-bottom: 0.5rem;
font-size: 0.889rem;
font-weight: 400;
}
.#{$prefix}-header__link {
font-weight: 400;
text-decoration: none;
color: var(--cwf-header__link--color);
@include animation.transition(color);
&:hover,
&:focus {
text-decoration: underline;
color: var(--cwf-header__link--hover-focus--color);
}
}
.#{$prefix}-header__link--multi-line {
display: flex;
flex-direction: column;
}
.#{$prefix}-header__controls {
position: absolute;
top: 0;
right: 0.25rem;
display: flex;
@include style.z-index("page");
@include media.breakpoint {
display: none;
}
}
// Header colors
$toggle__color: style.darken("gray-dark", 0.75%) !default; // #313131
$toggle--active__background-color: style.opacity("black", 5%) !default;
$toggle--active__color: style.color("black") !default;
.#{$prefix}-header__toggle {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
width: 48px;
height: 64px;
padding: 0.5rem 0;
border: none;
background-color: transparent;
font-size: 0.65rem;
font-weight: 700;
color: var(--cwf-header__toggle--color);
@include animation.transition(background-color, color);
--cwf-header__toggle--color: #{$toggle__color};
--cwf-header__toggle--active--background-color: #{$toggle--active__background-color};
--cwf-header__toggle--active--color: #{$toggle--active__color};
&:hover,
&:focus {
background-color: var(--cwf-header__toggle--active--background-color);
color: var(--cwf-header__toggle--active--color);
}
& .#{$prefix}-header__icon {
font-size: 1.25rem;
}
}
.#{$prefix}-header__features {
display: none;
@include media.breakpoint {
display: block;
}
}
.#{$prefix}-header__exit {
display: none;
}
.#{$prefix}-header__features--nav-modal {
align-items: flex-end;
@include media.breakpoint {
align-items: center;
}
}
$features--modal__background-color: style.opacity("black", 75%) !default;
$features--modal__background-color--reduced-transparency: style.color(
"black"
) !default;
.#{$prefix}-header__features--nav-modal,
.#{$prefix}-header__features--search-modal {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
padding-left: 1rem;
padding-right: 1rem;
background-color: var(--cwf-header__features--modal--background-color);
@include style.z-index("modal");
--cwf-header__features--modal--background-color: #{$features--modal__background-color};
@include media.reduced(transparency) {
--cwf-header__features--modal--background-color: #{$features--modal__background-color--reduced-transparency};
}
@include media.reduced(transparency, no-preference) {
--cwf-header__features--modal--background-color: #{$features--modal__background-color};
}
&[aria-hidden="false"] {
@include animation.animation--fadeIn;
}
&[aria-hidden="true"] {
@include animation.animation--fadeOut;
}
$exit__background-color: style.color("black") !default;
$exit__background-color--active: style.color("black") !default;
$exit__background-color--desktop: style.opacity("black", 50%) !default;
$exit__color: style.color("white") !default;
.#{$prefix}-header__exit {
position: absolute;
top: 0;
right: 0;
display: flex;
align-items: center;
justify-content: space-evenly;
min-width: 128px;
height: 64px;
padding: 0;
border: none;
background-color: var(--cwf-header__exit--background-color);
font-size: 1rem;
font-weight: 700;
color: var(--cwf-header__exit--color);
@include animation.transition(background-color);
--cwf-header__exit--background-color: #{$exit__background-color};
--cwf-header__exit--color: #{$exit__color};
--cwf-header__exit--active--background-color: #{$exit__background-color--active};
--cwf-header__exit--desktop--background-color: #{$exit__background-color--desktop};
&:hover,
&:focus {
background-color: var(--cwf-header__exit--active--background-color);
}
&:after {
@include style.icon("\f00d"); // X-mark
font-size: 1.5rem;
}
@include media.breakpoint {
background-color: var(
--cwf-header__exit--desktop--background-color
);
}
}
}
.#{$prefix}-header__features--search-modal {
align-items: flex-start;
@include media.breakpoint {
align-items: center;
}
}
.#{$prefix}-header__nav {
display: flex;
justify-content: flex-end;
font-size: 0.889rem;
}
.#{$prefix}-header__features--search-modal .#{$prefix}-header__nav {
display: none;
}
.#{$prefix}-header__features--nav-modal .#{$prefix}-header__nav {
display: flex;
flex-direction: column;
width: 760px;
max-width: 760px;
margin-bottom: 1rem;
@include media.breakpoint {
margin-bottom: 0;
}
}
.#{$prefix}-header__features--nav-modal[aria-hidden="false"]
.#{$prefix}-header__nav {
@include animation.animation--slideInUp;
}
.#{$prefix}-header__features--nav-modal[aria-hidden="true"]
.#{$prefix}-header__nav {
@include animation.animation--slideOutDown;
}
.#{$prefix}-header__nav .#{$prefix}-header__link {
margin-right: 0.5rem;
text-decoration: underline;
&:hover,
&:focus {
text-decoration: none !important;
}
&:last-child {
margin-right: 0;
}
}
$features--nav-modal__link__border-color: style.color("white-darkest") !default;
$features--nav-modal__link__background-color: style.color("white") !default;
$features--nav-modal__link__color: style.color("gray-darkest") !default;
$features--nav-modal__link__border-color--active: style.opacity(
"black",
12.5%
) !default;
$features--nav-modal__link__background-color--active: theme.accent--background(
) !default;
$features--nav-modal__link__color--active: theme.accent--foreground() !default;
.#{$prefix}-header__features--nav-modal .#{$prefix}-header__link {
display: block;
width: 100%;
max-width: 760px;
margin-right: 0;
padding: 1rem 1.5rem;
border-bottom: 2px solid
var(--cwf-header__features--nav-modal__link--border-color);
background-color: var(
--cwf-header__features--nav-modal__link--background-color
);
font-size: 1.5rem;
text-decoration: none;
color: var(--cwf-header__features--nav-modal__link--color);
transition: none;
--cwf-header__features--nav-modal__link--border-color: #{$features--nav-modal__link__border-color};
--cwf-header__features--nav-modal__link--background-color: #{$features--nav-modal__link__background-color};
--cwf-header__features--nav-modal__link--color: #{$features--nav-modal__link__color};
--cwf-header__features--nav-modal__link--active--border-color: #{$features--nav-modal__link__border-color--active};
--cwf-header__features--nav-modal__link--active--background-color: #{$features--nav-modal__link__background-color--active};
--cwf-header__features--nav-modal__link--active--color: #{$features--nav-modal__link__color--active};
&:hover,
&:focus {
border-color: var(
--cwf-header__features--nav-modal__link--active--border-color
);
background-color: var(
--cwf-header__features--nav-modal__link--active--background-color
);
color: var(--cwf-header__features--nav-modal__link--active--color);
}
&:focus {
outline: none;
}
&:first-of-type {
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
}
&:last-of-type {
border-bottom: none;
border-bottom-right-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
}
}
.#{$prefix}-header__nav ~ .#{$prefix}-header__search {
@include media.breakpoint {
margin-top: 0.5rem;
}
}
.#{$prefix}-header__search {
position: relative;
display: flex;
flex: 1;
}
.#{$prefix}-header__features--nav-modal .#{$prefix}-header__search {
display: none;
}
.#{$prefix}-header__features--search-modal .#{$prefix}-header__search {
display: flex;
flex-direction: column;
width: 100%;
min-width: 0;
max-width: 760px;
margin-top: calc(64px + 1rem);
@include media.breakpoint {
margin-top: 0;
}
}
.#{$prefix}-header__features--search-modal[aria-hidden="false"]
.#{$prefix}-header__search {
@include animation.animation--slideInUp;
}
.#{$prefix}-header__features--search-modal[aria-hidden="true"]
.#{$prefix}-header__search {
@include animation.animation--slideOutDown;
}
.#{$prefix}-header__input {
flex: 1;
font-size: 0.889rem;
padding: 0.5rem 2.25rem 0.5rem 0.75rem;
border-width: 1px;
border-style: solid;
border-radius: 1.5rem;
border-color: var(--cwf-header__input--border-color);
background-color: var(--cwf-header__input--background-color);
color: var(--cwf-header__input--color);
@include animation.transition(border-color, background-color);
&:hover {
border-color: var(--cwf-header__input--hover--border-color);
}
&:focus {
background-color: var(--cwf-header__input--focus--background-color);
}
}
.#{$prefix}-header__input:focus,
.#{$prefix}-header__input:focus:hover {
outline: none;
border-color: var(--cwf-header__input--focus--border-color);
}
$features--search-modal__input__border-color: style.color("white") !default;
$features--search-modal__input__background-color: style.color("white") !default;
$features--search-modal__input__color: style.color("gray-darkest") !default;
$features--search-modal__input__border-color--active: style.color(
"gray-darkest"
) !default;
.#{$prefix}-header__features--search-modal .#{$prefix}-header__input {
font-size: 1.5rem;
padding: 1rem 4rem 1rem 1.5rem;
border-width: 2px;
border-color: var(
--cwf-header__features--search-modal__input--border-color
);
border-radius: 3rem;
background-color: var(
--cwf-header__features--search-modal__input--background-color
);
color: var(--cwf-header__features--search-modal__input--color);
transition: none;
--cwf-header__features--search-modal__input--border-color: #{$features--search-modal__input__border-color};
--cwf-header__features--search-modal__input--background-color: #{$features--search-modal__input__background-color};
--cwf-header__features--search-modal__input--color: #{$features--search-modal__input__color};
--cwf-header__features--search-modal__input--active--border-color: #{$features--search-modal__input__border-color--active};
&:focus,
&:focus:hover {
border-color: var(
--cwf-header__features--search-modal__input--active--border-color
);
}
}
.#{$prefix}-header__submit {
position: absolute;
right: 0.4rem;
top: 50%;
display: flex;
justify-content: center;
align-items: center;
width: 1.5rem;
height: 1.5rem;
border: 1px solid var(--cwf-header__submit--border-color);
border-radius: 100%;
background-color: var(--cwf-header__input--background-color);
font-size: 0.6rem;
color: var(--cwf-header__label--color);
transform: translateY(-50%);
@include animation.transition(border-color, background-color, color);
&:hover {
border-color: var(--cwf-header__submit--hover--border-color);
}
&:focus {
outline: none;
border-color: var(--cwf-header__submit--focus--border-color);
background-color: var(--cwf-header__input--focus--background-color);
color: var(--cwf-header__input--color);
}
}
$features--search-modal__submit__border-color: style.color(
"white-darkest"
) !default;
$features--search-modal__submit__background-color: style.color(
"white"
) !default;
$features--search-modal__submit__color: style.color("gray-lightest") !default;
$features--search-modal__submit__border-color--hover: style.color(
"gray-lightest"
) !default;
$features--search-modal__submit__border-color--focus: style.color(
"gray-darkest"
) !default;
$features--search-modal__submit__color--focus: style.color(
"gray-darkest"
) !default;
.#{$prefix}-header__features--search-modal .#{$prefix}-header__submit {
right: 0.5rem;
width: 3rem;
height: 3rem;
border-width: 2px;
border-color: var(
--cwf-header__features--search-modal__submit--border-color
);
background-color: var(
--cwf-header__features--search-modal__submit--background-color
);
font-size: 1rem;
color: var(--cwf-header__features--search-modal__submit--color);
transition: none;
--cwf-header__features--search-modal__submit--border-color: #{$features--search-modal__submit__border-color};
--cwf-header__features--search-modal__submit--background-color: #{$features--search-modal__submit__background-color};
--cwf-header__features--search-modal__submit--color: #{$features--search-modal__submit__color};
--cwf-header__features--search-modal__submit--hover--border-color: #{$features--search-modal__submit__border-color--hover};
--cwf-header__features--search-modal__submit--focus--border-color: #{$features--search-modal__submit__border-color--focus};
--cwf-header__features--search-modal__submit--focus--color: #{$features--search-modal__submit__color--focus};
&:hover {
border-color: var(
--cwf-header__features--search-modal__submit--hover--border-color
);
}
&:focus {
border-color: var(
--cwf-header__features--search-modal__submit--focus--border-color
);
color: var(--cwf-header__features--search-modal__submit--focus--color);
}
}
.#{$prefix}-header__label {
position: absolute;
left: 0.75rem;
font-size: 0.889rem;
color: var(--cwf-header__label--color);
transform: translateY(-50%);
@include animation.animation--slideFadeIn(forwards);
&:hover {
cursor: text;
}
}
.#{$prefix}-header__input:valid ~ .#{$prefix}-header__label {
@include animation.animation--slideFadeOut(forwards);
}
$features--search-modal__label__color: style.color("gray-lightest") !default;
.#{$prefix}-header__features--search-modal .#{$prefix}-header__label {
left: 1.5rem;
font-size: 1.5rem;
color: var(--cwf-header__features--search-modal__label--color);
--cwf-header__features--search-modal__label--color: #{$features--search-modal__label__color};
}
// The default component class
import { Component } from '../../shared/component.js';
// Lock/unlock document scrolling
import { toggleScrollLock } from '../../shared/event.js';
// Traps the focus to a specifc set of elements
import { trap as trapFocus } from '../../shared/focus.js';
// Provide functionality to the header
export class Header extends Component {
constructor({
prefix = 'cwf',
header = 'header',
features = 'header__features',
featuresNavModal = 'header__features--nav-modal',
featuresSearchModal = 'header__features--search-modal',
toggle = 'header__toggle',
nav = 'header__nav',
search = 'header__search',
link = 'header__link',
input = 'header__input',
submit = 'header__submit',
exit = 'header__exit'
} = {}) {
super({
prefix,
classes: {
header,
features,
featuresNavModal,
featuresSearchModal,
toggle,
nav,
search,
link,
input,
submit,
exit
},
references: {}
});
// Initialize the feature and modal state objects
this.feature = {};
this.modal = {};
// Bind "this" to all methods that need it
this.blurFeaturesModal = this.blurFeaturesModal.bind(this);
this.modalOnClick = this.modalOnClick.bind(this);
this.modalOnKeyDown = this.modalOnKeyDown.bind(this);
this.closeFeaturesModal = this.closeFeaturesModal.bind(this);
this.focusFeature = this.focusFeature.bind(this);
this.navLinksOnKeyDown = this.navLinksOnKeyDown.bind(this);
this.navLinksOnFocusBlur = this.navLinksOnFocusBlur.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
// Register the toggle scroll lock...
this.toggleScrollLock = toggleScrollLock.bind(this);
// ... and trap focus functions to this component
this.trapFocus = trapFocus.bind(this);
}
get modalListener() {
return super.stateListener(this.modal.open);
}
// Stop showing the features as a modal
blurFeaturesModal() {
// Remove the event listener that triggered this function,...
this.references.features.wrapper[this.modalListener](
'animationend',
this.blurFeaturesModal,
true
);
// ... remove the modal classes from the feature wrapper,...
this.references.features.wrapper.className = this.classes.features;
// ... remove the aria-hidden attribute from the feature wrapper,...
this.references.features.wrapper.removeAttribute('aria-hidden');
// ... toggle the scroll lock,...
this.toggleScrollLock();
// ... and reinitialize the global modal object
this.modal = {};
}
// Close the features modal
closeFeaturesModal() {
// Set the modal to be closed...
this.modal.open = false;
// Set the features to be hidden...
this.references.features.wrapper.setAttribute('aria-hidden', true);
// ... and stop showing the features as a modal when its animation ends
this.references.features.wrapper.addEventListener(
'animationend',
this.blurFeaturesModal,
true
);
// Unfocus the search input
document.activeElement.blur();
// Remove the event listeners for the exit functionality
this.references.features.wrapper[this.modalListener](
'click',
this.modalOnClick,
true
);
this.references.features.exit[this.modalListener](
'click',
this.modalOnClick,
true
);
document[this.modalListener]('keydown', this.modalOnKeyDown, true);
}
// Bind to the features' wrapper and exit button's click event when in modal view
modalOnClick({ target, currentTarget }) {
// Check to see if the target is the features wrapper...
const targetIsFeatures = target === this.references.features.wrapper,
// ... and the current target is the exit button
currentTargetIsExit =
currentTarget === this.references.features.exit;
// If the target is the features or the current target is the exit button, close the features modal
if (targetIsFeatures || currentTargetIsExit)
return this.closeFeaturesModal();
}
// Bind to the document's key-down event when in modal view
modalOnKeyDown(event) {
// Grab the key pressed from the event
const { key } = event;
// If "Escape" is pressed, close the features modal
if (key === 'Escape') return this.closeFeaturesModal();
// If "Tab" is pressed, trap the focus
if (key === 'Tab') return this.trapFocus(this.feature.focusable, event);
}
// Open the features wrapper as a modal
openFeaturesAsModal() {
// Set the modal to be opened
this.modal.open = true;
// Prevent the body from scrolling
this.toggleScrollLock();
// Show the features wrapper as a modal...
this.references.features.wrapper.classList.add(this.modal.class);
// ... and set it to be visible in order to animate
this.references.features.wrapper.setAttribute('aria-hidden', false);
// Bind to the features' wrapper and exit button's click event...
this.references.features.wrapper[this.modalListener](
'click',
this.modalOnClick,
true
);
this.references.features.exit[this.modalListener](
'click',
this.modalOnClick,
true
);
// ... and the document's key-down event
document[this.modalListener]('keydown', this.modalOnKeyDown, true);
}
// Focus a feature's first element of a given selector
focusFeatureElement(feature) {
// Attempt to grab the feature element from the given selector...
const element = feature.querySelector(this.feature.focus);
// ... and if doesn't exist, do nothing else
if (!element) return;
// Otherwise, focus the element
element.focus();
}
// Checks to see if the feature is within the viewport
featureIsWithinViewport() {
// If the feature element has not been set, do nothing,...
if (!this.feature.element) return;
// ... otherwise, check to see if the feature is within the viewport...
const scroll = window.scrollY || window.pageYOffset,
boundsTop =
this.feature.element.getBoundingClientRect().top + scroll,
viewport = {
top: scroll,
bottom: scroll + window.innerHeight
},
bounds = {
top: boundsTop,
bottom: boundsTop + this.feature.element.clientHeight
};
// ... and return the findings
return (
(bounds.bottom >= viewport.top &&
bounds.bottom <= viewport.bottom) ||
(bounds.top <= viewport.bottom && bounds.top >= viewport.top)
);
}
// Checks to see if the feature is visible (e.g. not display: none;)
featureIsVisible() {
// If the feature element has not been set, do nothing,...
if (!this.feature.element) return;
// ... otherwise, return whether the feature is visible (e.g. not display: none;)
return this.feature.element.offsetParent !== null;
}
// Select an argument based off the feature's type
selectFromFeatureType(navOption, searchOption) {
// If the feature element has not been set, do nothing
if (!this.feature.element) return;
// If the feature is a nav, return the nav argument
if (
this.references.features.nav &&
this.feature.element === this.references.features.nav.element
) {
return typeof navOption === 'function' ? navOption() : navOption;
}
// If the feature is a search, return the search argument
if (
this.references.features.search &&
this.feature.element === this.references.features.search.element
) {
return typeof searchOption === 'function'
? searchOption()
: searchOption;
}
}
// Return all focusable nav elements
returnFocusableNavElements() {
return [
this.references.features.exit, // Exit button
...this.references.features.nav.links // Nav links
];
}
// Return all focusable search elements
returnFocusableSearchElements() {
return [
this.references.features.exit, // Exit button
this.references.features.search.input, // Search input
this.references.features.search.submit // Search submit button
];
}
// Focus a header feature
focusFeature({ currentTarget }) {
// Attempt to grab the header feature...
const id = currentTarget.getAttribute('aria-controls');
this.feature.element = document.getElementById(id);
// ... and if it doesn't exist, do nothing else
if (!this.feature.element) return;
// Figure out what element to focus and modal class to use based off its type
this.feature.focus = this.selectFromFeatureType(
this.selectors.link,
this.selectors.input
);
// If the modal is already open, close it
if (this.modal.open) return this.closeFeaturesModal();
// If the feature is not within the viewport or is not visible, open the features as a modal
if (!this.featureIsVisible() || !this.featureIsWithinViewport()) {
// Find what elements are focusable within the feature...
this.feature.focusable = this.selectFromFeatureType(
this.returnFocusableNavElements.bind(this),
this.returnFocusableSearchElements.bind(this)
);
// ... and modal class to be used,...
this.modal.class = this.selectFromFeatureType(
this.classes.featuresNavModal,
this.classes.featuresSearchModal
);
// ... and open the features wrapper as a modal
this.openFeaturesAsModal();
}
// Focus the feature's first element of the given selector
this.focusFeatureElement(this.feature.element);
}
// Bind to every toggle's click event
togglesOnClick() {
// If no toggles exist, do nothing else
if (!this.references.toggles.length) return;
// For each toggle,...
this.references.toggles.forEach((toggle) => {
// ... open the features as a modal when clicked
toggle[this.listener]('click', this.focusFeature);
});
}
// Bind to every nav link's key-down event if focused
navLinksOnKeyDown(event) {
// Grab the type, current target, and key from the event
const { type, currentTarget, key } = event;
// If the type is focus/blur...
if (['focus', 'blur'].includes(type)) {
// ... add/remove key-down functionality respectively
const listener =
type === 'focus' ? 'addEventListener' : 'removeEventListener';
return currentTarget[listener]('keydown', this.navLinksOnKeyDown);
}
// Always assign Home/End keys for navigation,...
const navigation = ['Home', 'End'];
// ... figure out what previous/next keys make sense based on if a modal is open,...
const previous = this.modal.open ? 'ArrowUp' : 'ArrowLeft',
next = this.modal.open ? 'ArrowDown' : 'ArrowRight';
// ... and push them to the navigation array
navigation.push(previous, next);
// If the current key pressed is not within the navigation, do nothing else
if (!navigation.includes(key)) return;
// Otherwise, prevent the event's default behavior...
event.preventDefault();
// ... and handle the navigation
switch (key) {
case 'Home':
// Home = Focus the first nav link
return this.references.features.nav.links[0].focus();
case 'End':
// End = Focus the last nav link
return this.references.features.nav.links[
this.references.features.nav.links.length - 1
].focus();
default:
// Arrow keys = trap focus within the nav links
return this.trapFocus(
this.references.features.nav.links,
key === previous
);
}
}
// Bind to every nav link's focus/blur event
navLinksOnFocusBlur() {
// If a nav...
if (!this.references.features.nav) return;
// ... or nav links exist, do nothing else
if (!this.references.features.nav.links) return;
// For each nav link...
this.references.features.nav.links.forEach((link) => {
// ... bind to its key-down even when focused
link[this.listener]('focus', this.navLinksOnKeyDown);
link[this.listener]('blur', this.navLinksOnKeyDown);
});
}
// Bind to the document's key presses
onKeyDown(event) {
// Check to see if the user is typing within a text area or input...
const focused = document.activeElement,
inputting = ['TEXTAREA', 'INPUT'].includes(focused.tagName);
// ... and if so, do nothing else
if (inputting) return;
// Grab the key pressed from the event
const { key } = event;
// Attempt to find a toggle with a key shorcut that matches the one pressed...
const toggle = this.references.toggles.find((toggle) => {
return toggle.getAttribute('aria-keyshortcuts') === key;
});
// ... and if doesn't exist, do nothing else
if (!toggle) return;
// Prevent the default event behavior...
event.preventDefault();
// ... and virtually click the toggle
toggle.click();
}
// Bind/unbind to the document's key-down event
documentOnKeyDown() {
document[this.listener]('keydown', this.onKeyDown);
}
// Get the nav
getNav(features) {
// Attempt to grab the nav...
const nav = features.querySelector(this.selectors.nav);
// ... and if none exists, do nothing else
if (!nav) return;
// Otherwise, store it and its relevant children
this.references.features.nav = {};
this.references.features.nav.element = nav;
this.references.features.nav.links = Array.from(
nav.querySelectorAll(this.selectors.link)
);
}
// Get the search
getSearch(features) {
// Attempt to grab the search...
const search = features.querySelector(this.selectors.search);
// ... and if none exists, do nothing else
if (!search) return;
// Otherwise, store it and its relevant children
this.references.features.search = {};
this.references.features.search.element = search;
this.references.features.search.input = search.querySelector(
this.selectors.input
);
this.references.features.search.submit = search.querySelector(
this.selectors.submit
);
}
// Mount/unmount the header functionality
mount(run) {
super.mount(run);
// Attempt to find the header...
const header =
this.references.header ||
document.querySelector(this.selectors.header);
// ... and if it doesn't exist, do nothing,...
if (!header) return;
// ... otherwise, save it as a reference
this.references.header = header;
// Attempt to find the header features...
this.references.features = this.references.features || {};
const features =
this.references.features.wrapper ||
header.querySelector(this.selectors.features);
// ... and if doesn't exist, do nothing,...
if (!features) return;
// ... otherwise, save it as a reference
this.references.features.wrapper = features;
// Grab all toggles...
this.references.toggles =
this.references.toggles ||
Array.from(header.querySelectorAll(this.selectors.toggle));
// ... and the feature's exit button
this.references.features.exit =
this.references.features.exit ||
features.querySelector(this.selectors.exit);
// If mounting,...
if (run) {
// ... attempt to grab the nav...
this.getNav(features);
// ... and search features
this.getSearch(features);
}
// Bind to every toggle's click event,...
this.togglesOnClick();
// ... every nav link's focus/blur events (if mounting),...
if (run) this.navLinksOnFocusBlur();
// ... and the document's key-down event
this.documentOnKeyDown();
if (!run) this.references = {};
}
}
The header component should be used at the top of every page to show the title of the website and provide a link to the homepage. The header also includes a few optional elements such a link to a unit’s parent unit, a search form and place for quick links.
The header component has the ability to have a white, gray, gold or dark background. By default the background will be white.
The header component also has the option to display in compact mode, which will reduce the font size of the unit name and the padding size of the wrapping block (while at a md breakpoint or larger). When using the header in compact mode it is suggested to not include the optional parent unit information. Consider using compact mode for sites or applications with internal audiences.
The header is implemented in T4 as the “Header” Compass content type, meaning its classes retain the .cwf-
prefix.
The number of quick links allowed in the content type is limited to 5; Any links beyond the limit will be ignored.
This content type should only be used within the global “Site-Header” section to have it displayed globally within the header area.
In the “Injectors” field of the header content type, the following injectors can be used:
id:{custom_id}
- Overrides the default, T4 ID of the header with a custom ID.class:{custom_classes}
- Adds custom classes to the header.style:{custom_styles}
- Adds custom styles to a style
attribute of the header.before:{custom_html}
- Adds custom HTML before the header.after:{custom_html}
- Adds custom HTML after the header.