Skip to content

Futuristic Card card

A clean, practical sci-fi style information container.

I finally made the component of my dreams! ◝(≧∀≦)◟

My college major was in mechatronics and automatic control, so I've always had a soft spot for sci-fi style components.

My closest past works were:

Due to limited technical skills back then, the interfaces were very basic -- barely touching the sci-fi aesthetic. (́⊙◞౪◟⊙‵)

After all these years, I've finally realized my vision through web technologies! ✧⁑。٩(ˊᗜˋ*)و✧⁕。

Tech Keywords

NameDescription
Pointer EventsDetects pointer movement, clicks, hovers, and more, providing coordinates and target information
Anime.jsLightweight JavaScript animation library
JS AnimationJavaScript-driven animation for more complex, precise control; popular libraries include GSAP and anime.js
CSS clip-pathClips an element to a geometric path, commonly used for masks, cuts, and shaping
Vue SuspenseHandles async components with loading and error states
Vue Provide/InjectVue dependency injection that lets a parent component provide data or methods to all its descendants
Vue AsyncComponentLoads components asynchronously on demand, reducing initial load time
SVGGraphics format usable in the DOM, great for complex shapes and animation; combined with Vue v-bind it enables data-driven effects

Usage Examples

Basic Usage

Title
The best things in life are actually really expensive.
View example source code
vue
<template>
  <div class="w-full flex flex-col gap-10 example-wrap pb-16">
    <base-checkbox
      v-model="visible"
      :label="t('show')"
      class="example-ctrl"
    />

    <div class="h-full flex justify-center">
      <card-futuristic
        v-on-click-outside="() => toggleSelect(false)"
        :visible
        :selected
        class="font-orbitron"
        @click="toggleSelect(true)"
      >
        <div class="flex flex-col gap-4">
          <div class="text-xl font-bold">
            Title
          </div>

          <div>
            The best things in life are actually really expensive.
          </div>
        </div>
      </card-futuristic>
    </div>
  </div>
</template>

<script setup lang="ts">
import { vOnClickOutside } from '@vueuse/components'
import { useToggle } from '@vueuse/core'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseCheckbox from '../../base-checkbox.vue'
import CardFuturistic from '../card-futuristic.vue'

const visible = ref(true)

const [selected, toggleSelect] = useToggle(false)

const { t } = useI18n()
</script>

Mix Parts

Combine different parts to create a variety of styles.

COD-00
FUTURISTIC CARD
CARD-01
QUOTE CORNER
CARD-02
SIDE BORDER CLIP
CARD-03
SOLID BACKGROUND
ERROR
FISH OVERWEIGHT
COD
View example source code
vue
<template>
  <div class="w-full flex flex-col gap-4 example-wrap">
    <div class="flex flex-wrap items-center justify-center gap-10">
      <card-futuristic
        v-for="item, i in list"
        :key="i"
        v-on-click-outside="() => item.selected = false"
        v-bind="item"
        class="font-orbitron"
        @click="item.selected = true"
      >
        <div class="flex flex-col">
          <div
            v-if="item.title"
            :class="item.titleClass"
          >
            {{ item.title }}
          </div>

          <div
            v-if="item.text"
            :class="item.textClass"
          >
            {{ item.text }}
          </div>
        </div>
      </card-futuristic>
    </div>

    <base-checkbox
      v-model="visible"
      :label="t('show')"
      class="checkbox sticky bottom-4 z-10 example-ctrl md:relative md:bottom-0"
    />
  </div>
</template>

<script setup lang="ts">
import type { Writable } from 'type-fest'
import type { ExtractComponentProps } from '../../../types'
import { vOnClickOutside } from '@vueuse/components'
import { promiseTimeout } from '@vueuse/core'
import { map, pipe } from 'remeda'
import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseCheckbox from '../../base-checkbox.vue'
import CardFuturistic from '../card-futuristic.vue'

type CardProp = Writable<ExtractComponentProps<typeof CardFuturistic>> & {
  title?: string;
  titleClass?: string;
  text?: string;
  textClass?: string;
}

