Webアプリホーム画面(お知らせ+KPIカード)

応用例 中級

この画面パターンについて

SaaSや社内ツールでログイン直後に表示される「一般ユーザー向けホーム画面」のパターンです。 お知らせの確認・利用状況の把握・よく使う機能への移動という日常動線に最適化したカードレイアウトを実装します。 お知らせの未読管理とヘッダー通知バッジの連動が実装の核心です。

こんな場面で使えます

  • 社内ポータル・SaaSのトップ — ログイン後最初に見る画面を作る
  • 会員マイページ — お知らせと利用状況をまとめて見せる
  • 管理画面のホーム — KPIとショートカットで日常業務の起点にする

この画面で使っているUIコンポーネント

#パーツこの画面での役割
1アプリヘッダーアプリ名・通知ベル・ユーザー名
2通知バッジベルに未読件数を表示
3KPIカード ×3主要数値+前月比(▲緑/▼赤)
4お知らせリスト未読ドット・重要度バッジ・日付
5既読処理クリックで既読化・バッジと連動
6使用量メーター使用状況バー+80%/95%で色変化
7クイックリンクカードアイコン+ラベルのショートカット
8サポート窓口カード問い合わせへの導線

実装のポイント・注意点

核は未読数の扱いです。未読数を独立した変数で持つと、既読化処理との食い違いでバッジと一覧がずれる事故が起きます。 未読数は常に NOTICES 配列の unread フラグから数えて算出し(データが唯一の情報源)、 お知らせの既読化もデータを書き換えてから renderBadge() で関連UIをまとめて更新します。 レイアウトはCSS Gridの2段構え(.kpi-grid は3等分、.home-columns は2:1)で、768px以下は1カラムに畳みます。 使用量メーターは80%で警告色・95%で危険色としきい値を2段階にし、デモでは「+1GB」ボタンで色の変化を体験できるようにしています。

8個のUIコンポーネントをHTML・CSS・バニラJavaScriptのみで組み合わせており、 フレームワーク不要で画面ごとコピペして使えます。

動作サンプル

Webアプリホーム画面(お知らせ+KPIカード)のデモ画面 動作サンプルを別ウィンドウで確認 ↗

試してみる:

  • 未読のお知らせをクリックして、ヘッダーのバッジ件数が減ることを確認
  • 未読3件をすべて既読にして、バッジ自体が消えることを確認
  • 「使用量を増やす」を連打して、メーターが警告色→危険色に変わることを確認

そのほかの操作も自由に試してみてください。

サンプルソース

3つのファイルを同じフォルダに保存し、ブラウザで index.html を開くと動作確認できます。
ファイル名:index.html / style.css / script.js
保存時の文字コードは UTF-8 を指定してください。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Webアプリホーム画面 サンプル</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>

<div class="home-screen">
  <!-- ===== アプリヘッダー ===== -->
  <header class="app-header">
    <span class="app-logo">Sample App</span>
    <div class="app-header-right">
      <button type="button" class="bell-btn" aria-label="通知">
        🔔<span class="bell-badge" id="bellBadge">3</span>
      </button>
      <span class="user-name">サンプル 太郎</span>
    </div>
  </header>

  <main class="home-main">
    <h1 class="welcome-text">こんにちは、サンプル 太郎 さん</h1>

    <!-- ===== KPIカード ===== -->
    <section class="kpi-grid" id="kpiGrid" aria-label="利用状況サマリー">
      <!-- JSで生成:.kpi-card > .kpi-label / .kpi-value / .kpi-diff -->
    </section>

    <!-- ===== お知らせ + 使用量メーター ===== -->
    <div class="home-columns">
      <section class="card notice-card" aria-label="お知らせ">
        <h2 class="card-title">お知らせ</h2>
        <ul class="notice-list" id="noticeList">
          <!-- JSで生成:li.notice-item(.is-unread)-->
        </ul>
      </section>

      <section class="card usage-card" aria-label="ストレージ使用量">
        <h2 class="card-title">ストレージ使用量</h2>
        <div class="usage-meter">
          <div class="usage-bar"><div class="usage-fill" id="usageFill"></div></div>
          <p class="usage-text"><span id="usageUsed">6.5</span> / 10 GB</p>
        </div>
        <div class="demo-controls">
          <button type="button" class="mini-btn" id="addUsageBtn">使用量を増やす(+1GB)</button>
          <button type="button" class="mini-btn" id="resetUsageBtn">リセット</button>
        </div>
      </section>
    </div>

    <!-- ===== クイックリンク + サポート ===== -->
    <div class="home-columns">
      <section class="card quicklink-card" aria-label="クイックリンク">
        <h2 class="card-title">クイックリンク</h2>
        <div class="quicklink-grid">
          <a href="#" class="quicklink"><span class="ql-icon">📄</span>レポート作成</a>
          <a href="#" class="quicklink"><span class="ql-icon">📊</span>データ一覧</a>
          <a href="#" class="quicklink"><span class="ql-icon">⚙️</span>設定</a>
          <a href="#" class="quicklink"><span class="ql-icon">👥</span>メンバー管理</a>
        </div>
      </section>

      <section class="card support-card" aria-label="サポート">
        <h2 class="card-title">お困りですか?</h2>
        <p class="support-text">操作方法やトラブルはサポート窓口までお問い合わせください。</p>
        <a href="#" class="btn-primary support-btn">お問い合わせ</a>
      </section>
    </div>
  </main>
