Card
Display content in a card with a header, body and footer.
Demo
Card Title
Lorem ipsum dolor sit amet consectetur adipisicing elit. Vel officia fugit repellendus quasi nemo tempora quos possimus aliquid earum voluptates iste doloremque tempore laudantium, placeat nihil quia quis perferendis voluptatem?
Related Types
This requires the following types to be installed:
Related Theme
This requires the following theme to be installed:
Component
vue
<script lang="ts">
import type { ComponentConfig } from '@/ui/utils/utils'
import theme from '@/ui/theme/card'
import { Primitive } from 'reka-ui'
import { tv } from 'tailwind-variants'
import { computed } from 'vue'
type Card = ComponentConfig<typeof theme>
export interface CardProps {
as?: any
variant?: Card['variants']['variant']
class?: any
ui?: Card['slots']
}
export interface CardSlots {
header: (props?: object) => any
default: (props?: object) => any
footer: (props?: object) => any
}
</script>
<script setup lang="ts">
const props = defineProps<CardProps>()
const slots = defineSlots<CardSlots>()
const ui = computed(() => tv(theme)({
variant: props.variant,
}))
</script>
<template>
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
<div v-if="!!slots.header" :class="ui.header({ class: props.ui?.header })">
<slot name="header" />
</div>
<div v-if="!!slots.default" :class="ui.body({ class: props.ui?.body })">
<slot />
</div>
<div v-if="!!slots.footer" :class="ui.footer({ class: props.ui?.footer })">
<slot name="footer" />
</div>
</Primitive>
</template>vue
<script lang="ts">
import type { ComponentConfig } from '@/UI/Utils/utils'
import theme from '@/UI/Theme/card'
import { Primitive } from 'reka-ui'
import { tv } from 'tailwind-variants'
import { computed } from 'vue'
type Card = ComponentConfig<typeof theme>
export interface CardProps {
as?: any
variant?: Card['variants']['variant']
class?: any
ui?: Card['slots']
}
export interface CardSlots {
header: (props?: object) => any
default: (props?: object) => any
footer: (props?: object) => any
}
</script>
<script setup lang="ts">
const props = defineProps<CardProps>()
const slots = defineSlots<CardSlots>()
const ui = computed(() => tv(theme)({
variant: props.variant,
}))
</script>
<template>
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
<div v-if="!!slots.header" :class="ui.header({ class: props.ui?.header })">
<slot name="header" />
</div>
<div v-if="!!slots.default" :class="ui.body({ class: props.ui?.body })">
<slot />
</div>
<div v-if="!!slots.footer" :class="ui.footer({ class: props.ui?.footer })">
<slot name="footer" />
</div>
</Primitive>
</template>Theme
ts
export default {
slots: {
root: '',
header: '',
body: '',
footer: '',
},
variants: {
variant: {
solid: {
root: '',
},
outline: {
root: '',
},
soft: {
root: '',
},
subtle: {
root: '',
},
},
},
defaultVariants: {
variant: 'outline',
} as const,
}View Nuxt UI theme
ts
export default {
slots: {
root: 'rounded-lg',
header: 'p-4 sm:px-6',
body: 'p-4 sm:p-6',
footer: 'p-4 sm:px-6',
},
variants: {
variant: {
solid: {
root: 'bg-inverted text-inverted',
},
outline: {
root: 'bg-default ring ring-default divide-y divide-default',
},
soft: {
root: 'bg-elevated/50 divide-y divide-default',
},
subtle: {
root: 'bg-elevated/50 ring ring-default divide-y divide-default',
},
},
},
defaultVariants: {
variant: 'outline',
} as const,
}Test
To test this component, you can use the following test file:
ts
import type { RenderOptions } from '@testing-library/vue'
import Card from '@/ui/components/Card.vue'
import { render, screen } from '@testing-library/vue'
import { describe, expect, it } from 'vitest'
describe('card', () => {
it.each<[string, RenderOptions<typeof Card>]>([
// Props
['with as', { props: { as: 'section' } }],
['with class', { props: { class: 'rounded-xl' } }],
['with ui', { props: { ui: { body: 'font-bold' } } }],
// Slots
['with default slot', { slots: { default: () => 'Default slot' } }],
['with header slot', { slots: { header: () => 'Header slot' } }],
['with footer slot', { slots: { footer: () => 'Footer slot' } }],
])('renders %s correctly', (name, options) => {
render(Card, {
attrs: {
'data-testid': 'card',
},
...options,
})
expect(screen.getByTestId('card')).toMatchSnapshot()
})
})ts
import type { RenderOptions } from '@testing-library/vue'
import Card from '@/UI/Components/Card.vue'
import { render, screen } from '@testing-library/vue'
import { describe, expect, it } from 'vitest'
describe('card', () => {
it.each<[string, RenderOptions<typeof Card>]>([
// Props
['with as', { props: { as: 'section' } }],
['with class', { props: { class: 'rounded-xl' } }],
['with ui', { props: { ui: { body: 'font-bold' } } }],
// Slots
['with default slot', { slots: { default: () => 'Default slot' } }],
['with header slot', { slots: { header: () => 'Header slot' } }],
['with footer slot', { slots: { footer: () => 'Footer slot' } }],
])('renders %s correctly', (name, options) => {
render(Card, {
attrs: {
'data-testid': 'card',
},
...options,
})
expect(screen.getByTestId('card')).toMatchSnapshot()
})
})