Skip to content

Style 組み込みスタイル - Vant 4

組み込みスタイル

紹介

🎨 スタイルの魔法使いの道具箱

Vant は便利な組み込みスタイルを多数用意しています。クラス名を付与するだけで、美しい見た目を素早く適用できます。煩雑な CSS や複雑なデバッグは不要。磨き上げられたユーティリティクラスがプロジェクトを強力に支援します。🌟

📝 テキスト省略 - 上品なスペース演出

✂️ スマートに長文を省略

テキストがコンテナを超える場合、上品な「…」で省略します。1 行・2 行・3 行表示に対応し、整ったレイアウトを保ちます。🎭

html
これは最大 1 行で表示されるテキストです。超過分は省略されます。
これは最大 2 行で表示されるテキストです。超過分は省略されます。
これは最大 3 行で表示されるテキストです。超過分は省略されます。

🌟 ベストプラクティス

スタイルクラスの組み合わせ

html
<!-- 複数ユーティリティの併用 -->
<div class="van-ellipsis van-haptic-feedback">
  テキスト省略とタッチフィードバックを併用した要素
  </div>

<!-- セーフエリア + 1px ボーダーの組み合わせ -->
<div class="van-safe-area-bottom van-hairline--top">
  下部セーフエリアの内容
</div>

<!-- 複数行省略 + タッチフィードバック -->
<p class="van-multi-ellipsis--l2 van-haptic-feedback">
  長文は 2 行まで表示し、省略記号を付与。タッチフィードバックあり。
</p>

レスポンシブなスタイル適用

javascript
// スタイルを動的適用
const applyResponsiveStyles = (element, isMobile) => {
  if (isMobile) {
    element.classList.add('van-safe-area-bottom');
    element.classList.add('van-haptic-feedback');
  } else {
    element.classList.remove('van-safe-area-bottom');
    element.classList.add('van-ellipsis');
  }
};

// 画面幅に応じて調整
const handleResize = () => {
  const isMobile = window.innerWidth <= 768;
  const elements = document.querySelectorAll('.responsive-element');
  elements.forEach(el => applyResponsiveStyles(el, isMobile));
};

window.addEventListener('resize', handleResize);

パフォーマンス最適化の提案

css
/* カスタム時も一貫性を保つ */
.custom-ellipsis {
  /* Vant の省略スタイルを継承 */
  @extend .van-ellipsis;
  
  /* カスタム効果を追加 */
  color: var(--van-primary-color);
  font-weight: 500;
}

/* スタイル衝突の回避 */
.my-component {
  /* CSS 変数でテーマ一貫性を維持 */
  --van-text-color: #333;
  --van-border-color: #eee;
}

/* アニメーション性能の最適化 */
.smooth-transition {
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  will-change: transform, opacity;
}

💡 使い方のコツ

スマート省略の処理

javascript
// 最適な省略行数を動的計算
const calculateOptimalLines = (element, maxHeight) => {
  const lineHeight = parseInt(getComputedStyle(element).lineHeight);
  const maxLines = Math.floor(maxHeight / lineHeight);
  
  // 行数に応じた省略スタイルを適用
  const ellipsisClass = maxLines === 1 ? 'van-ellipsis' : 
                       maxLines === 2 ? 'van-multi-ellipsis--l2' :
                       'van-multi-ellipsis--l3';
  
  element.className = ellipsisClass;
  return maxLines;
};

// テキスト表示の自適応
const adaptiveTextDisplay = (text, container) => {
  const tempElement = document.createElement('div');
  tempElement.textContent = text;
  tempElement.style.visibility = 'hidden';
  tempElement.style.position = 'absolute';
  
  document.body.appendChild(tempElement);
  
  const textHeight = tempElement.offsetHeight;
  const containerHeight = container.offsetHeight;
  
  document.body.removeChild(tempElement);
  
  return calculateOptimalLines(container, containerHeight);
};

高度なボーダー効果

css
/* グラデーションボーダー */
.gradient-hairline {
  position: relative;
  background: linear-gradient(90deg, transparent, var(--van-border-color), transparent);
}

.gradient-hairline::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 1px;
  background: linear-gradient(
    90deg, 
    transparent 0%, 
    var(--van-border-color) 20%, 
    var(--van-border-color) 80%, 
    transparent 100%
  );
  transform: scaleY(0.5);
}

/* ダイナミックボーダー */
.animated-border {
  position: relative;
  overflow: hidden;
}

