ヘッダー
プレビュー
特徴
- レスポンシブデザイン: スマホ・タブレット・PC などの画面サイズに合わせてナビゲーションが変化します。
- アクセシビリティ: スキップリンクや
aria-label
を活用し、スクリーンリーダーでも快適に操作できる設計です。 - ダークモード対応: システムの設定に合わせて自動的にライトモード・ダークモードを切り替えます。
- 直感的なハンバーガーボタン: スマホやタブレットなどの小さな画面でメニューを展開・収束できるハンバーガーアイコンを採用しています。
- 最低限の Props:
logo
とnavItems
を指定するだけで、豊富な UI を簡単に実装できます。
logo
のテキストが長い場合でも、折り返しやはみ出しを防ぐためのスタイルを適用しています。
プロパティ
プロパティ名 | 型 | デフォルト値 | 説明 |
---|---|---|---|
logo | string | "LOGO" | ヘッダー左上に表示される文字列。クリックするとルートに戻ります。 |
navItems | {label: string; href: string;}[] | [ { label: "Home", href: "/" } ] | 表示したいナビゲーションアイテムの配列。 |
Github
コード例
import "@/styles/tailwind.css";import { useState } from "react";
type NavItem = { label: string; href: string;};
type HeaderProps = { logo?: string; navItems?: NavItem[];};
export default function Header({ logo = "LOGO", navItems = [ { label: "Home", href: "#" }, { label: "About", href: "#" }, { label: "Contact", href: "#" }, ],}: HeaderProps) { const [isOpen, setIsOpen] = useState(false);
return ( <header className="w-full bg-white text-black dark:bg-black dark:text-white"> <div className="flex items-center justify-between px-4 py-4 md:px-8"> <h1 className="text-xl font-bold overflow-hidden text-ellipsis whitespace-nowrap"> <a href="/" className="transition-colors duration-200 hover:opacity-75 focus:opacity-75" > {logo} </a> </h1> <button onClick={() => setIsOpen(!isOpen)} aria-label="Toggle Menu" type="button" aria-expanded={isOpen} className="inline-flex items-center justify-center p-2 rounded md:hidden hover:bg-gray/10 dark:hover:bg-white/10" > <div className="relative w-6 h-6"> <span className={`absolute block h-[2px] w-6 bg-current transition-transform duration-300 ease-in-out ${ isOpen ? "top-[13px] rotate-45" : "top-[6px]" }`} /> <span className={`absolute block h-[2px] w-6 bg-current transition-opacity duration-300 ease-in-out ${ isOpen ? "opacity-0" : "top-[13px]" }`} /> <span className={`absolute block h-[2px] w-6 bg-current transition-transform duration-300 ease-in-out ${ isOpen ? "bottom-[6px] -rotate-45" : "bottom-[6px]" }`} /> </div> </button> <nav className="hidden md:flex md:items-center md:space-x-8"> {navItems.map(({ label, href }) => ( <a key={href} href={href} className="block px-2 py-1 transition-opacity duration-200 hover:opacity-75 focus:opacity-75" > {label} </a> ))} </nav> </div> <div className={`md:hidden transition-[max-height] duration-300 overflow-hidden ${ isOpen ? "max-h-96 border-t border-gray/30 dark:border-white/30" : "max-h-0" }`} > <ul className="flex flex-col gap-4 py-4 px-4"> {navItems.map(({ label, href }) => ( <li key={href}> <a href={href} className="block px-2 py-1 transition-opacity duration-200 hover:opacity-75 focus:opacity-75" > {label} </a> </li> ))} </ul> </div> </header> );}
使用方法
コンポーネントをインポートして、ページやレイアウトに組み込みます。logo
や navItems
を必要に応じて上書きしてください。
各フレームワークによってインポート先は調整し、実際には上記のコードを別ファイルで管理すると便利です。
// import "@/components/Header";import Header from "@/components/Header";
export default function Layout({ children }) { return ( <> <Header logo="MyWebsite" navItems={[ { label: "Home", href: "/" }, { label: "Services", href: "/services" }, { label: "Blog", href: "/blog" }, ]} /> <main id="main">{children}</main> </> );}
import { colorPalette } from "@/styles/ColorPalette";import { typography } from "@/styles/Typography";import { LitElement, css, html } from "lit";import { customElement, property, state } from "lit/decorators.js";
type NavItem = { label: string; href: string;};
@customElement("my-header")export class MyHeader extends LitElement { @property({ type: String }) logo = "LOGO"; @property({ type: Array }) navItems: NavItem[] = [ { label: "Home", href: "/" }, { label: "About", href: "/about" }, { label: "Contact", href: "/contact" }, ];
@state() private isOpen = false;
static styles = [ colorPalette, typography, css` :host { display: block; background-color: var(--light-basic-white); color: var(--light-basic-black); font-family: var(--font-family-sans); } @media (prefers-color-scheme: dark) { :host { background-color: var(--dark-basic-black); color: var(--dark-basic-white); } } .header { width: 100%; } .skip-link { position: absolute; left: -9999px; top: auto; width: 1px; height: 1px; overflow: hidden; } .skip-link:focus { position: static; width: auto; height: auto; margin: var(--spacing-2); padding: var(--spacing-2); background-color: var(--basic-green); color: var(--basic-white); } .header-content { display: flex; align-items: center; justify-content: space-between; padding: var(--spacing-4); } .logo { font-size: var(--font-xl); font-weight: var(--font-bold); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin: 0; } .logo-link { text-decoration: none; color: inherit; transition: color 0.2s ease-in-out; } .logo-link:hover, .logo-link:focus { opacity: 0.75; } .hamburger { display: inline-flex; align-items: center; justify-content: center; width: var(--spacing-6); height: var(--spacing-6); border: none; background: transparent; cursor: pointer; border-radius: var(--spacing-1); transition: background-color 0.2s ease-in-out; } .hamburger:hover, .hamburger:focus { background-color: var(--basic-gray) 0.1; } @media (prefers-color-scheme: dark) { .hamburger { color: var(--dark-basic-white); } } .lines { position: relative; width: var(--spacing-6); height: var(--spacing-6); } .line { position: absolute; width: var(--spacing-6); height: 2px; background-color: currentColor; transition: transform 0.3s ease-in-out, top 0.3s ease-in-out, opacity 0.3s ease-in-out; } .line1 { top: 6px; } .line2 { top: 13px; } .line3 { bottom: 6px; } .open .line1 { top: 13px; transform: rotate(45deg); } .open .line2 { opacity: 0; } .open .line3 { bottom: 6px; transform: rotate(-45deg) translateY(-2px); } nav { display: none; } nav ul { list-style: none; margin: 0; padding: 0; } nav a { text-decoration: none; color: inherit; padding: var(--spacing-2) var(--spacing-1); display: block; transition: opacity 0.2s ease-in-out; } nav a:hover, nav a:focus { opacity: 0.75; } @media (min-width: 768px) { .hamburger { display: none; } nav { display: flex; align-items: center; gap: var(--spacing-8); } } .menu-container { max-height: 0; overflow: hidden; transition: max-height 0.3s ease-in-out; border-top: none; background-color: var(--light-basic-white); } .menu-open { max-height: 400px; border-top: 1px solid var(--basic-gray); } @media (prefers-color-scheme: dark) { .menu-container { background-color: var(--dark-basic-black); } .menu-open { border-top: 1px solid var(--dark-basic-white); } } .menu-list { display: flex; flex-direction: column; gap: var(--spacing-4); padding: var(--spacing-4); list-style-type: none; } .menu { color: var(--light-basic-black); } @media (prefers-color-scheme: dark) { .menu { color: var(--dark-basic-white); } } @media (min-width: 768px) { .menu-container { display: none; } } `, ];
private toggleMenu() { this.isOpen = !this.isOpen; }
render() { return html` <header class="header"> <a href="#main" class="skip-link">Skip to main content</a> <div class="header-content"> <h1 class="logo"> <a href="/" class="logo-link">${this.logo}</a> </h1> <button class="hamburger ${this.isOpen ? "open" : ""}" @click="${this.toggleMenu}" aria-label="Toggle Menu" aria-expanded="${this.isOpen}" > <div class="lines"> <span class="line line1"></span> <span class="line line2"></span> <span class="line line3"></span> </div> </button> <nav> ${this.navItems.map( (item) => html` <a href="${item.href}">${item.label}</a> `, )} </nav> </div> <div class="menu-container ${this.isOpen ? "menu-open" : ""}" aria-hidden="${!this.isOpen}" > <ul class="menu-list"> ${this.navItems.map( (item) => html` <li> <a class="menu" href="${item.href}">${item.label}</a> </li> `, )} </ul> </div> </header> `; }}
使用方法
コンポーネントをインポートして、ページやレイアウトに組み込みます。logo
や navItems
を必要に応じて上書きしてください。
各フレームワークによってインポート先は調整し、実際には上記のコードを別ファイルで管理すると便利です。
// import "@/components/MyHeader";export class AppLayout extends HTMLElement { connectedCallback() { this.innerHTML = ` <my-header logo="MyAwesomeSite" .navItems="${JSON.stringify([ { label: 'Home', href: '/' }, { label: 'Services', href: '/services' }, { label: 'Blog', href: '/blog' }, { label: 'Contact', href: '/contact' }, ])}" ></my-header> <main id="main"> <!-- 本文コンテンツ --> </main> `; }}