Universal Component

Card

Surface container with slots, material backgrounds, loading, clickable, inertial, and refraction modes.

This page was migrated by AI, please review carefully

Migration is complete, but please validate against source code and manual review.

Card

Surface container with slots, material backgrounds, loading, clickable, inertial, and refraction modes.

Basic Usage

Basic

Demo will load when visible.
<script setup lang="ts">
</script>

<template>
  <div class="card-demo-shell">
    <TxCard variant="solid" background="glass" shadow="none" :padding="14">
      <div class="card-demo-title">
        Card
      </div>
      <div class="card-demo-desc">
        A generic container for content.
      </div>
    </TxCard>
  </div>
</template>

<style scoped>
.card-demo-shell {
  width: 520px;
  max-width: 100%;
}

.card-demo-title {
  font-weight: 600;
  color: var(--tx-text-color-primary, #303133);
}

.card-demo-desc {
  margin-top: 6px;
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}
</style>

Basic slots

Demo will load when visible.
<script setup lang="ts">
</script>

<template>
  <div class="card-demo-shell">
    <TxCard variant="solid" background="glass" shadow="soft" :radius="18" :padding="14">
      <template #header>
        <div class="card-demo-header">
          <div class="card-demo-title">
            Card title
          </div>
          <TxButton size="small" variant="text">
            Action
          </TxButton>
        </div>
      </template>

      <div class="card-demo-desc">
        Slots: <code>header</code> / <code>default</code> / <code>footer</code>
      </div>

      <template #footer>
        <div class="card-demo-footer">
          <TxButton size="small" variant="outline">
            Cancel
          </TxButton>
          <TxButton size="small" variant="primary">
            Confirm
          </TxButton>
        </div>
      </template>
    </TxCard>
  </div>
</template>

<style scoped>
.card-demo-shell {
  width: 520px;
  max-width: 100%;
}

.card-demo-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.card-demo-title {
  font-weight: 600;
  color: var(--tx-text-color-primary, #303133);
}

.card-demo-desc {
  font-size: 12px;
  line-height: 1.5;
  color: var(--tx-text-color-secondary, #909399);
}

.card-demo-footer {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
}
</style>

Inertial Follow-Back (inertial)

inertial

Demo will load when visible.
<script setup lang="ts">
import { ref } from 'vue'

const rebound = ref(0.12)
const maxOffset = ref(26)
</script>

<template>
  <div class="card-inertial-shell">
    <div class="card-inertial-control-row">
      <div class="card-inertial-label">
        rebound
      </div>
      <TxSlider
        v-model="rebound"
        :min="0"
        :max="1"
        :step="0.02"
        show-value
        :format-value="(value) => value.toFixed(2)"
        class="card-inertial-slider"
      />
    </div>

    <div class="card-inertial-control-row">
      <div class="card-inertial-label">
        maxOffset
      </div>
      <TxSlider
        v-model="maxOffset"
        :min="0"
        :max="40"
        :step="1"
        show-value
        class="card-inertial-slider"
      />
    </div>

    <TxCard
      variant="solid"
      background="glass"
      shadow="none"
      inertial
      :inertial-max-offset="maxOffset"
      :inertial-rebound="rebound"
      :padding="14"
    >
      <div class="card-demo-title">
        Inertial drag
      </div>
      <div class="card-demo-desc">
        Move mouse around on hover, leave to snap back.
      </div>
    </TxCard>
  </div>
</template>

<style scoped>
.card-inertial-shell {
  width: 520px;
  max-width: 100%;
  padding: 16px;
  overflow: visible;
}

.card-inertial-control-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 12px;
}

.card-inertial-label {
  min-width: 84px;
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}

.card-inertial-slider {
  flex: 1;
}

.card-demo-title {
  font-weight: 600;
  color: var(--tx-text-color-primary, #303133);
}

.card-demo-desc {
  margin-top: 6px;
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}
</style>

Card with Title

Demo will load when visible.
<script setup lang="ts">
</script>

<template>
  <div class="card-demo-shell">
    <TxCard variant="solid" background="glass" shadow="none" :padding="14">
      <template #header>
        <div class="card-demo-title">
          Card title
        </div>
      </template>
      <div class="card-demo-desc">
        Using <code>header</code> slot
      </div>
    </TxCard>
  </div>
</template>

<style scoped>
.card-demo-shell {
  width: 520px;
  max-width: 100%;
}

.card-demo-title {
  font-weight: 600;
  color: var(--tx-text-color-primary, #303133);
}

.card-demo-desc {
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}
</style>

Card with Action Buttons

Demo will load when visible.
<script setup lang="ts">
</script>

<template>
  <div class="card-actions-shell">
    <TxCard variant="solid" background="glass" shadow="soft" :radius="18" :padding="14">
      <template #header>
        <div class="card-actions-header">
          <div class="card-actions-title">
            User info
          </div>
          <TxButton size="small" variant="text">
            More
          </TxButton>
        </div>
      </template>

      <div class="card-actions-content">
        <div class="card-actions-avatar" />
        <div class="card-actions-content-col">
          <div class="card-actions-title">
            Zhang San
          </div>
          <div class="card-actions-subtitle">
            Frontend Engineer
          </div>
        </div>
      </div>

      <template #footer>
        <div class="card-actions-footer">
          <TxButton size="small" variant="outline">
            Cancel
          </TxButton>
          <TxButton size="small" variant="primary">
            Confirm
          </TxButton>
        </div>
      </template>
    </TxCard>
  </div>
</template>

<style scoped>
.card-actions-shell {
  width: 520px;
  max-width: 100%;
}

.card-actions-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.card-actions-title {
  font-weight: 600;
  color: var(--tx-text-color-primary, #303133);
}

.card-actions-content {
  display: flex;
  gap: 12px;
  align-items: center;
}

.card-actions-avatar {
  width: 34px;
  height: 34px;
  border-radius: 12px;
  background: color-mix(in srgb, var(--tx-color-primary, #409eff) 24%, transparent);
}

.card-actions-content-col {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.card-actions-subtitle {
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}

.card-actions-footer {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}
</style>

Card Variants

Provides multiple visual styles:

variants

Demo will load when visible.
<script setup lang="ts">
</script>

<template>
  <div class="card-variants-wrap">
    <div class="card-variants-grid">
      <TxCard variant="solid" background="glass" shadow="none" :padding="14">
        <div class="card-demo-title">
          solid
        </div>
        <div class="card-demo-desc">
          Border + hover feedback
        </div>
      </TxCard>

      <TxCard variant="dashed" background="glass" shadow="none" :padding="14">
        <div class="card-demo-title">
          dashed
        </div>
        <div class="card-demo-desc">
          Dashed border
        </div>
      </TxCard>

      <TxCard variant="plain" background="glass" shadow="none" :padding="14">
        <div class="card-demo-title">
          plain
        </div>
        <div class="card-demo-desc">
          No border, no hover
        </div>
      </TxCard>
    </div>
  </div>
</template>

<style scoped>
.card-variants-wrap {
  width: 100%;
  overflow-x: auto;
}

.card-variants-grid {
  min-width: 680px;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}

.card-demo-title {
  font-weight: 600;
  color: var(--tx-text-color-primary, #303133);
}

.card-demo-desc {
  margin-top: 6px;
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}
</style>

Effects on Different Backgrounds (refraction / glass / blur)

Single-card comparison: text/graphics behind the card, toggle background (refraction / glass / blur / mask) via the switch. refraction is the recommended advanced style.

Card backgrounds (scroll)

Demo will load when visible.
<script setup lang="ts">
import { computed, ref } from 'vue'

type Bg = 'blur' | 'glass' | 'refraction' | 'mask'
type CardVariant = 'solid' | 'dashed' | 'plain'
type CardShadow = 'none' | 'soft' | 'medium'
type RefractionProfile = 'soft' | 'filmic' | 'cinematic'
type RefractionPresetGroup = 'core' | 'filmic'

interface RefractionPreset {
  id: string
  name: string
  group: RefractionPresetGroup
  profile: RefractionProfile
  strength: number
  angle: number
  blurAmount: number
  overlayOpacity: number
  followIntensity: number
  spring: boolean
  springStiffness: number
  springDamping: number
}

const refractionPresetGroups: Array<{ id: RefractionPresetGroup, name: string }> = [
  { id: 'filmic', name: 'Filmic Set' },
  { id: 'core', name: 'Core Set' },
]

const refractionPresets: RefractionPreset[] = [
  {
    id: 'soft-mist',
    name: 'Soft Mist',
    group: 'core',
    profile: 'soft',
    strength: 42,
    angle: -16,
    blurAmount: 16,
    overlayOpacity: 0.16,
    followIntensity: 0.34,
    spring: true,
    springStiffness: 0.14,
    springDamping: 0.88,
  },
  {
    id: 'filmic-classic',
    name: 'Filmic Classic',
    group: 'core',
    profile: 'filmic',
    strength: 62,
    angle: -24,
    blurAmount: 22,
    overlayOpacity: 0.22,
    followIntensity: 0.5,
    spring: true,
    springStiffness: 0.18,
    springDamping: 0.84,
  },
  {
    id: 'cinematic-rgb',
    name: 'Cinematic RGB',
    group: 'core',
    profile: 'cinematic',
    strength: 82,
    angle: -34,
    blurAmount: 26,
    overlayOpacity: 0.28,
    followIntensity: 0.64,
    spring: true,
    springStiffness: 0.24,
    springDamping: 0.78,
  },
  {
    id: 'sharp-glass',
    name: 'Sharp Glass',
    group: 'core',
    profile: 'filmic',
    strength: 70,
    angle: 14,
    blurAmount: 10,
    overlayOpacity: 0.12,
    followIntensity: 0.46,
    spring: false,
    springStiffness: 0.2,
    springDamping: 0.8,
  },
  {
    id: 'filmic-bloom',
    name: 'Bloom Halo',
    group: 'filmic',
    profile: 'filmic',
    strength: 66,
    angle: -18,
    blurAmount: 30,
    overlayOpacity: 0.34,
    followIntensity: 0.56,
    spring: true,
    springStiffness: 0.16,
    springDamping: 0.86,
  },
  {
    id: 'filmic-chrome',
    name: 'Chrome Split',
    group: 'filmic',
    profile: 'cinematic',
    strength: 88,
    angle: -42,
    blurAmount: 24,
    overlayOpacity: 0.24,
    followIntensity: 0.7,
    spring: true,
    springStiffness: 0.28,
    springDamping: 0.76,
  },
  {
    id: 'filmic-prism',
    name: 'Prism Noir',
    group: 'filmic',
    profile: 'cinematic',
    strength: 78,
    angle: 28,
    blurAmount: 18,
    overlayOpacity: 0.2,
    followIntensity: 0.62,
    spring: true,
    springStiffness: 0.22,
    springDamping: 0.8,
  },
]

const bg = ref<Bg>('refraction')
const variant = ref<CardVariant>('solid')
const shadow = ref<CardShadow>('soft')
const radius = ref(18)
const padding = ref(14)
const clickable = ref(false)
const loading = ref(false)
const disabled = ref(false)
const inertial = ref(false)
const inertialMaxOffset = ref(22)
const inertialRebound = ref(0.12)

const glassBlur = ref(true)
const glassBlurAmount = ref(22)
const glassOverlay = ref(true)
const glassOverlayOpacity = ref(0.22)

const refractionStrength = ref(62)
const refractionProfile = ref<RefractionProfile>('filmic')
const refractionAngle = ref(-24)
const refractionFollowMouse = ref(true)
const refractionFollowIntensity = ref(0.5)
const refractionLightSpring = ref(true)
const refractionLightSpringStiffness = ref(0.18)
const refractionLightSpringDamping = ref(0.84)
const activePresetGroup = ref<RefractionPresetGroup>('filmic')
const activePresetId = ref('filmic-bloom')

const visibleRefractionPresets = computed(() =>
  refractionPresets.filter(item => item.group === activePresetGroup.value),
)

const sliderCommon = { showValue: true as const }
const formatFixed2 = (value: number) => value.toFixed(2)
const formatDeg = (value: number) => `${Math.round(value)}deg`

function applyRefractionPreset(id: string) {
  const preset = refractionPresets.find(item => item.id === id)
  if (!preset)
    return

  activePresetId.value = preset.id
  activePresetGroup.value = preset.group
  bg.value = 'refraction'
  refractionProfile.value = preset.profile
  refractionStrength.value = preset.strength
  refractionAngle.value = preset.angle
  glassBlurAmount.value = preset.blurAmount
  glassOverlayOpacity.value = preset.overlayOpacity
  refractionFollowIntensity.value = preset.followIntensity
  refractionLightSpring.value = preset.spring
  refractionLightSpringStiffness.value = preset.springStiffness
  refractionLightSpringDamping.value = preset.springDamping
}

function switchRefractionPresetGroup(group: RefractionPresetGroup) {
  activePresetGroup.value = group
  const nextPreset = refractionPresets.find(item => item.group === group)
  if (!nextPreset)
    return
  applyRefractionPreset(nextPreset.id)
}

applyRefractionPreset(activePresetId.value)
</script>

<template>
  <div class="tx-card-bg-demo">
    <TxRadioGroup v-model="bg">
      <TxRadio value="refraction">
        refraction
      </TxRadio>
      <TxRadio value="glass">
        glass
      </TxRadio>
      <TxRadio value="blur">
        blur
      </TxRadio>
      <TxRadio value="mask">
        mask
      </TxRadio>
    </TxRadioGroup>

    <div class="tx-card-bg-controls">
      <div class="tx-card-bg-control tx-card-bg-control--block">
        <span class="tx-card-bg-control-label">variant</span>
        <TxRadioGroup v-model="variant" size="small">
          <TxRadio value="solid">
            solid
          </TxRadio>
          <TxRadio value="dashed">
            dashed
          </TxRadio>
          <TxRadio value="plain">
            plain
          </TxRadio>
        </TxRadioGroup>
      </div>

      <div class="tx-card-bg-control tx-card-bg-control--block">
        <span class="tx-card-bg-control-label">shadow</span>
        <TxRadioGroup v-model="shadow" size="small">
          <TxRadio value="none">
            none
          </TxRadio>
          <TxRadio value="soft">
            soft
          </TxRadio>
          <TxRadio value="medium">
            medium
          </TxRadio>
        </TxRadioGroup>
      </div>

      <label class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">radius</span>
        <TxSlider
          v-model="radius"
          :min="10"
          :max="28"
          :step="1"
          v-bind="sliderCommon"
          class="tx-card-bg-control-slider"
        />
      </label>

      <label class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">padding</span>
        <TxSlider
          v-model="padding"
          :min="8"
          :max="24"
          :step="1"
          v-bind="sliderCommon"
          class="tx-card-bg-control-slider"
        />
      </label>

      <label class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">clickable</span>
        <TxSwitch v-model="clickable" />
      </label>

      <label class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">loading</span>
        <TxSwitch v-model="loading" />
      </label>

      <label class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">disabled</span>
        <TxSwitch v-model="disabled" />
      </label>

      <label class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">inertial</span>
        <TxSwitch v-model="inertial" />
      </label>

      <label v-if="inertial" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">max offset</span>
        <TxSlider
          v-model="inertialMaxOffset"
          :min="0"
          :max="40"
          :step="1"
          v-bind="sliderCommon"
          class="tx-card-bg-control-slider"
        />
      </label>

      <label v-if="inertial" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">rebound</span>
        <TxSlider
          v-model="inertialRebound"
          :min="0"
          :max="1"
          :step="0.02"
          v-bind="sliderCommon"
          :format-value="formatFixed2"
          class="tx-card-bg-control-slider"
        />
      </label>

      <label v-if="bg === 'glass' || bg === 'refraction'" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">glass blur</span>
        <TxSwitch v-model="glassBlur" />
      </label>

      <label v-if="(bg === 'glass' || bg === 'refraction') && glassBlur" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">blur</span>
        <TxSlider
          v-model="glassBlurAmount"
          :min="0"
          :max="40"
          :step="1"
          v-bind="sliderCommon"
          class="tx-card-bg-control-slider"
        />
      </label>

      <label v-if="bg === 'glass' || bg === 'refraction'" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">overlay</span>
        <TxSwitch v-model="glassOverlay" />
      </label>

      <label v-if="(bg === 'glass' || bg === 'refraction') && glassOverlay" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">opacity</span>
        <TxSlider
          v-model="glassOverlayOpacity"
          :min="0"
          :max="0.6"
          :step="0.02"
          v-bind="sliderCommon"
          :format-value="formatFixed2"
          class="tx-card-bg-control-slider"
        />
      </label>

      <div v-if="bg === 'refraction'" class="tx-card-bg-control tx-card-bg-control--block">
        <span class="tx-card-bg-control-label">preset</span>
        <div class="tx-card-bg-preset-group-row">
          <TxButton
            v-for="group in refractionPresetGroups"
            :key="group.id"
            class="tx-card-bg-preset-group"
            :variant="activePresetGroup === group.id ? 'primary' : 'outline'"
            size="small"
            @click="switchRefractionPresetGroup(group.id)"
          >
            {{ group.name }}
          </TxButton>
        </div>
        <div class="tx-card-bg-preset-row">
          <TxButton
            v-for="preset in visibleRefractionPresets"
            :key="preset.id"
            class="tx-card-bg-preset"
            :variant="activePresetId === preset.id ? 'primary' : 'outline'"
            size="small"
            @click="applyRefractionPreset(preset.id)"
          >
            {{ preset.name }}
          </TxButton>
        </div>
      </div>

      <label v-if="bg === 'refraction'" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">strength</span>
        <TxSlider
          v-model="refractionStrength"
          :min="0"
          :max="100"
          :step="1"
          v-bind="sliderCommon"
          class="tx-card-bg-control-slider"
        />
      </label>

      <div v-if="bg === 'refraction'" class="tx-card-bg-control tx-card-bg-control--block">
        <span class="tx-card-bg-control-label">profile</span>
        <TxRadioGroup v-model="refractionProfile" size="small">
          <TxRadio value="soft">
            soft
          </TxRadio>
          <TxRadio value="filmic">
            filmic
          </TxRadio>
          <TxRadio value="cinematic">
            cinematic
          </TxRadio>
        </TxRadioGroup>
      </div>

      <label v-if="bg === 'refraction'" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">angle</span>
        <TxSlider
          v-model="refractionAngle"
          :min="-180"
          :max="180"
          :step="1"
          v-bind="sliderCommon"
          :format-value="formatDeg"
          class="tx-card-bg-control-slider"
        />
      </label>

      <label v-if="bg === 'refraction'" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">follow mouse</span>
        <TxSwitch v-model="refractionFollowMouse" />
      </label>

      <label v-if="bg === 'refraction' && refractionFollowMouse" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">follow intensity</span>
        <TxSlider
          v-model="refractionFollowIntensity"
          :min="0"
          :max="1"
          :step="0.02"
          v-bind="sliderCommon"
          :format-value="formatFixed2"
          class="tx-card-bg-control-slider"
        />
      </label>

      <label v-if="bg === 'refraction' && refractionFollowMouse" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">light spring</span>
        <TxSwitch v-model="refractionLightSpring" />
      </label>

      <label v-if="bg === 'refraction' && refractionFollowMouse && refractionLightSpring" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">spring stiffness</span>
        <TxSlider
          v-model="refractionLightSpringStiffness"
          :min="0.01"
          :max="0.55"
          :step="0.01"
          v-bind="sliderCommon"
          :format-value="formatFixed2"
          class="tx-card-bg-control-slider"
        />
      </label>

      <label v-if="bg === 'refraction' && refractionFollowMouse && refractionLightSpring" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">spring damping</span>
        <TxSlider
          v-model="refractionLightSpringDamping"
          :min="0.55"
          :max="0.99"
          :step="0.01"
          v-bind="sliderCommon"
          :format-value="formatFixed2"
          class="tx-card-bg-control-slider"
        />
      </label>
    </div>

    <div class="tx-card-bg-stage">
      <div class="tx-card-bg-scroll">
        <article class="tx-card-bg-article">
          <div class="tx-card-bg-hero" />

          <h3 class="tx-card-bg-title">
            Behind content: Article layout
          </h3>
          <p class="tx-card-bg-meta">
            Dec 25, 2025 · 5 min read
          </p>

          <p class="tx-card-bg-p">
            This demo intentionally uses neutral text and image blocks to help you judge how
            refraction / glass / blur / mask behaves on the same background.
          </p>

          <div class="tx-card-bg-grid">
            <div class="tx-card-bg-img" />
            <div class="tx-card-bg-img is-light" />
          </div>

          <p class="tx-card-bg-p">
            Scroll the content behind. The card stays floating above, similar to a sticky overlay.
            Use the switch to change background.
          </p>

          <div class="tx-card-bg-grid">
            <div class="tx-card-bg-img is-wide" />
            <div class="tx-card-bg-img is-wide is-light" />
          </div>

          <p class="tx-card-bg-p">
            End.
          </p>
          <div class="tx-card-bg-spacer" />
        </article>
      </div>

      <div class="tx-card-bg-overlay">
        <div class="tx-card-bg-card">
          <TxCard
            :variant="variant"
            :background="bg"
            :shadow="shadow"
            :radius="radius"
            :padding="padding"
            :clickable="clickable"
            :loading="loading"
            :disabled="disabled"
            :inertial="inertial"
            :inertial-max-offset="inertialMaxOffset"
            :inertial-rebound="inertialRebound"
            :glass-blur="glassBlur"
            :glass-blur-amount="glassBlurAmount"
            :glass-overlay="glassOverlay"
            :glass-overlay-opacity="glassOverlayOpacity"
            :refraction-strength="refractionStrength"
            :refraction-profile="refractionProfile"
            :refraction-angle="refractionAngle"
            :refraction-light-follow-mouse="refractionFollowMouse"
            :refraction-light-follow-intensity="refractionFollowIntensity"
            :refraction-light-spring="refractionLightSpring"
            :refraction-light-spring-stiffness="refractionLightSpringStiffness"
            :refraction-light-spring-damping="refractionLightSpringDamping"
            class="tx-card-bg-card__inner"
          >
            <template #header>
              <div class="tx-card-bg-card-header">
                <div class="tx-card-bg-card-title">
                  TxCard
                </div>
                <div class="tx-card-bg-card-meta">
                  bg={{ bg }} · {{ refractionProfile }}
                </div>
              </div>
            </template>

            <div class="tx-card-bg-skeleton">
              <div class="tx-card-bg-skeleton-line" />
              <div class="tx-card-bg-skeleton-line tx-card-bg-skeleton-line--w78" />
              <div class="tx-card-bg-skeleton-line tx-card-bg-skeleton-line--w64" />
              <div class="tx-card-bg-skeleton-row">
                <div class="tx-card-bg-skeleton-avatar" />
                <div class="tx-card-bg-skeleton-col">
                  <div class="tx-card-bg-skeleton-line tx-card-bg-skeleton-line--small" />
                  <div class="tx-card-bg-skeleton-line tx-card-bg-skeleton-line--small tx-card-bg-skeleton-line--w70" />
                </div>
              </div>
            </div>
          </TxCard>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped>
.tx-card-bg-demo {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 100%;
}

.tx-card-bg-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
}

.tx-card-bg-control {
  display: inline-flex;
  gap: 8px;
  align-items: center;
  font-size: 13px;
}

.tx-card-bg-control-slider {
  width: 240px;
}

.tx-card-bg-control--block {
  width: 100%;
  align-items: flex-start;
}

.tx-card-bg-control-label {
  color: var(--tx-text-color-secondary, #909399);
}

.tx-card-bg-preset-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.tx-card-bg-preset-group-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 8px;
}

.tx-card-bg-stage {
  position: relative;
  height: 420px;
  border-radius: 16px;
  overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--tx-border-color-light, #e4e7ed) 65%, transparent);
  background:
    linear-gradient(
      180deg,
      color-mix(in srgb, var(--tx-fill-color-lighter, #fafafa) 75%, transparent) 0%,
      color-mix(in srgb, var(--tx-bg-color-overlay, #fff) 82%, transparent) 100%
    );
}

.tx-card-bg-scroll {
  position: absolute;
  inset: 0;
  overflow-y: auto;
}

.tx-card-bg-article {
  padding: 22px;
  line-height: 1.7;
}

.tx-card-bg-hero {
  height: 120px;
  border-radius: 14px;
  background:
    linear-gradient(
      180deg,
      color-mix(in srgb, var(--tx-text-color-primary, #303133) 8%, transparent),
      color-mix(in srgb, var(--tx-text-color-primary, #303133) 3%, transparent)
    );
  border: 1px solid color-mix(in srgb, var(--tx-border-color-light, #e4e7ed) 72%, transparent);
  margin-bottom: 14px;
}

.tx-card-bg-title {
  margin: 0 0 8px 0;
  font-size: 18px;
  font-weight: 700;
  color: var(--tx-text-color-primary, #303133);
}

.tx-card-bg-meta {
  margin: 0 0 14px 0;
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}

.tx-card-bg-p {
  margin: 0 0 14px 0;
  font-size: 13px;
  color: var(--tx-text-color-regular, #606266);
}

.tx-card-bg-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  margin: 14px 0;
}

.tx-card-bg-img {
  height: 96px;
  border-radius: 12px;
  background: color-mix(in srgb, var(--tx-text-color-primary, #303133) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--tx-border-color-light, #e4e7ed) 72%, transparent);
}

.tx-card-bg-img.is-light {
  background: color-mix(in srgb, var(--tx-text-color-primary, #303133) 6%, transparent);
}

.tx-card-bg-img.is-wide {
  height: 120px;
}

.tx-card-bg-spacer {
  height: 240px;
}

.tx-card-bg-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 22px;
  pointer-events: none;
}

.tx-card-bg-card {
  position: relative;
  width: 100%;
  max-width: 320px;
  pointer-events: auto;
}

.tx-card-bg-card__inner {
  position: relative;
  z-index: 1;
  --tx-card-fake-background: color-mix(in srgb, var(--tx-bg-color-overlay, #fff) 94%, transparent);
}

.tx-card-bg-card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.tx-card-bg-card-title {
  font-weight: 700;
  color: var(--tx-text-color-primary, #303133);
}

.tx-card-bg-card-meta {
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}

.tx-card-bg-skeleton {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.tx-card-bg-skeleton-row {
  display: flex;
  gap: 8px;
  margin-top: 6px;
}

.tx-card-bg-skeleton-col {
  flex: 1;
  display: grid;
  gap: 6px;
}

.tx-card-bg-skeleton-line {
  height: 12px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--tx-text-color-primary, #303133) 12%, transparent);
}

.tx-card-bg-skeleton-line--small {
  height: 10px;
}

.tx-card-bg-skeleton-line--w78 {
  width: 78%;
}

.tx-card-bg-skeleton-line--w70 {
  width: 70%;
}

.tx-card-bg-skeleton-line--w64 {
  width: 64%;
}

.tx-card-bg-skeleton-avatar {
  width: 34px;
  height: 34px;
  border-radius: 12px;
  background: color-mix(in srgb, var(--tx-text-color-primary, #303133) 12%, transparent);
}
</style>

Empty Layout

Card with Empty

Demo will load when visible.
<script setup lang="ts">
</script>

<template>
  <div class="card-empty-shell">
    <TxCard class="card-empty-mask" variant="plain" background="mask" shadow="none" :padding="16">
      <TxEmpty title="Nothing here" description="Create your first item to get started.">
        <template #action>
          <TxButton variant="primary" size="small">
            Create
          </TxButton>
        </template>
      </TxEmpty>
    </TxCard>

    <div class="card-empty-block">
      <TxEmpty title="Empty only" description="No Card wrapper, pure empty block." compact />
    </div>
  </div>
</template>

<style scoped>
.card-empty-shell {
  display: grid;
  gap: 12px;
  width: 420px;
  max-width: 100%;
}

.card-empty-mask {
  --tx-card-fake-background: color-mix(in srgb, var(--tx-fill-color-lighter, #fafafa) 82%, transparent);
}

.card-empty-block {
  padding: 14px;
  border-radius: 14px;
  border: 1px dashed color-mix(in srgb, var(--tx-border-color-light, #e4e7ed) 70%, transparent);
}
</style>

Using with Components (Popover / SearchSelect)

Card compositions

Demo will load when visible.
<script setup lang="ts">
import { ref } from 'vue'

const open = ref(false)
const value = ref('')
const selectValue = ref('option1')
const cascaderValue = ref<any>()
const treeValue = ref<any>()

const cascaderOptions = [
  {
    value: 'zhejiang',
    label: 'Zhejiang',
    children: [
      { value: 'hangzhou', label: 'Hangzhou', children: [{ value: 'xihu', label: 'West Lake' }] },
      { value: 'ningbo', label: 'Ningbo', children: [{ value: 'jiangbei', label: 'Jiangbei' }] },
    ],
  },
  {
    value: 'jiangsu',
    label: 'Jiangsu',
    children: [{ value: 'nanjing', label: 'Nanjing', children: [{ value: 'gulou', label: 'Gulou' }] }],
  },
]

const treeNodes = [
  {
    key: 'docs',
    label: 'docs',
    children: [
      { key: 'card', label: 'card.md' },
      { key: 'select', label: 'select.md' },
    ],
  },
  {
    key: 'packages',
    label: 'packages',
    children: [{ key: 'components', label: 'components' }],
  },
]
</script>

<template>
  <div class="card-compositions-shell">
    <div class="card-compositions-row">
      <TxPopover
        v-model="open"
        :offset="8"
        :reference-full-width="false"
        panel-variant="solid"
        panel-background="glass"
        panel-shadow="soft"
        :panel-radius="18"
        :panel-padding="10"
      >
        <template #reference>
          <TxButton variant="primary">
            Popover panel
          </TxButton>
        </template>

        <div class="card-compositions-popover-content">
          <div class="card-compositions-meta">
            Popover panel uses TxCard
          </div>
          <TxButton size="small" @click="open = false">
            Close
          </TxButton>
        </div>
      </TxPopover>

      <div class="card-compositions-meta">
        panelVariant/panelBackground/panelShadow/panelRadius/panelPadding
      </div>
    </div>

    <TxSearchSelect
      v-model="value"
      placeholder="Search..."
      :options="[
        { value: 'a', label: 'Alpha' },
        { value: 'b', label: 'Beta' },
        { value: 'g', label: 'Gamma' },
      ]"
      panel-variant="solid"
      panel-background="glass"
      panel-shadow="soft"
      :panel-radius="18"
      :panel-padding="6"
    />

    <div class="card-compositions-grid">
      <TuffSelect v-model="selectValue" placeholder="Select">
        <TuffSelectItem value="option1" label="Option 1" />
        <TuffSelectItem value="option2" label="Option 2" />
        <TuffSelectItem value="option3" label="Option 3" />
      </TuffSelect>

      <TxCascader v-model="cascaderValue" :options="cascaderOptions" placeholder="Cascader" />
    </div>

    <TxTreeSelect v-model="treeValue" :nodes="treeNodes" placeholder="TreeSelect" />
  </div>
</template>

<style scoped>
.card-compositions-shell {
  display: flex;
  flex-direction: column;
  gap: 14px;
  width: 520px;
  max-width: 100%;
}

.card-compositions-row {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  align-items: center;
}

.card-compositions-popover-content {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.card-compositions-meta {
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}

.card-compositions-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}
</style>

Card Sizes

size

Demo will load when visible.
<script setup lang="ts">
</script>

<template>
  <div class="card-size-grid">
    <TxCard size="small" variant="solid" background="glass" shadow="none" :padding="undefined">
      <div class="card-demo-title">
        size=small
      </div>
      <div class="card-demo-desc">
        Padding controlled by size
      </div>
    </TxCard>

    <TxCard size="medium" variant="solid" background="glass" shadow="none">
      <div class="card-demo-title">
        size=medium
      </div>
      <div class="card-demo-desc">
        Default
      </div>
    </TxCard>

    <TxCard size="large" variant="solid" background="glass" shadow="none">
      <div class="card-demo-title">
        size=large
      </div>
      <div class="card-demo-desc">
        More padding
      </div>
    </TxCard>
  </div>
</template>

<style scoped>
.card-size-grid {
  display: grid;
  gap: 10px;
  width: 520px;
  max-width: 100%;
}

.card-demo-title {
  font-weight: 600;
  color: var(--tx-text-color-primary, #303133);
}

.card-demo-desc {
  margin-top: 6px;
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}
</style>

Layout Props (padding / radius)

layout props

Demo will load when visible.
<script setup lang="ts">
</script>

<template>
  <div class="card-layout-wrap">
    <div class="card-layout-grid">
      <TxCard variant="solid" background="glass" shadow="none" :padding="10" :radius="10">
        <div class="card-demo-title">
          padding
        </div>
        <div class="card-demo-desc">
          padding=10
        </div>
      </TxCard>

      <TxCard variant="solid" background="glass" shadow="none" :padding="18" :radius="10">
        <div class="card-demo-title">
          padding
        </div>
        <div class="card-demo-desc">
          padding=18
        </div>
      </TxCard>

      <TxCard variant="plain" background="glass" shadow="none" :padding="14" :radius="22">
        <div class="card-demo-title">
          radius
        </div>
        <div class="card-demo-desc">
          radius=22
        </div>
      </TxCard>
    </div>
  </div>
</template>

<style scoped>
.card-layout-wrap {
  width: 100%;
  overflow-x: auto;
}

.card-layout-grid {
  min-width: 520px;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}

.card-demo-title {
  font-weight: 600;
  color: var(--tx-text-color-primary, #303133);
}

.card-demo-desc {
  margin-top: 6px;
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}
</style>

States (clickable / loading / disabled)

states

Demo will load when visible.
<script setup lang="ts">
function onClick() {}
</script>

<template>
  <div class="card-states-wrap">
    <div class="card-states-grid">
      <TxCard variant="solid" background="glass" shadow="none" clickable :padding="14" @click="onClick">
        <div class="card-demo-title">
          clickable
        </div>
        <div class="card-demo-desc">
          Hover/active feedback
        </div>
      </TxCard>

      <TxCard variant="solid" background="glass" shadow="none" :loading="true" :padding="14" :loading-spinner-size="12">
        <div class="card-demo-title">
          loading
        </div>
        <div class="card-demo-desc">
          spinner=12
        </div>
      </TxCard>

      <TxCard variant="solid" background="glass" shadow="none" :loading="true" :padding="14" :loading-spinner-size="20">
        <div class="card-demo-title">
          loading
        </div>
        <div class="card-demo-desc">
          spinner=20
        </div>
      </TxCard>

      <TxCard variant="solid" background="glass" shadow="none" disabled :padding="14">
        <div class="card-demo-title">
          disabled
        </div>
        <div class="card-demo-desc">
          Reduced opacity
        </div>
      </TxCard>
    </div>
  </div>
</template>

<style scoped>
.card-states-wrap {
  width: 100%;
  overflow-x: auto;
}

.card-states-grid {
  min-width: 720px;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 12px;
}

.card-demo-title {
  font-weight: 600;
  color: var(--tx-text-color-primary, #303133);
}

.card-demo-desc {
  margin-top: 6px;
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}
</style>

Selection Guide (TxBaseSurface vs TxCard)

  • Prefer TxCard for higher-level, production-oriented usage. It already bundles variant/shadow/slots/loading/inertial and pointer-light + spring interactions.
  • Use TxBaseSurface when you need low-level material tuning, especially refraction/filter parameters (displace, distortionScale, redOffset/greenOffset/blueOffset, filterSaturation/filterContrast/filterBrightness, refractionHaloOpacity).
  • Convention: keep TxCard as a stable, out-of-the-box API; do material experiments and brand-level rendering in TxBaseSurface.

API Reference

Props

PropTypeDefaultDescription
variant'solid' | 'dashed' | 'plain''solid'Border treatment and interaction shape. plain removes the standard border.
background'pure' | 'blur' | 'glass' | 'refraction' | 'mask''pure'Surface mode forwarded to TxBaseSurface; mask applies card color/opacity, glass and refraction enable optical layers.
shadow'none' | 'soft' | 'medium''none'Elevation shadow strength applied by the card wrapper.
size'small' | 'medium' | 'large''medium'Built-in padding preset used when padding is not provided.
radiusnumber18Border radius in px, forwarded to the visual surface and root CSS variable.
paddingnumber-Explicit padding in px. Defaults to 10, 12, or 16 by size.
glassBlurbooleantrueApplies when background='glass' | 'refraction': enables blur.
glassBlurAmountnumber22Applies when background='glass' | 'refraction': blur strength (px).
glassOverlaybooleantrueApplies when background='glass' | 'refraction': enables highlight overlay.
glassOverlayOpacitynumber0.18Applies when background='glass' | 'refraction': overlay opacity.
fallbackMaskOpacitynumber0.26Mask opacity (0-1) while motion fallback is active.
refractionStrengthnumber62Applies when background='refraction': refraction strength (0-100), controlling dispersion and distortion.
refractionProfile'soft' | 'filmic' | 'cinematic''filmic'Applies when background='refraction': refraction style preset.
refractionTone'mist' | 'balanced' | 'vivid''vivid'Applies when background='refraction': refraction tone preset (vivid by default to avoid gray haze).
refractionAnglenumber-24Applies when background='refraction': primary dispersion angle in degrees.
refractionLightFollowMousebooleanfalseApplies when background='refraction': binds the highlight anchor to pointer position.
refractionLightFollowIntensitynumber0.45Applies when background='refraction': pointer follow intensity (0-1), controlling angle/strength coupling.
refractionLightSpringbooleantrueApplies when background='refraction': enables spring interpolation for pointer light tracking.
refractionLightSpringStiffnessnumber0.18Applies when background='refraction': pointer-light spring stiffness (recommended 0.01-0.55).
refractionLightSpringDampingnumber0.84Applies when background='refraction': pointer-light spring damping (recommended 0.55-0.99).
clickablebooleanfalseEnables hover feedback and allows the card to emit click when not disabled.
loadingbooleanfalseShows a loading overlay with TxSpinner.
loadingSpinnerSizenumber-Spinner size in px. Defaults to 12 when omitted.
disabledbooleanfalseApplies disabled styling and blocks card click emission and pointer-driven motion updates.
inertialbooleanfalseEnables pointer-follow transform with spring-like return motion.
inertialMaxOffsetnumber22Maximum pointer-follow offset in px.
inertialReboundnumber0.12Return rebound factor clamped to 0..1; higher values feel more elastic.

Events

EventParamsDescription
click(event: MouseEvent)Emitted only when clickable=true and disabled=false.

Slots

ColumnDescription
defaultMain card body content.
headerOptional header region rendered above the body.
footerOptional footer region rendered below the body.
coverOptional cover region rendered before the header and body.

Style Customization

CSS Variables

EXAMPLE.CSS
.custom-card {
  --tx-card-fake-background: rgba(255, 255, 255, 0.8);
  --tx-card-padding: 24px;
}

Theme Customization

EXAMPLE.CSS
:root {
  /* text */
  --tx-card-background-default: rgba(255, 255, 255, 0.8);
  --tx-card-background-glass: rgba(255, 255, 255, 0.1);
  
  /* text */
  --tx-card-border-default: 1px solid rgba(0, 0, 0, 0.1);
  --tx-card-border-outlined: 1px solid var(--tx-color-border);
  
  /* text */
  --tx-card-shadow-default: 0 2px 8px rgba(0, 0, 0, 0.1);
  --tx-card-shadow-elevated: 0 8px 24px rgba(0, 0, 0, 0.15);
  
  /* textSize */
  --tx-card-padding-small: 16px;
  --tx-card-padding-medium: 20px;
  --tx-card-padding-large: 24px;
}

Best Practices

Usage Tips

  1. Content structure: use header/body/footer sections appropriately
  2. Visual hierarchy: use card variants to create hierarchy
  3. Interaction feedback: provide clear feedback for clickable cards
  4. Responsive design: ensure good behavior across screen sizes

Layout Examples

EXAMPLE.VUE
<template>
  <div class="card-grid">
    <TxCard 
      v-for="item in items" 
      :key="item.id"
      clickable
      @click="handleItemClick(item)"
    >
      <template #header>
        <h3>{{ item.title }}</h3>
      </template>
      
      <p>{{ item.description }}</p>
      
      <template #footer>
        <div class="card-actions">
          <TxButton size="small" variant="text">Edit</TxButton>
          <TxButton size="small" variant="text">Delete</TxButton>
        </div>
      </template>
    </TxCard>
  </div>
</template>