const list = ref(pipe(
  [
    {
      title: 'COD-00',
      titleClass: 'text-xl font-bold',
      text: 'FUTURISTIC CARD',
      bg: {
        type: 'halftone',
        color: 'light-dark(#FAFAFA, #2b2b2b)',
      },
      border: {
        type: 'specific',
        selectedColor: '#FFF',
        strokeWidth: 1,
        side: {
          t: {},
          l: {},
          b: {},
          r: {},
        },
      },
      animeSequence: {
        visible: {
          border: { duration: 400 },
        },
      },
    },
    {
      title: 'CARD-01',
      titleClass: 'text-xl font-bold',
      text: 'QUOTE CORNER',
      corner: { type: 'quote' },
      content: {
        type: 'scale',
        class: 'p-4',
      },
      bg: {
        type: 'halftone',
        color: 'light-dark(#FAFAFA, #2b2b2b)',
      },
      border: null,
      animeSequence: {
        visible: {
          content: { delay: 200 },
          bg: { delay: 400 },
        },
      },
    },
    {
      title: 'CARD-02',
      titleClass: 'text-xl font-bold ',
      text: 'SIDE BORDER CLIP',
      textClass: '',
      corner: null,
      content: {
        type: 'clip',
        class: 'p-4',
      },
      bg: {
        type: 'typical',
        margin: '0',
        color: 'light-dark(#FAFAFA, #2b2b2b)',
      },
      border: { type: 'side' },
      animeSequence: {
        normal: {
          border: { delay: 0 },
        },
        visible: {
          border: { delay: 0 },
          bg: { delay: 200 },
          content: { delay: 300 },
        },
        hidden: {
          border: { delay: 300 },
          bg: { delay: 0 },
          content: { delay: 0 },
        },
      },
    },
    {
      title: 'CARD-03',
      titleClass: 'text-xl font-bold text-white',
      text: 'SOLID BACKGROUND',
      textClass: 'text-white',
      corner: null,
      bg: {
        type: 'solid',
        selectedColor: '#444',
      },
      content: {
        type: 'typical',
        class: 'p-4 pl-6',
      },
      border: { type: 'specific' },
      animeSequence: {
        visible: {
          border: { delay: 400 },
        },
      },
    },
    {
      title: 'ERROR',
      titleClass: 'text-2xl font-bold text-[#ba2507] ',
      text: 'FISH OVERWEIGHT',
      textClass: 'text-[#ba2507]',
      corner: null,
      bg: {
        type: 'typical',
        margin: '4px 0px',
        color: 'light-dark(#ffe8e8, #451717)',
      },
      content: {
        type: 'typical',
        class: 'py-4 px-8',
      },
      border: {
        type: 'specific',
        color: '#ba2507',
        selectedColor: '#f07860',
        strokeWidth: 2,
        side: {
          t: {},
          b: {},
        },
      },
      animeSequence: {
        normal: {
          border: { delay: 0 },
        },
        visible: {
          border: { delay: 0 },
          bg: { delay: 400 },
          content: { delay: 500 },
        },
        hidden: {
          border: { delay: 300 },
          bg: { delay: 0 },
          content: { delay: 0 },
        },
        // null 表示停用動畫
        selected: { content: null },
        hover: { content: null },
      },
    },
    {
      title: 'COD',
      corner: {
        type: 'square',
        size: 2,
      },
      bg: null,
      content: {
        type: 'typical',
        class: 'p-1 px-2',
      },
      border: {
        type: 'typical',
        color: '#BBB',
      },
      animeSequence: {
        visible: {
          corner: { delay: 0 },
          border: { delay: 400 },
          content: { delay: 500 },
        },
        hidden: {
          corner: { delay: 400 },
          border: { delay: 0 },
          content: { delay: 0 },
        },
      },
    },
  ] as CardProp[],
  map((data) => ({
    ...data,
    visible: false,
    selected: false,
  })),
))

const visible = ref(false)
watch(visible, async (value) => {
  for (const data of list.value) {
    data.visible = value
    await promiseTimeout(100)
  }
})

const { t } = useI18n()
</script>