.animated-border::before {
  content: '';
  position: absolute;
  top: 0;
  left: -100%;
  width: 100%;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--van-primary-color), transparent);
  animation: border-slide 2s infinite;
  transform: scaleY(0.5);
}

@keyframes border-slide {
  0% { left: -100%; }
  100% { left: 100%; }
}

クリエイティブなセーフエリア活用

vue
<template>
  <div class="smart-safe-area">
    <!-- 上部セーフエリアインジケータ -->
    <div class="safe-area-indicator top" v-if="hasTopNotch">
      <div class="notch-simulation"></div>
    </div>
    
    <!-- コンテンツ領域 -->
    <div class="content-area van-safe-area-top van-safe-area-bottom">
      <slot />
    </div>
    
    <!-- 下部セーフエリアインジケータ -->
    <div class="safe-area-indicator bottom" v-if="hasBottomSafeArea">
      <div class="home-indicator"></div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';

const hasTopNotch = ref(false);
const hasBottomSafeArea = ref(false);

onMounted(() => {
// セーフエリア検出
  const safeAreaTop = getComputedStyle(document.documentElement)
    .getPropertyValue('--van-safe-area-inset-top');
  const safeAreaBottom = getComputedStyle(document.documentElement)
    .getPropertyValue('--van-safe-area-inset-bottom');
    
  hasTopNotch.value = parseInt(safeAreaTop) > 0;
  hasBottomSafeArea.value = parseInt(safeAreaBottom) > 0;
});
</script>

<style scoped>
.notch-simulation {
  width: 150px;
  height: 30px;
  background: #000;
  border-radius: 0 0 15px 15px;
  margin: 0 auto;
}

.home-indicator {
  width: 134px;
  height: 5px;
  background: rgba(0, 0, 0, 0.3);
  border-radius: 2.5px;
  margin: 8px auto;
}
</style>

🔧 よくある問題の解決

省略記号が表示されない

javascript
// Q: 省略スタイルが効かない?
// A: よくある原因を確認

const debugEllipsis = (element) => {
  const styles = getComputedStyle(element);
  
  console.log('省略スタイルをデバッグ:');
  console.log('width:', styles.width);
  console.log('overflow:', styles.overflow);
  console.log('white-space:', styles.whiteSpace);
  console.log('text-overflow:', styles.textOverflow);
  
  // 代表的なチェック
  if (styles.width === 'auto') {
    console.warn('⚠️ 幅が auto。固定幅が必要です');
  }
  
  if (styles.overflow !== 'hidden') {
    console.warn('⚠️ overflow が hidden ではありません');
  }
  
  if (styles.whiteSpace !== 'nowrap' && !element.classList.contains('van-multi-ellipsis')) {
    console.warn('⚠️ 1 行省略には white-space: nowrap が必要');
  }
};

// 省略スタイルの修正
const fixEllipsis = (element) => {
  element.style.width = '100%';
  element.style.maxWidth = '200px'; // 必要に応じて調整
  element.style.overflow = 'hidden';
  element.style.textOverflow = 'ellipsis';
  element.style.whiteSpace = 'nowrap';
};

1px ボーダーが一部デバイスで崩れる

css
/* 解決策:互換性の高い実装を使用 */
.reliable-hairline {
  position: relative;
}

.reliable-hairline::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 200%;
  height: 200%;
  border: 1px solid var(--van-border-color);
  transform: scale(0.5);
  transform-origin: 0 0;
  box-sizing: border-box;
  pointer-events: none;
}

/* 異なる DPR への最適化 */
@media (-webkit-min-device-pixel-ratio: 2) {
  .hairline-dpr2::after {
    transform: scale(0.5);
  }
}

@media (-webkit-min-device-pixel-ratio: 3) {
  .hairline-dpr3::after {
    transform: scale(0.33);
  }
}

セーフエリアが一部ブラウザで効かない

javascript
// セーフエリアの互換性処理
const setupSafeAreaFallback = () => {
  // セーフエリア対応の可否を検出
  const supportsSafeArea = CSS.supports('padding-top: env(safe-area-inset-top)');
  
  if (!supportsSafeArea) {
    // 端末を判定してセーフエリアを設定
    const isIPhoneX = /iPhone/.test(navigator.userAgent) && 
                     window.screen.height === 812 && 
                     window.screen.width === 375;
    
    if (isIPhoneX) {
      document.documentElement.style.setProperty('--van-safe-area-inset-top', '44px');
      document.documentElement.style.setProperty('--van-safe-area-inset-bottom', '34px');
    }
  }
};

    // セーフエリアの動的検出