</div>

<script src="./script.js"></script>
</body>
</html>
/* ===== Webアプリホーム画面(お知らせ+KPIカード) — style.css ===== */
*, *::before, *::after { box-sizing: border-box; }

:root {
  --color-primary: #2B7FE8;
  --color-up:      #16A34A; /* 前月比プラス(緑) */
  --color-down:    #DC2626; /* 前月比マイナス(赤) */
  --color-warn:    #F59E0B; /* 使用量80%超(黄) */
  --color-danger:  #DC2626; /* 使用量95%超(赤) */
  --color-text:    #1E293B;
  --color-muted:   #64748B;
  --color-border:  #E2E8F0;
  --color-bg:      #F4F6F9;
  --color-card:    #FFFFFF;
  --radius-card:   10px;
}

body {
  margin: 0;
  font-family: "Hiragino Kaku Gothic ProN", "Hiragino Sans", Meiryo, sans-serif;
  background: var(--color-bg);
  color: var(--color-text);
}

/* ===== アプリヘッダー ===== */
.app-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 24px;
  background: var(--color-card);
  border-bottom: 1px solid var(--color-border);
}

.app-logo {
  font-size: 18px;
  font-weight: 700;
  color: var(--color-primary);
}

.app-header-right {
  display: flex;
  align-items: center;
  gap: 16px;
}

/* ベルアイコン(バッジを右上に絶対配置するため relative) */
.bell-btn {
  position: relative;
  padding: 4px;
  font-size: 20px;
  line-height: 1;
  background: transparent;
  border: none;
  cursor: pointer;
}

.bell-badge {
  position: absolute;
  top: -2px;
  right: -4px;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  font-size: 11px;
  font-weight: 700;
  line-height: 18px;
  color: #fff;
  text-align: center;
  background: var(--color-danger);
  border-radius: 9999px;
}

.bell-badge[hidden] { display: none; }

.user-name {
  font-size: 14px;
  font-weight: 600;
}

/* ===== 本文レイアウト ===== */
.home-main {
  max-width: 1040px;
  margin: 0 auto;
  padding: 24px 20px 48px;
}

.welcome-text {
  font-size: 20px;
  margin: 0 0 20px;
}

/* ===== KPIカード ===== */
/* 3等分グリッド。768px以下で1カラムに畳む */
.kpi-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  margin-bottom: 16px;
}

.kpi-card {
  background: var(--color-card);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  padding: 18px 20px;
}

.kpi-label {
  margin: 0;
  font-size: 13px;
  color: var(--color-muted);
}

.kpi-value {
  margin: 8px 0 4px;
  font-size: 28px;
  font-weight: 700;
}

.kpi-diff {
  font-size: 13px;
  font-weight: 700;
}

.kpi-diff.up   { color: var(--color-up); }
.kpi-diff.down { color: var(--color-down); }

