Skip to content
Welcome to vote for your favorite component! You can also tell me anything you want to say! (*´∀`)~♥

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! ✧⁑。٩(ˊᗜˋ*)و✧⁕。

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 border border-gray-200 rounded-xl p-6 pb-16">
    <base-checkbox
      v-model="visible"
      :label="t('show')"
      class="border rounded p-4"
    />

    <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>

<style lang="sass">
</style>

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 border border-gray-200 rounded-xl p-6">
    <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 border rounded p-4 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 border border-gray-200 rounded-xl p-6">
    <base-checkbox
      v-model="visible"
      :label="t('show')"
      class="sticky top-20 z-10 border rounded p-4 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>

<style lang="sass">
</style>

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.59.0