Style 組み込みスタイル - Vant 4
組み込みスタイル
紹介
🎨 スタイルの魔法使いの道具箱 ✨
Vant は便利な組み込みスタイルを多数用意しています。クラス名を付与するだけで、美しい見た目を素早く適用できます。煩雑な CSS や複雑なデバッグは不要。磨き上げられたユーティリティクラスがプロジェクトを強力に支援します。🌟
📝 テキスト省略 - 上品なスペース演出
✂️ スマートに長文を省略
テキストがコンテナを超える場合、上品な「…」で省略します。1 行・2 行・3 行表示に対応し、整ったレイアウトを保ちます。🎭
これは最大 1 行で表示されるテキストです。超過分は省略されます。
これは最大 2 行で表示されるテキストです。超過分は省略されます。
これは最大 3 行で表示されるテキストです。超過分は省略されます。🌟 ベストプラクティス
スタイルクラスの組み合わせ
<!-- 複数ユーティリティの併用 -->
<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>レスポンシブなスタイル適用
// スタイルを動的適用
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);パフォーマンス最適化の提案
/* カスタム時も一貫性を保つ */
.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;
}💡 使い方のコツ
スマート省略の処理
// 最適な省略行数を動的計算
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);
};高度なボーダー効果
/* グラデーションボーダー */
.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%; }
}クリエイティブなセーフエリア活用
<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>🔧 よくある問題の解決
省略記号が表示されない
// 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 ボーダーが一部デバイスで崩れる
/* 解決策:互換性の高い実装を使用 */
.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);
}
}セーフエリアが一部ブラウザで効かない
// セーフエリアの互換性処理
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;
};🎨 デザインのヒント
クリエイティブなテキスト効果
/* レインボー省略記号 */
.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); }
}インタラクティブなセーフエリア
/* セーフエリアの可視化 */
.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);
}
}高度なタッチフィードバック
/* リップル効果 */
.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);
}🚀 上級機能の拡張
スマートスタイルシステム
// アダプティブスタイルマネージャー
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']
});テーマ化スタイルシステム
<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>📚 参考資料
技術ドキュメント
- CSS 変形とアニメーション - transform の理解
- CSS セーフエリア - env() 関数
- テキストオーバーフロー - text-overflow
デザインガイド
パフォーマンス最適化
関連コンポーネント
📏 1px ボーダー - 精密なライン演出
🔍 Retina でも美しく見える 1px ボーダー
疑似要素と transform を活用して、髪の毛のように繊細な 1px ボーダーを実現します。輪郭を細筆で描くような、シャープで滲みのない表現に。✨
🛡️ セーフエリア - やさしい画面の守護者
📱 全面画面時代のスマート適応のエキスパート。コンテンツを「危険地帯」から遠ざけます。
この心強い画面の守護者は、現代のデバイスにあるノッチ、ピル型、角丸などの特殊領域を熟知しています。経験豊かな安全管理者のように、要素へセーフエリア対応を賢く付与し、重要なコンテンツが決して隠れないようにします。どんな形の画面でも美しく表示できます。🌟
🎬 アニメーション - 生き生きしたビジュアルの魔術師
✨ 静的な画面を一瞬で生き生きとさせる動きの達人
このビジュアルの魔術師は豊富なアニメーションの宝庫を備えています。transition コンポーネントという魔法のゲートを通じて、上品なフェードや軽快なスライドなど、さまざまな内蔵エフェクトを手軽に呼び出せます。ひとつひとつのアニメーションが丁寧に調整され、UI に生命力と楽しさを与えます。🎭
👆 タッチフィードバック - 繊細なインタラクションの精霊
🎯 すべてのタップに手触りのある応答を返す頼れる相棒
この繊細なインタラクションの精霊は、ユーザーのあらゆるタッチに素早く反応します。指が画面をなぞると、要素の透明度がさりげなく変化し、まるで照れ屋の小さな精霊がウィンクしたかのよう。こうした細やかなフィードバックにより「確かに押した!」と実感でき、ボタンなどのクリック可能な要素に最適です。体験に温かい人間味を添えます。💫
🧹 浮動解除 - レイアウトの整理達人
🔧 あらゆる float の「乱れ」を治すレイアウトの清掃員
この働き者の整理達人は、float レイアウトで起こるさまざまな乱れを解消します。要素が風船のように勝手に漂ってしまうときでも、魔法の整頓ワンドで一斉に整列させ、ページをすっきり秩序ある状態へ。ひと工夫でレイアウトの調和を取り戻します。✨