const detectSafeArea = () => {
  const testElement = document.createElement('div');
  testElement.style.paddingTop = 'env(safe-area-inset-top)';
  testElement.style.visibility = 'hidden';
  testElement.style.position = 'absolute';
  
  document.body.appendChild(testElement);
  
  const computedPadding = getComputedStyle(testElement).paddingTop;
  const hasSafeArea = computedPadding !== '0px';
  
  document.body.removeChild(testElement);
  
  return hasSafeArea;
};

🎨 デザインのヒント

クリエイティブなテキスト効果

css
/* レインボー省略記号 */
.rainbow-ellipsis {
  position: relative;
}

.rainbow-ellipsis::after {
  content: '...';
  background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4, #feca57);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  animation: rainbow-shift 3s ease-in-out infinite;
}

@keyframes rainbow-shift {
  0%, 100% { filter: hue-rotate(0deg); }
  50% { filter: hue-rotate(180deg); }
}

/* タイプライター風の省略 */
.typewriter-ellipsis {
  overflow: hidden;
  white-space: nowrap;
  animation: typewriter 3s steps(40) infinite;
}

@keyframes typewriter {
  0% { width: 0; }
  50% { width: 100%; }
  100% { width: 0; }
}

/* 呼吸するボーダー */
.breathing-border::after {
  animation: breathing 2s ease-in-out infinite;
}

@keyframes breathing {
  0%, 100% { opacity: 0.3; transform: scaleY(0.5); }
  50% { opacity: 1; transform: scaleY(1); }
}

インタラクティブなセーフエリア

css
/* セーフエリアの可視化 */
.visual-safe-area {
  position: relative;
}

.visual-safe-area::before {
  content: '';
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: env(safe-area-inset-top);
  background: linear-gradient(180deg, 
    rgba(255, 107, 107, 0.1) 0%, 
    transparent 100%);
  pointer-events: none;
  z-index: 9999;
}

.visual-safe-area::after {
  content: '';
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  height: env(safe-area-inset-bottom);
  background: linear-gradient(0deg, 
    rgba(78, 205, 196, 0.1) 0%, 
    transparent 100%);
  pointer-events: none;
  z-index: 9999;
}

/* セーフエリアのアニメーションインジケータ */
.safe-area-pulse {
  animation: safe-area-pulse 2s ease-in-out infinite;
}

@keyframes safe-area-pulse {
  0%, 100% { 
    box-shadow: 0 0 0 0 rgba(var(--van-primary-color-rgb), 0.4);
  }
  50% { 
    box-shadow: 0 0 0 10px rgba(var(--van-primary-color-rgb), 0);
  }
}

高度なタッチフィードバック

css
/* リップル効果 */
.ripple-feedback {
  position: relative;
  overflow: hidden;
}

.ripple-feedback::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 0;
  height: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.5);
  transform: translate(-50%, -50%);
  transition: width 0.6s, height 0.6s;
}

.ripple-feedback:active::before {
  width: 300px;
  height: 300px;
}

/* 弾性フィードバック */
.elastic-feedback {
  transition: transform 0.2s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}

.elastic-feedback:active {
  transform: scale(0.95);
}

/* グローフィードバック */
.glow-feedback {
  transition: box-shadow 0.3s ease;
}

.glow-feedback:active {
  box-shadow: 
    0 0 20px rgba(var(--van-primary-color-rgb), 0.5),
    0 0 40px rgba(var(--van-primary-color-rgb), 0.3),
    0 0 60px rgba(var(--van-primary-color-rgb), 0.1);
}

🚀 上級機能の拡張

スマートスタイルシステム

javascript
// アダプティブスタイルマネージャー
class AdaptiveStyleManager {
  constructor() {
    this.observers = new Map();
    this.breakpoints = {
      mobile: 768,
      tablet: 1024,
      desktop: 1200
    };
  }
  
  // レスポンシブスタイルを登録
  registerResponsiveStyle(element, styles) {
    const observer = new ResizeObserver(entries => {
      for (const entry of entries) {
        const { width } = entry.contentRect;
        this.applyResponsiveStyles(element, width, styles);
      }
    });
    
    observer.observe(element);
    this.observers.set(element, observer);
  }
  
