Appearance
フロントエンド開発ガイドライン
概要
このドキュメントは、プロジェクトにおけるフロントエンド開発のガイドラインを定義します。
ターゲットデバイス
スマートフォン(SP)を主なターゲットとする
このサイトは基本的にスマートフォン(SP)で表示されるサイトです。
- モバイルファーストのアプローチを採用
- タッチ操作を前提としたUI/UX設計
- スマートフォンでの表示を最優先に考慮
デスクトップ・タブレット対応
- スマートフォンでの表示を最優先としつつ、デスクトップ・タブレットでも適切に表示されることを目指します
- レスポンシブデザインを採用し、各デバイスで適切な表示を実現します
アクセシビリティ
キーボード操作について
キーボード操作についてはあまり考える必要がありません。
このサイトはスマートフォンでのタッチ操作を前提としているため、キーボードナビゲーションの実装は必須ではありません
tabindex="-1"を使用してフォーカスを無効化することも許可されますoutline: noneの使用は原則禁止とします。やむを得ず使用する場合は、必ずbox-shadowやborderなどによる視認可能なカスタムフォーカススタイルを同時に実装してください。例:
css.custom-button:focus { outline: none; box-shadow: 0 0 0 2px var(--existing-primary-blue); }
スクリーンリーダー対応
- セマンティックHTMLの使用(
<th scope="row">など) - 適切な見出し構造
- ARIA属性の使用(必要に応じて)
視覚的なアクセシビリティ
- 適切なコントラスト比の確保
- タッチターゲットのサイズを適切に設定(最小44x44px推奨)
レスポンシブデザイン
画面幅の段階設定
このプロジェクトでは、以下のブレークポイントが定義されています(core_src/View/templates/Common/common-base-styles.css参照):
css
:root {
/* レスポンシブブレークポイント */
--breakpoint-tablet: 768px;
--breakpoint-mobile: 480px;
--breakpoint-small-mobile: 360px;
--breakpoint-xs-mobile: 320px;
}ブレークポイントの段階:
| ブレークポイント | 幅 | 用途 |
|---|---|---|
| デスクトップ | 769px以上 | デスクトップ表示 |
| タブレット | 481px - 768px | タブレット表示 |
| モバイル | 361px - 480px | スマートフォン表示 |
| 小さなモバイル | 321px - 360px | 小さなスマートフォン表示 |
| 極小モバイル | 301px - 320px | 極小スマートフォン表示 |
| 最小幅 | 300px以下 | 横スクロール対応 |
最小幅の設定
300px以上の幅で正常に表示されるものにします。
- コンポーネントは300px以上の画面幅で正常に表示されるように設計します
- 300px以上では、レスポンシブデザインにより適切に表示されます
300px以下の画面幅対応
300px以下となる画面ではmin-widthを指定して幅を維持し、はみ出た分は横スクロールで表示します。
css
/* 300px以下の画面幅対応 */
@media (max-width: 300px) {
.component {
min-width: 300px; /* 300px未満では横スクロールを許可 */
}
}注意事項:
- コンポーネント毎に横スクロールの設定をする必要は基本的にはありません
- ページ全体または親コンテナで
min-width: 300pxを設定することで、300px以下の画面幅でも横スクロールで表示されます - 個別のコンポーネントで横スクロールが必要な場合は、そのコンポーネントのみに設定します
メディアクエリの使用例
css
/* タブレット以下(768px以下) */
@media (max-width: 768px) {
.component {
/* タブレット用スタイル */
}
}
/* モバイル(480px以下) */
@media (max-width: 480px) {
.component {
/* モバイル用スタイル */
}
}
/* 小さなモバイル(360px以下) */
@media (max-width: 360px) {
.component {
/* 小さなモバイル用スタイル */
}
}
/* 極小モバイル(320px以下) */
@media (max-width: 320px) {
.component {
/* 極小モバイル用スタイル */
}
}
/* 300px以下の画面幅対応(横スクロール) */
@media (max-width: 300px) {
.component {
min-width: 300px; /* 300px未満では横スクロールを許可 */
}
}注意: CSS変数はメディアクエリの条件部(@mediaの括弧内)では使用できません(例:@media (max-width: var(--breakpoint-tablet)) は不可)。そのため、各コンポーネントで個別にメディアクエリ条件を記述する必要があります。
タッチ操作の考慮
- タッチターゲットのサイズを適切に設定(最小44x44px推奨)
- スワイプ操作などのジェスチャーを考慮
- タッチフィードバック(
:active状態など)を提供
TypeScript/JavaScript コーディング規約
キャストの利用を極力避ける
原則: 型キャスト(as演算子)の使用は極力避け、型ガード関数や型定義ファイルを使用して型安全性を確保します。
typescript
// ✅ 正しい(型ガード関数を使用)
function isHallEnum(value: string): value is HallEnum {
return Object.values(HallEnum).includes(value as HallEnum);
}
const halls = hallsString
.split(',')
.map((hall) => hall.trim())
.filter((hall) => hall !== '')
.filter(isHallEnum); // 型ガードにより、キャスト不要
// ❌ 誤り(キャストを直接使用)
const halls = hallsString
.split(',')
.map((hall) => hall.trim())
.filter((hall) => hall !== '')
.filter((hall): hall is HallEnum => {
return Object.values(HallEnum).includes(hall as HallEnum); // キャストを使用
});型ガード関数の作成:
typescript
// ✅ 正しい(型ガード関数を作成)
export function isHTMLElement(element: Element | null): element is HTMLElement {
if (!element) {
return false;
}
return 'style' in element && 'display' in (element as HTMLElement).style;
}
// 使用例
const metaBox = container.closest('.postbox');
if (isHTMLElement(metaBox)) {
metaBox.style.display = ''; // 型ガードにより、キャスト不要
}windowオブジェクトの拡張:
typescript
// ✅ 正しい(型定義ファイルで拡張)
// types/window.d.ts
declare global {
interface Window {
dailyArticleSelectedHalls?: HallEnum[];
}
}
// 使用例
window.dailyArticleSelectedHalls = initialHalls; // 型定義により、キャスト不要
// ❌ 誤り(キャストを使用)
(window as unknown as { dailyArticleSelectedHalls?: HallEnum[] }).dailyArticleSelectedHalls = initialHalls;カスタムイベントの型ガード:
typescript
// ✅ 正しい(型ガード関数を作成)
export function isDailyArticleSelectedHallsChangedEvent(
event: Event
): event is CustomEvent<{ selectedHalls: HallEnum[] }> {
if (!('detail' in event)) {
return false;
}
const customEvent = event as CustomEvent;
if (!customEvent.detail || typeof customEvent.detail !== 'object') {
return false;
}
if (!('selectedHalls' in customEvent.detail)) {
return false;
}
const selectedHalls = (customEvent.detail as { selectedHalls: unknown }).selectedHalls;
if (!Array.isArray(selectedHalls)) {
return false;
}
return selectedHalls.every((hall): hall is HallEnum => isHallEnum(hall));
}
// 使用例
const handleHallsChange = (event: Event): void => {
if (isDailyArticleSelectedHallsChangedEvent(event)) {
// 型ガードにより、event.detail.selectedHallsが確実にHallEnum[]型になる
this.updateHallVisibility();
}
};
// ❌ 誤り(キャストを直接使用)
const handleHallsChange = (event: Event): void => {
const customEvent = event as CustomEvent<{ selectedHalls: HallEnum[] }>;
if (customEvent.detail?.selectedHalls) {
this.updateHallVisibility();
}
};キャストが必要な場合のコメント:
キャスト先の型になることが明確である場合は、コメントで補完してください。
typescript
// ✅ 正しい(コメントで補完)
// closest()の戻り値はElement | nullだが、WordPressのメタボックスは確実にHTMLElementである
const metaBox = container.closest('.postbox') as HTMLElement;
// ❌ 誤り(コメントなし)
const metaBox = container.closest('.postbox') as HTMLElement;理由:
- 型ガード関数を使用することで、型安全性が向上し、実行時エラーを防げる
- 型定義ファイルを使用することで、windowオブジェクトの拡張を型安全に行える
- キャストを避けることで、コードの可読性と保守性が向上する
- 型ガード関数は再利用可能で、テストしやすい
適用範囲:
- 型アサーション(
as演算子)の使用 - windowオブジェクトの拡張
- カスタムイベントの型チェック
- DOM要素の型チェック
例外:
- 型ガード関数を作成するコストが高すぎる場合(例: 非常に複雑な型チェック)
- 既存のライブラリの型定義が不十分で、やむを得ずキャストが必要な場合
- キャスト先の型になることが明確で、コメントで補完できる場合
- 型ガード関数内でのキャストが実装上不可避な場合(例:
isHallEnumのような列挙型判定の内部実装、includesメソッドの引数型チェックなど)
パフォーマンス
モバイル環境での最適化
- 画像の遅延読み込み
- CSS/JSの最小化
- キャッシュ戦略の活用
- モバイルネットワークを考慮したリソースサイズの最適化