/* ===== 2カラムレイアウト(本文 2 : サイド 1) ===== */
/* 768px以下で1カラムに畳む */
.home-columns {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 16px;
  margin-bottom: 16px;
}

/* ===== カード共通 ===== */
.card {
  background: var(--color-card);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  padding: 18px 20px;
}

.card-title {
  margin: 0 0 12px;
  font-size: 15px;
  font-weight: 700;
}

/* ===== お知らせリスト ===== */
.notice-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.notice-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 8px 12px 18px;
  border-bottom: 1px solid #EEF2F6;
  cursor: pointer;
  transition: background 0.15s;
}

.notice-item:last-child { border-bottom: none; }
.notice-item:hover { background: #F8FAFC; }

/* 未読:太字+左端に青ドット */
.notice-item.is-unread .notice-title { font-weight: 700; }

.notice-item.is-unread::before {
  content: "";
  position: absolute;
  left: 4px;
  top: 50%;
  width: 8px;
  height: 8px;
  margin-top: -4px;
  background: var(--color-primary);
  border-radius: 50%;
}

/* 重要度バッジ */
.notice-badge {
  flex-shrink: 0;
  padding: 2px 8px;
  font-size: 11px;
  font-weight: 700;
  border-radius: 4px;
  white-space: nowrap;
}

.notice-badge.badge-important { color: #fff; background: var(--color-danger); }
.notice-badge.badge-update    { color: #fff; background: var(--color-primary); }
.notice-badge.badge-info      { color: var(--color-muted); background: #EEF2F6; }

.notice-title {
  flex: 1;
  min-width: 0;
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.notice-date {
  flex-shrink: 0;
  font-size: 12px;
  color: var(--color-muted);
}

/* ===== 使用量メーター ===== */
.usage-meter { margin-bottom: 14px; }

.usage-bar {
  width: 100%;
  height: 10px;
  background: #EEF2F6;
  border-radius: 9999px;
  overflow: hidden;
}

/* 幅は%でJSが指定。.warn / .danger で色変化 */
.usage-fill {
  height: 100%;
  width: 65%;
  background: var(--color-primary);
  border-radius: 9999px;
  transition: width 0.3s, background 0.3s;
}

.usage-fill.warn   { background: var(--color-warn); }
.usage-fill.danger { background: var(--color-danger); }

.usage-text {
  margin: 8px 0 0;
  font-size: 13px;
  color: var(--color-muted);
}

/* デモ操作ボタン */
.demo-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.mini-btn {
  padding: 7px 12px;
  font-size: 12px;
  font-family: inherit;
  color: var(--color-text);
  background: var(--color-card);
  border: 1px solid var(--color-border);
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.15s;
}

.mini-btn:hover { background: var(--color-bg); }

/* ===== クイックリンク ===== */
.quicklink-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 12px;
}

.quicklink {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 16px 8px;
  font-size: 13px;
  color: var(--color-text);
  text-align: center;
  text-decoration: none;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: 8px;
  transition: transform 0.15s, box-shadow 0.15s;
}

/* ホバーで浮き上がり */
.quicklink:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
}

.ql-icon { font-size: 24px; }

/* ===== サポートカード ===== */
.support-text {
  margin: 0 0 14px;
  font-size: 13px;
  line-height: 1.7;
  color: var(--color-muted);
}

.btn-primary {
  display: inline-block;
  padding: 10px 22px;
  font-size: 14px;
  font-family: inherit;
  color: #fff;
  text-decoration: none;
  background: var(--color-primary);
  border: 1.5px solid var(--color-primary);
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.15s;
}

.btn-primary:hover { background: #1D6AD0; }

/* ===== レスポンシブ(768px以下) ===== */
@media (max-width: 768px) {
  .kpi-grid { grid-template-columns: 1fr; }
  .home-columns { grid-template-columns: 1fr; }
  .quicklink-grid { grid-template-columns: repeat(2, 1fr); }
}
/* =====================================================
   Webアプリホーム画面(お知らせ+KPIカード)のスクリプト

   仕組み:お知らせ・KPI・使用量はすべて定数配列/オブジェクトが
   唯一の情報源。未読数は NOTICES の unread フラグから都度数えるので
   バッジと一覧がズレない。お知らせをクリックしたら unread を false に
   書き換えてから renderNotices() と renderBadge() で関連UIをまとめて
   描き直す(DOMのクラス操作だけで未読を管理しない)。
   使用量は update() 1関数で「幅%・数値・色クラス」をまとめて反映する。
   ===================================================== */

// ===== 設定値 =====
var USAGE_WARN_PERCENT   = 80;  // この割合を超えるとバーが警告色(黄)
var USAGE_DANGER_PERCENT = 95;  // この割合を超えるとバーが危険色(赤)
var USAGE_STEP_GB        = 1;   // 「使用量を増やす」1クリックあたりの増分
var USAGE_INITIAL_GB     = 6.5; // 使用量の初期値・リセット値

// 重要度コード → 表示ラベルとバッジ用クラス
var LEVEL_LABELS = {
  important: '重要',
  update:    '更新',
  info:      'お知らせ'
};

// ===== サンプルデータ(汎用的な内容) =====
var NOTICES = [
  { id: 1, level: 'important', title: 'システムメンテナンスのお知らせ', date: '2026-06-08', unread: true },
  { id: 2, level: 'update',    title: '新機能をリリースしました',       date: '2026-06-05', unread: true },
  { id: 3, level: 'update',    title: '一部機能の改善について',         date: '2026-06-03', unread: true },
  { id: 4, level: 'info',      title: '利用規約改定のお知らせ',         date: '2026-06-01', unread: false },
  { id: 5, level: 'info',      title: 'ヘルプページを更新しました',     date: '2026-05-28', unread: false }
];

var KPIS = [
  { label: '今月の利用回数', value: '128回', diff: +12 },
  { label: '登録データ件数', value: '542件', diff: +3 },
  { label: '完了タスク',     value: '36件',  diff: -8 }
];

var USAGE = { used: USAGE_INITIAL_GB, limit: 10, unit: 'GB' };

// ===== DOM要素 =====
var bellBadge    = document.getElementById('bellBadge');
var kpiGrid      = document.getElementById('kpiGrid');
var noticeList   = document.getElementById('noticeList');
var usageFill    = document.getElementById('usageFill');
var usageUsed    = document.getElementById('usageUsed');
var addUsageBtn  = document.getElementById('addUsageBtn');
var resetUsageBtn = document.getElementById('resetUsageBtn');

// ===== KPIカードの描画 =====
// diff の正負で up / down クラスと ▲ / ▼ を出し分ける
function renderKPIs() {
  kpiGrid.textContent = '';
  KPIS.forEach(function (kpi) {
    var card = document.createElement('div');
    card.className = 'kpi-card';

    var label = document.createElement('p');
    label.className = 'kpi-label';
    label.textContent = kpi.label;

    var value = document.createElement('p');
    value.className = 'kpi-value';
    value.textContent = kpi.value;

    var diff = document.createElement('span');
    var isUp = kpi.diff >= 0;
    diff.className = 'kpi-diff ' + (isUp ? 'up' : 'down');
    diff.textContent = (isUp ? '▲' : '▼') + Math.abs(kpi.diff) + '%';

    card.appendChild(label);
    card.appendChild(value);
    card.appendChild(diff);
    kpiGrid.appendChild(card);
  });
}

// ===== お知らせの描画 =====
// 未読は .is-unread を付与(CSSで青ドット+太字)。日付は M/D 形式に整形
function renderNotices() {
  noticeList.textContent = '';
  NOTICES.forEach(function (notice) {
    var item = document.createElement('li');
    item.className = 'notice-item' + (notice.unread ? ' is-unread' : '');
    item.dataset.id = notice.id;

    var badge = document.createElement('span');
    badge.className = 'notice-badge badge-' + notice.level;
    badge.textContent = LEVEL_LABELS[notice.level];

    var title = document.createElement('span');
    title.className = 'notice-title';
    title.textContent = notice.title;

    var date = document.createElement('span');
    date.className = 'notice-date';
    date.textContent = formatDate(notice.date);

    item.appendChild(badge);
    item.appendChild(title);
    item.appendChild(date);
    noticeList.appendChild(item);
  });
}

// 'YYYY-MM-DD' → 'M/D'(先頭ゼロを落とす)
function formatDate(iso) {
  var parts = iso.split('-');
  return Number(parts[1]) + '/' + Number(parts[2]);
}

// ===== 通知バッジの描画 =====
// 未読数は NOTICES から都度数える(データが唯一の情報源)。0件なら隠す
function renderBadge() {
  var unreadCount = NOTICES.filter(function (n) { return n.unread; }).length;
  bellBadge.textContent = unreadCount;
  bellBadge.hidden = (unreadCount === 0);
}

// お知らせクリック → 対象を既読化してリストとバッジを再描画
// (既読の再クリックでは何も起きない。未読化はしない)
noticeList.addEventListener('click', function (e) {
  var item = e.target.closest('.notice-item');
  if (!item) { return; }

  var target = NOTICES.find(function (n) { return n.id === Number(item.dataset.id); });
  if (!target || !target.unread) { return; }

  target.unread = false;
  renderNotices();
  renderBadge();
});

// ===== 使用量メーター =====
// 幅%・数値・色クラスを1関数でまとめて反映する。
// しきい値を超えたら警告色→危険色へ段階的に切り替える
function updateUsage() {
  var percent = (USAGE.used / USAGE.limit) * 100;

  usageFill.style.width = percent + '%';
  usageUsed.textContent = USAGE.used;

  usageFill.classList.remove('warn', 'danger');
  if (percent > USAGE_DANGER_PERCENT) {
    usageFill.classList.add('danger');
  } else if (percent > USAGE_WARN_PERCENT) {
    usageFill.classList.add('warn');
  }
}

// 「使用量を増やす」→ 上限まで +1GB ずつ増やす
addUsageBtn.addEventListener('click', function () {
  USAGE.used = Math.min(USAGE.used + USAGE_STEP_GB, USAGE.limit);
  updateUsage();
});

// 「リセット」→ 初期値に戻す
resetUsageBtn.addEventListener('click', function () {
  USAGE.used = USAGE_INITIAL_GB;
  updateUsage();
});

// ===== 初期化 =====
renderKPIs();
renderNotices();
renderBadge();
updateUsage();

AI用プロンプト

以下のプロンプトをコピーしてAIに渡すと、同様の画面パターンを生成できます。

ChatGPTやClaudeにこのプロンプトを渡すと、同様の画面をゼロから生成・カスタマイズできます。お知らせ項目の追加やKPIの変更など、要件を追記して使うのがおすすめです。

※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。

💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。

# Webアプリホーム画面 作成依頼

## 概要
ログイン後に表示される一般ユーザー向けのホーム画面(ポータルトップ)を作成してください。
お知らせ・KPIカード・使用量メーター・クイックリンクをカードレイアウトで配置します。

## 要件
- ヘッダー:アプリ名・通知ベル(未読件数の赤バッジ付き)・ユーザー名
- KPIカード3枚:ラベル・数値・前月比(プラスは緑の▲、マイナスは赤の▼)
- お知らせリスト5件:未読は青ドット+太字。重要度バッジ(重要=赤・更新=青・お知らせ=グレー)と日付を表示
- 未読のお知らせをクリックすると既読になり、ヘッダーのバッジ件数が連動して減る。全件既読でバッジ非表示
- ストレージ使用量メーター:プログレスバー+「6.5 / 10 GB」表示。80%超で黄色、95%超で赤に変化
- 動作確認用に「使用量を増やす(+1GB)」ボタンと「リセット」ボタンをメーターの下に付ける
- クイックリンク4枚(アイコン+ラベル)。ホバーでカードが浮き上がる
- サポート窓口カード(説明文+お問い合わせボタン)

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- データ(お知らせ・KPI・使用量)はJavaScript内の定数で保持する
- レスポンシブ対応:必要(768px以下でカードを1カラムに再配置)

## 動作詳細
- 未読件数はお知らせデータから都度算出してバッジに反映する(DOM上のクラスだけで管理しない)
- DOM生成は createElement と textContent を使い、innerHTML に変数を結合しない
- 全体は薄グレー背景+白カード+角丸+薄い影のカードUIで統一する

## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。