POS開発 UIテンプレート集

コピペで使える HTML / CSS / JavaScript テンプレート

各テンプレートは HTML・CSS・JavaScript の3ブロックで構成されています。 使いたいテンプレートの各ブロックをコピーし、プロジェクトの該当ファイルに貼り付けてください。 クラス名やIDは必要に応じて変更してください。

1. タブ切り替え

使い方: タブボタンとパネルを data-tab 属性で紐づけます。 タブの数は自由に増減可能。初期表示タブには active クラスを付与してください。 POS画面では「商品カテゴリの切り替え」や「注文/履歴の切り替え」に最適です。

HTML

<!-- タブ切り替え -->
<div class="tabs">
  <div class="tab-nav">
    <button class="tab-btn active" data-tab="tab1">タブ1</button>
    <button class="tab-btn" data-tab="tab2">タブ2</button>
    <button class="tab-btn" data-tab="tab3">タブ3</button>
  </div>
  <div class="tab-panel active" id="tab1">タブ1の内容</div>
  <div class="tab-panel" id="tab2">タブ2の内容</div>
  <div class="tab-panel" id="tab3">タブ3の内容</div>
</div>

CSS

/* タブ切り替え */
.tab-nav {
  display: flex;
  gap: 0;
  border-bottom: 2px solid #ddd;
}
.tab-btn {
  padding: 10px 20px;
  border: none;
  background: transparent;
  cursor: pointer;
  font-size: 14px;
  color: #666;
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
  transition: color 0.2s, border-color 0.2s;
}
.tab-btn:hover { color: #333; }
.tab-btn.active {
  color: #e94560;
  border-bottom-color: #e94560;
  font-weight: bold;
}
.tab-panel {
  display: none;
  padding: 16px 0;
}
.tab-panel.active { display: block; }

JavaScript

// タブ切り替え
document.querySelectorAll('.tab-btn').forEach(btn => {
  btn.addEventListener('click', () => {
    // 全タブのactiveを解除
    document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
    document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
    // クリックしたタブをactive
    btn.classList.add('active');
    document.getElementById(btn.dataset.tab).classList.add('active');
  });
});

3. アコーディオン

使い方: 各アイテムのヘッダーをクリックで開閉します。 デフォルトで開いておきたい項目には active クラスを付与してください。 POS画面では「FAQ」「商品カテゴリ詳細」「設定項目の折りたたみ」に使えます。 一つずつ開く排他モードにするには JS の該当行のコメントを外してください。

HTML

<!-- アコーディオン -->
<div class="accordion">
  <div class="accordion-item active">
    <button class="accordion-header">
      セクション1
      <span class="accordion-icon"></span>
    </button>
    <div class="accordion-body">
      <p>セクション1の内容がここに入ります。</p>
    </div>
  </div>
  <div class="accordion-item">
    <button class="accordion-header">
      セクション2
      <span class="accordion-icon"></span>
    </button>
    <div class="accordion-body">
      <p>セクション2の内容がここに入ります。</p>
    </div>
  </div>
  <div class="accordion-item">
    <button class="accordion-header">
      セクション3
      <span class="accordion-icon"></span>
    </button>
    <div class="accordion-body">
      <p>セクション3の内容がここに入ります。</p>
    </div>
  </div>
</div>

CSS

/* アコーディオン */
.accordion {
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
}
.accordion-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  padding: 14px 16px;
  background: #f9f9f9;
  border: none;
  border-bottom: 1px solid #ddd;
  cursor: pointer;
  font-size: 14px;
  text-align: left;
}
.accordion-icon {
  font-size: 10px;
  transition: transform 0.3s;
}
.accordion-item.active .accordion-icon {
  transform: rotate(180deg);
}
.accordion-body {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s ease;
  padding: 0 16px;
}
.accordion-item.active .accordion-body {
  max-height: 500px;
  padding: 14px 16px;
}

JavaScript

// アコーディオン
document.querySelectorAll('.accordion-header').forEach(header => {
  header.addEventListener('click', () => {
    const item = header.parentElement;

    // 排他モード(一つだけ開く)にする場合は下の2行を有効にする
    // const siblings = item.parentElement.querySelectorAll('.accordion-item');
    // siblings.forEach(s => { if (s !== item) s.classList.remove('active'); });

    item.classList.toggle('active');
  });
});