  // レスポンシブスタイルを適用
  applyResponsiveStyles(element, width, styles) {
    // 既存のクラスをクリア
    element.className = element.className
      .split(' ')
      .filter(cls => !cls.startsWith('van-'))
      .join(' ');
    
    // 新しいクラスを適用
    if (width <= this.breakpoints.mobile) {
      element.classList.add(...styles.mobile);
    } else if (width <= this.breakpoints.tablet) {
      element.classList.add(...styles.tablet);
    } else {
      element.classList.add(...styles.desktop);
    }
  }
  
  // オブザーバの破棄
  destroy(element) {
    const observer = this.observers.get(element);
    if (observer) {
      observer.disconnect();
      this.observers.delete(element);
    }
  }
}

// 使用例
const styleManager = new AdaptiveStyleManager();

styleManager.registerResponsiveStyle(document.querySelector('.adaptive-text'), {
  mobile: ['van-ellipsis', 'van-haptic-feedback'],
  tablet: ['van-multi-ellipsis--l2'],
  desktop: ['van-multi-ellipsis--l3']
});

テーマ化スタイルシステム

vue
<template>
  <div class="theme-provider" :class="themeClass">
    <div class="style-showcase">
      <div class="demo-card van-hairline--surround van-haptic-feedback">
        <h3 class="van-ellipsis">{{ title }}</h3>
        <p class="van-multi-ellipsis--l2">{{ description }}</p>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';

const currentTheme = ref('light');

const themeClass = computed(() => `theme-${currentTheme.value}`);

const switchTheme = () => {
  currentTheme.value = currentTheme.value === 'light' ? 'dark' : 'light';
};

defineExpose({ switchTheme });
</script>

<style scoped>
.theme-light {
  --van-text-color: #323233;
  --van-border-color: #ebedf0;
  --van-background-color: #ffffff;
}

.theme-dark {
  --van-text-color: #f7f8fa;
  --van-border-color: #323233;
  --van-background-color: #1e1e1e;
}

.demo-card {
  padding: 16px;
  margin: 16px;
  background: var(--van-background-color);
  border-radius: 8px;
  transition: all 0.3s ease;
}

.demo-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
</style>

📚 参考資料

技術ドキュメント

デザインガイド

パフォーマンス最適化

関連コンポーネント

📏 1px ボーダー - 精密なライン演出

🔍 Retina でも美しく見える 1px ボーダー

疑似要素と transform を活用して、髪の毛のように繊細な 1px ボーダーを実現します。輪郭を細筆で描くような、シャープで滲みのない表現に。✨

html

🛡️ セーフエリア - やさしい画面の守護者

📱 全面画面時代のスマート適応のエキスパート。コンテンツを「危険地帯」から遠ざけます。

この心強い画面の守護者は、現代のデバイスにあるノッチ、ピル型、角丸などの特殊領域を熟知しています。経験豊かな安全管理者のように、要素へセーフエリア対応を賢く付与し、重要なコンテンツが決して隠れないようにします。どんな形の画面でも美しく表示できます。🌟

html

🎬 アニメーション - 生き生きしたビジュアルの魔術師

✨ 静的な画面を一瞬で生き生きとさせる動きの達人

このビジュアルの魔術師は豊富なアニメーションの宝庫を備えています。transition コンポーネントという魔法のゲートを通じて、上品なフェードや軽快なスライドなど、さまざまな内蔵エフェクトを手軽に呼び出せます。ひとつひとつのアニメーションが丁寧に調整され、UI に生命力と楽しさを与えます。🎭

html

👆 タッチフィードバック - 繊細なインタラクションの精霊

🎯 すべてのタップに手触りのある応答を返す頼れる相棒

この繊細なインタラクションの精霊は、ユーザーのあらゆるタッチに素早く反応します。指が画面をなぞると、要素の透明度がさりげなく変化し、まるで照れ屋の小さな精霊がウィンクしたかのよう。こうした細やかなフィードバックにより「確かに押した!」と実感でき、ボタンなどのクリック可能な要素に最適です。体験に温かい人間味を添えます。💫

html

🧹 浮動解除 - レイアウトの整理達人

🔧 あらゆる float の「乱れ」を治すレイアウトの清掃員

この働き者の整理達人は、float レイアウトで起こるさまざまな乱れを解消します。要素が風船のように勝手に漂ってしまうときでも、魔法の整頓ワンドで一斉に整列させ、ページをすっきり秩序ある状態へ。ひと工夫でレイアウトの調和を取り戻します。✨

html

Vant に基づく企業向けモバイルソリューション