<style lang="sass">
.checkbox
  background: light-dark(#FFF, #222)
</style>

Text Animation

Use the text component to create sci-fi text effects.

Futuristic Card

This component is composed of border, bg, corner, content and ornament sub-components, with the card parent component orchestrating animations.

Sub-components can be freely combined to produce N kinds of interesting style designs.

Uses Vue-bound SVG attributes for rendering and anime.js for animations.

View example source code
vue
<template>
  <div class="w-full flex flex-col gap-4 example-wrap">
    <base-checkbox
      v-model="visible"
      :label="t('show')"
      class="sticky top-20 z-10 example-ctrl md:relative md:top-0"
    />

    <div class="flex flex-wrap items-center justify-center gap-20">
      <card-futuristic
        v-for="item, i in list"
        :key="i"
        v-on-click-outside="() => item.selected = false"
        v-bind="item"
        class="font-orbitron"
        @click="item.selected = true"
      >
        <div class="flex flex-col gap-2">
          <card-futuristic-text
            v-if="item.title"
            :text="item.title"
            :class="item.titleClass"
            class="!m-0"
          />

          <card-futuristic-text
            v-for="line, j in item.textList"
            :key="j"
            :text="line"
            :class="item.textClass"
            class="!m-0"
          />
        </div>
      </card-futuristic>
    </div>
  </div>
</template>

<script setup lang="ts">
import type { Writable } from 'type-fest'
import type { ExtractComponentProps } from '../../../types'
import { vOnClickOutside } from '@vueuse/components'
import { promiseTimeout } from '@vueuse/core'
import { map, pipe } from 'remeda'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseCheckbox from '../../base-checkbox.vue'
import CardFuturisticText from '../card-futuristic-text.vue'
import CardFuturistic from '../card-futuristic.vue'

type CardProp = Writable<ExtractComponentProps<typeof CardFuturistic>> & {
  title?: string;
  titleClass?: string;
  textList?: string[];
  textClass?: string;
}

const { t } = useI18n()

const list = computed(() => pipe(
  [
    {
      title: 'Futuristic Card',
      titleClass: 'text-xl font-bold',
      textList: [
        t('textLine1'),
        t('textLine2'),
        t('textLine3'),
      ],
      bg: {
        type: 'halftone',
        color: '#0001',
        dotSize: '1px',
        size: '10px',
      },
      corner: {
        type: 'quote',
        strokeWidth: 8,
      },
      border: {
        type: 'typical',
        color: '#777',
      },
      animeSequence: {
        visible: {
          corner: { delay: 0 },
          bg: { delay: 400 },
          border: { delay: 400 },
          content: { delay: 500 },
        },
        hidden: {
          corner: { delay: 700 },
          bg: { delay: 200 },
          border: { delay: 100 },
          content: { delay: 100 },
        },
        hover: {
          corner: null,
          border: null,
          content: null,
        },
        selected: {
          corner: null,
          border: null,
          content: null,
        },
      },
    },
  ] as CardProp[],
  map((data) => ({
    ...data,
    visible: false,
    selected: false,
  })),
))

const visible = ref(false)
watch(visible, async (value) => {
  for (const data of list.value) {
    data.visible = value
    await promiseTimeout(100)
  }
})
</script>

How It Works

The component is composed of sub-components: border, bg, corner, and content, with the card parent component orchestrating the animations.

Sub-components can be freely combined to produce N kinds of interesting style designs.

The sub-components use Vue-bound SVG attributes for rendering and anime.js for animations.

Source Code

API

Props

interface Props {
  /** 動畫序列,可自定義動畫參數 */
  animeSequence?: Partial<AnimeSequence>;

  visible?: boolean;
  selected?: boolean;
  /** 為空則自動處理,有提供則以參數數值為主 */
  hover?: boolean;

  /** null 表示不使用此元件 */
  border?: BorderParam | null;
  bg?: BgParam | null;
  corner?: CornerParam | null;
  content?: ContentParam | null;
}

Methods

defineExpose({
  /** 執行特定 part 狀態動畫 */
  execute,
})

Slots

interface Slots {
  default?: () => unknown;
}

v0.71.4