4. トースト通知

使い方: showToast('メッセージ', 'success') で呼び出します。 タイプは success / error / warning / info の4種類。 第3引数で表示時間(ms)を変更可能(デフォルト3000ms)。 POS画面では「商品追加完了」「エラー通知」「在庫警告」に使えます。

HTML

<!-- トーストコンテナ — body直下に1つ配置 -->
<div id="toast-container"></div>

<!-- 使用例 -->
<button onclick="showToast('商品を追加しました', 'success')">成功</button>
<button onclick="showToast('エラーが発生しました', 'error')">エラー</button>
<button onclick="showToast('在庫が残りわずかです', 'warning')">警告</button>
<button onclick="showToast('処理中です...', 'info')">情報</button>

CSS

/* トースト通知 */
#toast-container {
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 9999;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.toast {
  padding: 12px 20px;
  border-radius: 6px;
  color: #fff;
  font-size: 14px;
  min-width: 250px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.2);
  animation: toast-in 0.3s ease;
}
.toast.removing { animation: toast-out 0.3s ease forwards; }
.toast--success { background: #2ecc71; }
.toast--error   { background: #e74c3c; }
.toast--warning { background: #f39c12; }
.toast--info    { background: #3498db; }

@keyframes toast-in {
  from { opacity: 0; transform: translateX(100%); }
  to   { opacity: 1; transform: translateX(0); }
}
@keyframes toast-out {
  from { opacity: 1; transform: translateX(0); }
  to   { opacity: 0; transform: translateX(100%); }
}

JavaScript

// トースト通知
function showToast(message, type = 'info', duration = 3000) {
  const container = document.getElementById('toast-container');
  const toast = document.createElement('div');
  toast.className = `toast toast--${type}`;
  toast.textContent = message;
  container.appendChild(toast);

  setTimeout(() => {
    toast.classList.add('removing');
    toast.addEventListener('animationend', () => toast.remove());
  }, duration);
}

6. ツールチップ

使い方: 要素に data-tooltip 属性を追加するだけで動作します(CSS only)。 JavaScriptは不要ですが、動的に内容を変えたい場合はJSで data-tooltip 属性を書き換えてください。 POS画面では「ボタンの説明」「アイコンのラベル」「入力フィールドのヒント」に使えます。

HTML

<!-- ツールチップ — data-tooltip属性を追加するだけ -->
<button data-tooltip="商品を追加します">追加</button>
<button data-tooltip="この操作は取り消せません" data-tooltip-pos="bottom">削除</button>
<span data-tooltip="税込価格">¥1,080</span>

CSS

/* ツールチップ(CSS only) */
[data-tooltip] {
  position: relative;
  cursor: help;
}
[data-tooltip]::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  background: #333;
  color: #fff;
  padding: 6px 10px;
  border-radius: 4px;
  font-size: 12px;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s;
  margin-bottom: 6px;
}
[data-tooltip]:hover::after {
  opacity: 1;
}
/* 下方向に表示する場合 */
[data-tooltip-pos="bottom"]::after {
  bottom: auto;
  top: 100%;
  margin-bottom: 0;
  margin-top: 6px;
}

JavaScript

// ツールチップはCSS onlyで動作します
// 動的にツールチップ内容を変えたい場合:
function setTooltip(element, text) {
  element.setAttribute('data-tooltip', text);
}

// 使用例:
// const btn = document.querySelector('#myBtn');
// setTooltip(btn, '新しいツールチップテキスト');

7. ローディングスピナー

使い方: スピナーは円形のCSSアニメーション、スケルトンはコンテンツのプレースホルダーです。 API通信やデータ読み込み時に showLoading() で表示、完了後に hideLoading() で非表示にします。 POS画面では「商品データ読み込み」「会計処理中」「レシート印刷中」に使えます。

HTML

<!-- スピナー(全画面オーバーレイ型) -->
<div id="loading-overlay" class="loading-overlay">
  <div class="spinner"></div>
  <p>読み込み中...</p>
</div>

<!-- スケルトンスクリーン -->
<div class="skeleton-card">
  <div class="skeleton skeleton-img"></div>
  <div class="skeleton skeleton-line"></div>
  <div class="skeleton skeleton-line short"></div>
</div>

CSS

/* ローディングオーバーレイ */
.loading-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(255,255,255,0.8);
  z-index: 9999;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
}
.loading-overlay.show { display: flex; }

/* スピナー */
.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #ddd;
  border-top-color: #e94560;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}

/* スケルトンスクリーン */
.skeleton-card { padding: 16px; }
.skeleton {
  background: #e0e0e0;
  border-radius: 4px;
  animation: shimmer 1.5s infinite;
}
.skeleton-img {
  width: 100%;
  height: 120px;
  margin-bottom: 12px;
}
.skeleton-line {
  height: 14px;
  margin-bottom: 8px;
}
.skeleton-line.short { width: 60%; }
@keyframes shimmer {
  0%   { opacity: 1; }
  50%  { opacity: 0.4; }
  100% { opacity: 1; }
}

JavaScript

// ローディング表示/非表示
function showLoading() {
  document.getElementById('loading-overlay').classList.add('show');
}
function hideLoading() {
  document.getElementById('loading-overlay').classList.remove('show');
}

// 使用例: API通信時
// showLoading();
// const res = await fetch('/api/products');
// const data = await res.json();
// hideLoading();

8. 商品カードグリッド

使い方: 商品データをグリッド表示するテンプレートです。 各カードに data-iddata-price を設定し、「カートに追加」ボタンで商品情報を取得します。 グリッドのカラム数は minmax() の値で調整可能。画像は実際のパスに差し替えてください。

HTML

<!-- 商品カードグリッド -->
<div class="product-grid">

  <div class="product-card" data-id="001" data-price="350">
    <div class="product-img">画像</div>
    <div class="product-info">
      <p class="product-name">コーヒー(S)</p>
      <p class="product-price">&yen;350</p>
    </div>
    <button class="add-to-cart-btn">カートに追加</button>
  </div>

  <div class="product-card" data-id="002" data-price="500">
    <div class="product-img">画像</div>
    <div class="product-info">
      <p class="product-name">サンドイッチ</p>
      <p class="product-price">&yen;500</p>
    </div>
    <button class="add-to-cart-btn">カートに追加</button>
  </div>

  <!-- 商品カードを増やす場合はここにコピー -->
</div>

CSS

/* 商品カードグリッド */
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 16px;
}
.product-card {
  background: #fff;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  overflow: hidden;
  transition: box-shadow 0.2s;
}
.product-card:hover {
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.product-img {
  height: 100px;
  background: #f5f5f5;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #aaa;
}
.product-info { padding: 12px; }
.product-name {
  font-weight: bold;
  font-size: 14px;
  margin-bottom: 4px;
}
.product-price {
  color: #e94560;
  font-weight: bold;
}
.add-to-cart-btn {
  width: 100%;
  padding: 10px;
  background: #e94560;
  color: #fff;
  border: none;
  cursor: pointer;
  font-size: 13px;
  transition: background 0.2s;
}
.add-to-cart-btn:hover { background: #c73a52; }

JavaScript

// 商品カード — カートに追加(イベント委譲)
document.querySelector('.product-grid').addEventListener('click', (e) => {
  const btn = e.target.closest('.add-to-cart-btn');
  if (!btn) return;

  const card = btn.closest('.product-card');
  const item = {
    id: card.dataset.id,
    name: card.querySelector('.product-name').textContent,
    price: Number(card.dataset.price),
    quantity: 1
  };

  console.log('カートに追加:', item);
  // ここでカート配列に追加する処理を書く
  // 例: cart.push(item); renderCart();
});

9. ショッピングカート

使い方: カート内の商品を一覧表示し、数量変更・削除・合計計算を行うテンプレートです。 cart 配列にデータを追加して renderCart() を呼ぶと画面が更新されます。 税率は TAX_RATE 定数で変更可能。商品カードグリッドと組み合わせて使ってください。

HTML

<!-- ショッピングカート -->
<div class="cart">
  <h3>カート</h3>
  <table class="cart-table">
    <thead>
      <tr>
        <th>商品</th><th>単価</th><th>数量</th><th>小計</th><th></th>
      </tr>
    </thead>
    <tbody id="cart-body">
      <!-- JSで動的に描画 -->
    </tbody>
  </table>
  <div class="cart-summary">
    <p>小計: <span id="cart-subtotal">&yen;0</span></p>
    <p>消費税: <span id="cart-tax">&yen;0</span></p>
    <p class="cart-total">合計: <span id="cart-total">&yen;0</span></p>
  </div>
</div>

CSS

/* ショッピングカート */
.cart-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
}
.cart-table th {
  background: #f5f5f5;
  padding: 10px;
  text-align: left;
  font-size: 13px;
  color: #666;
}
.cart-table td {
  padding: 10px;
  border-bottom: 1px solid #eee;
}
.qty-control {
  display: flex;
  align-items: center;
  gap: 6px;
}
.qty-btn {
  width: 28px;
  height: 28px;
  border: 1px solid #ddd;
  border-radius: 50%;
  background: #fff;
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
}
.qty-btn:hover { background: #f0f0f0; }
.remove-btn {
  background: none;
  border: none;
  color: #e74c3c;
  cursor: pointer;
  font-size: 18px;
}
.cart-summary {
  text-align: right;
  padding: 16px 10px;
  font-size: 14px;
}
.cart-total {
  font-size: 20px;
  font-weight: bold;
  color: #e94560;
}

JavaScript

// ショッピングカート
const TAX_RATE = 0.10; // 消費税率 10%
let cart = [];

function addToCart(id, name, price) {
  const existing = cart.find(item => item.id === id);
  if (existing) {
    existing.quantity++;
  } else {
    cart.push({ id, name, price, quantity: 1 });
  }
  renderCart();
}

function updateQuantity(id, delta) {
  const item = cart.find(i => i.id === id);
  if (!item) return;
  item.quantity += delta;
  if (item.quantity < 1) cart = cart.filter(i => i.id !== id);
  renderCart();
}

function removeFromCart(id) {
  cart = cart.filter(i => i.id !== id);
  renderCart();
}

function renderCart() {
  const tbody = document.getElementById('cart-body');
  let subtotal = 0;

  tbody.innerHTML = cart.map(item => {
    const itemTotal = item.price * item.quantity;
    subtotal += itemTotal;
    return `<tr>
      <td>${item.name}</td>
      <td>&yen;${item.price.toLocaleString()}</td>
      <td>
        <div class="qty-control">
          <button class="qty-btn" onclick="updateQuantity('${item.id}', -1)">-</button>
          <span>${item.quantity}</span>
          <button class="qty-btn" onclick="updateQuantity('${item.id}', 1)">+</button>
        </div>
      </td>
      <td>&yen;${itemTotal.toLocaleString()}</td>
      <td><button class="remove-btn" onclick="removeFromCart('${item.id}')">&times;</button></td>
    </tr>`;
  }).join('');

  const tax = Math.floor(subtotal * TAX_RATE);
  document.getElementById('cart-subtotal').textContent = `¥${subtotal.toLocaleString()}`;
  document.getElementById('cart-tax').textContent = `¥${tax.toLocaleString()}`;
  document.getElementById('cart-total').textContent = `¥${(subtotal + tax).toLocaleString()}`;
}

10. テンキー入力

使い方: タッチスクリーン対応の数値入力パッドです。 ボタンサイズは最低60pxで、タッチ操作に最適化されています。 POS画面では「金額入力」「数量入力」「会員番号入力」に使えます。 onNumpadEnter コールバックで確定時の処理を実装してください。

HTML

<!-- テンキー入力 -->
<div class="numpad">
  <div class="numpad-display" id="numpadDisplay">0</div>
  <div class="numpad-keys">
    <button class="numpad-btn" data-num="7">7</button>
    <button class="numpad-btn" data-num="8">8</button>
    <button class="numpad-btn" data-num="9">9</button>
    <button class="numpad-btn" data-num="4">4</button>
    <button class="numpad-btn" data-num="5">5</button>
    <button class="numpad-btn" data-num="6">6</button>
    <button class="numpad-btn" data-num="1">1</button>
    <button class="numpad-btn" data-num="2">2</button>
    <button class="numpad-btn" data-num="3">3</button>
    <button class="numpad-btn" data-num="00">00</button>
    <button class="numpad-btn" data-num="0">0</button>
    <button class="numpad-btn numpad-backspace" data-action="backspace"></button>
    <button class="numpad-btn numpad-clear" data-action="clear">C</button>
    <button class="numpad-btn numpad-enter" data-action="enter" style="grid-column: span 2">確定</button>
  </div>
</div>

CSS

/* テンキー */
.numpad {
  max-width: 280px;
  background: #f9f9f9;
  border-radius: 8px;
  padding: 16px;
}
.numpad-display {
  background: #1e1e2e;
  color: #cdd6f4;
  font-size: 28px;
  text-align: right;
  padding: 12px 16px;
  border-radius: 6px;
  margin-bottom: 12px;
  font-family: "Cascadia Code", monospace;
}
.numpad-keys {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
.numpad-btn {
  padding: 16px;
  font-size: 20px;
  border: 1px solid #ddd;
  border-radius: 6px;
  background: #fff;
  cursor: pointer;
  min-height: 60px;
  transition: background 0.1s;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}
.numpad-btn:active { background: #e0e0e0; }
.numpad-clear { color: #e74c3c; }
.numpad-enter {
  background: #e94560;
  color: #fff;
  border-color: #e94560;
}
.numpad-enter:active { background: #c73a52; }

JavaScript

// テンキー入力
let numpadValue = '0';
const numpadDisplay = document.getElementById('numpadDisplay');

function updateNumpadDisplay() {
  const num = parseInt(numpadValue) || 0;
  numpadDisplay.textContent = num.toLocaleString();
}

// 確定時のコールバック(用途に応じて書き換え)
function onNumpadEnter(value) {
  console.log('入力確定:', value);
}

document.querySelector('.numpad-keys').addEventListener('click', (e) => {
  const btn = e.target.closest('.numpad-btn');
  if (!btn) return;

  const num = btn.dataset.num;
  const action = btn.dataset.action;

  if (num) {
    numpadValue = numpadValue === '0' ? num : numpadValue + num;
  } else if (action === 'backspace') {
    numpadValue = numpadValue.slice(0, -1) || '0';
  } else if (action === 'clear') {
    numpadValue = '0';
  } else if (action === 'enter') {
    onNumpadEnter(parseInt(numpadValue) || 0);
    numpadValue = '0';
  }
  updateNumpadDisplay();
});

11. レシート表示

使い方: レシート風のレイアウトで注文内容を表示します。 generateReceipt(order) にオーダーデータを渡すとレシートを生成します。 window.print() で印刷可能。印刷時はレシート部分のみが出力されるCSS付き。 感熱紙サイズ(幅80mm)に合わせた max-width: 320px になっています。

HTML

<!-- レシート表示 -->
<div class="receipt" id="receipt">
  <div class="receipt-header">
    <h3>SHOP NAME</h3>
    <p>〒000-0000 東京都...</p>
    <p>TEL: 03-0000-0000</p>
  </div>
  <div class="receipt-divider"></div>
  <p class="receipt-date" id="receipt-date"></p>
  <div class="receipt-divider"></div>
  <div id="receipt-items">
    <!-- JSで動的に描画 -->
  </div>
  <div class="receipt-divider"></div>
  <div class="receipt-totals" id="receipt-totals"></div>
  <div class="receipt-divider"></div>
  <div class="receipt-footer">
    <p>ありがとうございました</p>
  </div>
</div>
<button onclick="window.print()" class="print-btn">レシートを印刷</button>

CSS

/* レシート表示 */
.receipt {
  max-width: 320px;
  margin: 0 auto;
  background: #fff;
  padding: 20px;
  font-family: "Cascadia Code", monospace;
  font-size: 13px;
  box-shadow: 0 1px 4px rgba(0,0,0,0.1);
}
.receipt-header { text-align: center; }
.receipt-header h3 {
  font-size: 18px;
  margin-bottom: 4px;
}
.receipt-header p {
  font-size: 11px;
  color: #666;
}
.receipt-divider {
  border-top: 1px dashed #aaa;
  margin: 10px 0;
}
.receipt-date {
  font-size: 12px;
  color: #666;
}
.receipt-item {
  display: flex;
  justify-content: space-between;
  margin-bottom: 4px;
}
.receipt-totals .receipt-item { font-size: 13px; }
.receipt-totals .total-line {
  font-size: 18px;
  font-weight: bold;
  margin-top: 4px;
}
.receipt-footer { text-align: center; font-size: 12px; }
.print-btn {
  display: block;
  margin: 16px auto;
  padding: 8px 24px;
  background: #333;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

/* 印刷時はレシートだけ表示 */
@media print {
  body * { visibility: hidden; }
  .receipt, .receipt * { visibility: visible; }
  .receipt {
    position: absolute;
    top: 0;
    left: 0;
    box-shadow: none;
  }
  .print-btn { display: none; }
}

JavaScript

// レシート生成
function generateReceipt(order) {
  // order = { items: [{name, price, quantity}], taxRate: 0.10 }
  const now = new Date();
  document.getElementById('receipt-date').textContent =
    now.toLocaleString('ja-JP');

  // 商品リスト
  const itemsEl = document.getElementById('receipt-items');
  let subtotal = 0;
  itemsEl.innerHTML = order.items.map(item => {
    const total = item.price * item.quantity;
    subtotal += total;
    return `<div class="receipt-item">
      <span>${item.name} x${item.quantity}</span>
      <span>¥${total.toLocaleString()}</span>
    </div>`;
  }).join('');

  // 合計
  const tax = Math.floor(subtotal * (order.taxRate || 0.10));
  document.getElementById('receipt-totals').innerHTML = `
    <div class="receipt-item"><span>小計</span><span>¥${subtotal.toLocaleString()}</span></div>
    <div class="receipt-item"><span>消費税</span><span>¥${tax.toLocaleString()}</span></div>
    <div class="receipt-item total-line"><span>合計</span><span>¥${(subtotal + tax).toLocaleString()}</span></div>
  `;
}

// 使用例:
// generateReceipt({
//   items: [
//     { name: 'コーヒー', price: 350, quantity: 2 },
//     { name: 'サンドイッチ', price: 500, quantity: 1 }
//   ],
//   taxRate: 0.10
// });

12. バーコード入力

使い方: バーコードスキャナーからの高速入力を検知するテンプレートです。 スキャナーは高速にキーストロークを送信し最後にEnterを押します(通常50ms以内)。 手動入力とスキャナー入力を SCAN_THRESHOLD(ms)で判別します。 onBarcodeScanned コールバックで商品検索処理を実装してください。

HTML

<!-- バーコード入力 -->
<div class="barcode-input-wrapper">
  <label for="barcodeInput">バーコード</label>
  <div class="barcode-field">
    <span class="barcode-icon"></span>
    <input type="text" id="barcodeInput" placeholder="スキャンまたは入力..."
           autocomplete="off" autofocus>
    <span class="barcode-status" id="barcodeStatus"></span>
  </div>
</div>

CSS

/* バーコード入力 */
.barcode-input-wrapper { max-width: 400px; }
.barcode-input-wrapper label {
  display: block;
  font-size: 13px;
  color: #666;
  margin-bottom: 4px;
}
.barcode-field {
  display: flex;
  align-items: center;
  border: 2px solid #ddd;
  border-radius: 6px;
  padding: 0 12px;
  transition: border-color 0.2s;
}
.barcode-field:focus-within {
  border-color: #e94560;
}
.barcode-icon { font-size: 20px; color: #999; margin-right: 8px; }
.barcode-field input {
  flex: 1;
  border: none;
  outline: none;
  padding: 12px 0;
  font-size: 16px;
  font-family: "Cascadia Code", monospace;
}
.barcode-status {
  font-size: 12px;
  padding: 2px 8px;
  border-radius: 10px;
}
.barcode-status.scanned {
  background: #2ecc71;
  color: #fff;
}

JavaScript

// バーコードスキャナー入力検知
const SCAN_THRESHOLD = 50; // ms — この時間内の入力はスキャナーと判定
let lastKeyTime = 0;
let isScanning = false;

const barcodeInput = document.getElementById('barcodeInput');
const barcodeStatus = document.getElementById('barcodeStatus');

// スキャン完了時のコールバック
function onBarcodeScanned(code) {
  console.log('スキャン:', code);
  barcodeStatus.textContent = 'スキャン完了';
  barcodeStatus.className = 'barcode-status scanned';
  setTimeout(() => { barcodeStatus.textContent = ''; barcodeStatus.className = 'barcode-status'; }, 2000);
  // ここで商品検索処理を実装
}

barcodeInput.addEventListener('keydown', (e) => {
  const now = Date.now();
  if (now - lastKeyTime < SCAN_THRESHOLD) isScanning = true;
  lastKeyTime = now;

  if (e.key === 'Enter') {
    e.preventDefault();
    const code = barcodeInput.value.trim();
    if (code) {
      onBarcodeScanned(code);
      barcodeInput.value = '';
    }
    isScanning = false;
  }
});

// ページ読み込み時に自動フォーカス
barcodeInput.focus();

13. 会計画面レイアウト

使い方: POS会計画面の2カラムレイアウトです。 左側に商品リスト、右側に合計と支払い方法を配置します。 640px以下で1カラムに切り替わるレスポンシブ対応。 商品カード・カート・テンキーテンプレートと組み合わせて使えます。

HTML

<!-- 会計画面レイアウト -->
<div class="checkout-layout">

  <!-- 左: 商品リスト -->
  <div class="checkout-items">
    <h3>注文内容</h3>
    <table class="checkout-table">
      <thead>
        <tr><th>商品</th><th>数量</th><th>小計</th></tr>
      </thead>
      <tbody id="checkout-body">
        <tr><td>コーヒー(S)</td><td>2</td><td>&yen;700</td></tr>
        <tr><td>サンドイッチ</td><td>1</td><td>&yen;500</td></tr>
      </tbody>
    </table>
  </div>

  <!-- 右: 会計サマリー -->
  <div class="checkout-summary">
    <div class="checkout-totals">
      <div class="checkout-row"><span>小計</span><span>&yen;1,200</span></div>
      <div class="checkout-row"><span>消費税(10%)</span><span>&yen;120</span></div>
      <div class="checkout-row total"><span>合計</span><span>&yen;1,320</span></div>
    </div>

    <div class="payment-methods">
      <p>支払い方法</p>
      <div class="payment-buttons">
        <button class="payment-btn active" data-method="cash">現金</button>
        <button class="payment-btn" data-method="card">カード</button>
        <button class="payment-btn" data-method="epay">電子マネー</button>
      </div>
    </div>

    <button class="checkout-btn">会計する</button>
  </div>

</div>

CSS

/* 会計画面レイアウト */
.checkout-layout {
  display: grid;
  grid-template-columns: 1fr 360px;
  gap: 24px;
  align-items: start;
}
.checkout-items {
  background: #fff;
  border-radius: 8px;
  padding: 20px;
}
.checkout-table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 12px;
}
.checkout-table th, .checkout-table td {
  padding: 10px;
  text-align: left;
  border-bottom: 1px solid #eee;
  font-size: 14px;
}
.checkout-table th { color: #888; font-size: 13px; }
.checkout-summary {
  background: #fff;
  border-radius: 8px;
  padding: 20px;
  position: sticky;
  top: 70px;
}
.checkout-row {
  display: flex;
  justify-content: space-between;
  padding: 8px 0;
  font-size: 14px;
}
.checkout-row.total {
  font-size: 24px;
  font-weight: bold;
  color: #e94560;
  border-top: 2px solid #333;
  margin-top: 8px;
  padding-top: 12px;
}
.payment-methods { margin: 20px 0; }
.payment-methods p {
  font-size: 13px;
  color: #666;
  margin-bottom: 8px;
}
.payment-buttons { display: flex; gap: 8px; }
.payment-btn {
  flex: 1;
  padding: 10px;
  border: 2px solid #ddd;
  border-radius: 6px;
  background: #fff;
  cursor: pointer;
  font-size: 13px;
  transition: border-color 0.2s;
}
.payment-btn.active {
  border-color: #e94560;
  color: #e94560;
  font-weight: bold;
}
.checkout-btn {
  width: 100%;
  padding: 16px;
  background: #e94560;
  color: #fff;
  border: none;
  border-radius: 8px;
  font-size: 18px;
  font-weight: bold;
  cursor: pointer;
  transition: background 0.2s;
}
.checkout-btn:hover { background: #c73a52; }

@media (max-width: 640px) {
  .checkout-layout {
    grid-template-columns: 1fr;
  }
  .checkout-summary { position: static; }
}

JavaScript

// 支払い方法の切り替え
document.querySelectorAll('.payment-btn').forEach(btn => {
  btn.addEventListener('click', () => {
    document.querySelectorAll('.payment-btn').forEach(b => b.classList.remove('active'));
    btn.classList.add('active');
  });
});

// 会計ボタン
document.querySelector('.checkout-btn').addEventListener('click', () => {
  const method = document.querySelector('.payment-btn.active').dataset.method;
  console.log('会計処理:', method);
  // ここに会計処理を実装
  // 例: レシート生成、在庫更新、履歴保存など
});

14. ステータスバッジ

使い方: 注文・取引のステータスを色分けして表示するバッジです。 setBadge(element, 'paid') でバッジの状態を動的に変更できます。 POS画面では「注文ステータス」「在庫ステータス」「取引履歴の状態表示」に使えます。 テーブルの行に組み込んで使用するのが一般的です。

HTML

<!-- ステータスバッジ -->
<span class="badge badge--paid">支払済</span>
<span class="badge badge--pending">未処理</span>
<span class="badge badge--processing">処理中</span>
<span class="badge badge--cancelled">キャンセル</span>
<span class="badge badge--refunded">返金済</span>

<!-- テーブル内での使用例 -->
<table class="order-table">
  <thead>
    <tr><th>注文番号</th><th>金額</th><th>ステータス</th></tr>
  </thead>
  <tbody>
    <tr>
      <td>#0001</td><td>&yen;1,320</td>
      <td><span class="badge badge--paid">支払済</span></td>
    </tr>
    <tr>
      <td>#0002</td><td>&yen;850</td>
      <td><span class="badge badge--pending">未処理</span></td>
    </tr>
  </tbody>
</table>

CSS

/* ステータスバッジ */
.badge {
  display: inline-block;
  padding: 3px 10px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: bold;
}
.badge--paid       { background: #d4edda; color: #155724; }
.badge--pending    { background: #fff3cd; color: #856404; }
.badge--processing { background: #cce5ff; color: #004085; }
.badge--cancelled  { background: #f8d7da; color: #721c24; }
.badge--refunded   { background: #e2e3e5; color: #383d41; }

/* 注文テーブル(使用例) */
.order-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
}
.order-table th, .order-table td {
  padding: 10px;
  text-align: left;
  border-bottom: 1px solid #eee;
}
.order-table th {
  background: #f5f5f5;
  color: #666;
  font-size: 13px;
}

JavaScript

// ステータスバッジの動的変更
const STATUS_MAP = {
  paid:       { label: '支払済',   className: 'badge--paid' },
  pending:    { label: '未処理',   className: 'badge--pending' },
  processing: { label: '処理中',   className: 'badge--processing' },
  cancelled:  { label: 'キャンセル', className: 'badge--cancelled' },
  refunded:   { label: '返金済',   className: 'badge--refunded' }
};

function setBadge(element, status) {
  const info = STATUS_MAP[status];
  if (!info) return;
  // 既存のバッジクラスを全て削除
  Object.values(STATUS_MAP).forEach(s => element.classList.remove(s.className));
  element.classList.add('badge', info.className);
  element.textContent = info.label;
}

// 使用例:
// const badge = document.querySelector('.badge');
// setBadge(badge, 'paid');     // → 支払済(緑)
// setBadge(badge, 'cancelled'); // → キャンセル(赤)