Example Components
Site Header
A header for every page: a skip link, the site name, and the main navigation. The navigation is plain HTML written at build time, and the link for the current page is marked with aria-current, so the header needs no JavaScript.
Usage
name is the site name and defaults to "Example Site". Anything inside the tag goes at the end of the bar, for example a Theme Toggle. Every page needs a <main id="main"> for the skip link to land on.
true
examples/components/site-header/site-header.html
html
<site-header data-bascik-prop-name="Example Site" /> html examples/components/site-header/site-header.html
<!-- @bascik
A site header: a skip link, the site name, and the main navigation, with the current page marked at build time. The links live in links.ts next to this file. Every page needs <main id="main"> for the skip link.
@prop name - Site name, linking to the home page. Defaults to "Example Site".
@slot default - Controls at the end of the bar, such as a theme toggle. Optional.
-->
<style>
.skip-link {
position: absolute;
top: -10rem;
left: var(--space-3, 1rem);
z-index: 100;
padding: var(--space-2, 0.5rem) var(--space-3, 1rem);
background: var(--color-accent, #2457d6);
color: var(--color-accent-contrast, #ffffff);
}
.skip-link:focus {
top: var(--space-3, 1rem);
}
.header {
border-bottom: 1px solid var(--color-border, currentColor);
}
.bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-3, 1rem);
max-width: var(--content-width, 70rem);
margin-inline: auto;
padding: var(--space-3, 1rem);
}
.brand {
margin-right: auto;
color: inherit;
font-weight: 700;
text-decoration: none;
}
/* The build script below prints this list. Build script output is scoped like the template, so these selectors
match it. Two classes outrank a site-wide rule such as `.content ul`, so the reset always applies. */
.header .nav-list {
display: flex;
flex-wrap: wrap;
gap: var(--space-3, 1rem);
margin: 0;
padding: 0;
list-style: none;
}
.nav-list li {
margin: 0;
}
.nav-list a {
color: inherit;
text-decoration: none;
}
.nav-list a[aria-current] {
color: var(--color-accent, inherit);
text-decoration: underline;
text-underline-offset: 0.3em;
}
</style>
<a class="skip-link" href="#main">Skip to content</a>
<header class="header">
<div class="bar">
<a class="brand" href="/"><span data-bascik-prop-name>Example Site</span></a>
<nav aria-label="Site">
<!-- "page" runs this once per page instead of once for the site, so each page marks its own link. -->
<script data-bascik-build="page">
import { navLinks } from './links.ts';
export default () => navLinks(process.env.BASCIK_PAGE_PATH);
</script>
</nav>
<div data-bascik-slot></div>
</div>
</header> Output is shown unminified (HTML, CSS, JS, and identifier names) for readability.
The links point at the paths in links.ts, which are pages of your site, not of this one, so none is marked in this preview. Press Tab at the top of a page that uses it to see the skip link.
links.ts
The navigation lives in a TypeScript file next to the component. Edit the list for your site.
ts examples/components/site-header/links.ts
// links.ts: the main navigation, in one place. Edit this list for your site. site-header.html imports it from a
// build script, so the links are plain HTML in every page and the current page is marked without JavaScript.
// Paths are relative to the deployment base; Bascik adds the base for you.
export interface NavLink {
label: string;
href: string;
}
export const NAV_LINKS: NavLink[] = [
{ label: 'Home', href: '/' },
{ label: 'About', href: '/about' },
{ label: 'Blog', href: '/blog/' },
{ label: 'Contact', href: '/contact' },
];
const ENTITIES: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
const escapeHtml = (value: string): string => value.replace(/[&<>"']/g, (ch) => ENTITIES[ch]);
/** "/blog/" and "/blog" compare equal; "/" only matches the home page. */
const normalize = (path: string): string => (path.length > 1 ? path.replace(/\/+$/, '') : path);
/** True when `href` is the current page, or a section that contains it ("/blog/" for "/blog/hello/"). */
export function isCurrent(href: string, pagePath: string): boolean {
const target = normalize(href);
const current = normalize(pagePath);
if (target === '/') return current === '/';
return current === target || current.startsWith(`${target}/`);
}
/** The nav list for one page. `pagePath` is the page being built, from BASCIK_PAGE_PATH. */
export function navLinks(pagePath = '/', links: NavLink[] = NAV_LINKS): string {
const items = links.map((link) => {
const exact = normalize(link.href) === normalize(pagePath);
// aria-current="page" for the page itself, "true" for the section it belongs to.
const current = exact ? ' aria-current="page"' : isCurrent(link.href, pagePath) ? ' aria-current="true"' : '';
return `<li><a href="${escapeHtml(link.href)}"${current}>${escapeHtml(link.label)}</a></li>`;
});
return `<ul class="nav-list">${items.join('')}</ul>`;
} How It Works
- The build script inside
<nav>imports./links.ts. Relative imports in a component's script resolve from the component's folder, so the component and its data travel together. - A build script in a component normally runs once, and its output is reused on every page.
data-bascik-build="page"runs it again for each page withBASCIK_PAGE_PATHset to that page's path, so each page marks its own link:aria-current="page"on the page itself, andaria-current="true"on the section it belongs to (Blog on a blog post). See Page-Aware Scripts. - Markup a build script prints is scoped like the component's template, so
.nav-list ain the component's CSS matches the printed links. - The skip link targets
#main. No element in the component has that ID, so Bascik leaves the reference unscoped and it finds the<main>on the page. <div data-bascik-slot></div>is replaced by whatever you put inside the tag, or by nothing.
Adapt It
- To share one list between the header and other places, move
links.tstosrc/lib/and import it as@/lib/links.ts. - On small screens the links wrap. For a collapsed menu, put the list inside a Popover Menu and show it inline above a breakpoint.