Navigation
LinkCard
Card that links to another page, with a title and a description
Preview
size
title
description
Code
import css from "./LinkCard.module.css";
type Size = "sm" | "md" | "lg";
interface ResponsiveSize {
base: Size;
mobile?: Size;
tablet?: Size;
desktop?: Size;
wide?: Size;
}
interface LinkCardProps {
href: string;
size: Size | ResponsiveSize;
title: string;
description: string;
}
function getSizeClasses(size: Size | ResponsiveSize): string {
if (typeof size === "string") {
return css[size];
}
const classes = [css[`${size.base}_base`]];
if (size.mobile) classes.push(css[`${size.mobile}_mobile`]);
if (size.tablet) classes.push(css[`${size.tablet}_tablet`]);
if (size.desktop) classes.push(css[`${size.desktop}_desktop`]);
if (size.wide) classes.push(css[`${size.wide}_wide`]);
return classes.join(" ");
}
export default function LinkCard({
href,
size,
title,
description,
}: LinkCardProps) {
return (
<a
href={href}
className={`
${css.card}
${getSizeClasses(size)}
`}
>
<span className={css.card_title}>{title}</span>
<span className={css.card_description}>{description}</span>
</a>
);
}
.card {
display: flex;
flex-direction: column;
border: 1px solid var(--color-border-card);
border-radius: var(--radius-12);
text-decoration: none;
/* font-size is the description's, inherited; the title has a scale of its own. */
&.sm {
padding: var(--space-16);
gap: var(--space-4);
--card-title: var(--text-14);
font-size: var(--text-14);
}
&.md {
padding: var(--space-20);
gap: var(--space-8);
--card-title: var(--text-16);
font-size: var(--text-14);
}
&.lg {
padding: var(--space-24);
gap: var(--space-8);
--card-title: var(--text-18);
font-size: var(--text-16);
}
&.sm_base {
padding: var(--space-16);
gap: var(--space-4);
--card-title: var(--text-14);
font-size: var(--text-14);
}
&.md_base {
padding: var(--space-20);
gap: var(--space-8);
--card-title: var(--text-16);
font-size: var(--text-14);
}
&.lg_base {
padding: var(--space-24);
gap: var(--space-8);
--card-title: var(--text-18);
font-size: var(--text-16);
}
@media (min-width: 480px) {
&.sm_mobile {
padding: var(--space-16);
gap: var(--space-4);
--card-title: var(--text-14);
font-size: var(--text-14);
}
&.md_mobile {
padding: var(--space-20);
gap: var(--space-8);
--card-title: var(--text-16);
font-size: var(--text-14);
}
&.lg_mobile {
padding: var(--space-24);
gap: var(--space-8);
--card-title: var(--text-18);
font-size: var(--text-16);
}
}
@media (min-width: 768px) {
&.sm_tablet {
padding: var(--space-16);
gap: var(--space-4);
--card-title: var(--text-14);
font-size: var(--text-14);
}
&.md_tablet {
padding: var(--space-20);
gap: var(--space-8);
--card-title: var(--text-16);
font-size: var(--text-14);
}
&.lg_tablet {
padding: var(--space-24);
gap: var(--space-8);
--card-title: var(--text-18);
font-size: var(--text-16);
}
}
@media (min-width: 1024px) {
&.sm_desktop {
padding: var(--space-16);
gap: var(--space-4);
--card-title: var(--text-14);
font-size: var(--text-14);
}
&.md_desktop {
padding: var(--space-20);
gap: var(--space-8);
--card-title: var(--text-16);
font-size: var(--text-14);
}
&.lg_desktop {
padding: var(--space-24);
gap: var(--space-8);
--card-title: var(--text-18);
font-size: var(--text-16);
}
}
@media (min-width: 1440px) {
&.sm_wide {
padding: var(--space-16);
gap: var(--space-4);
--card-title: var(--text-14);
font-size: var(--text-14);
}
&.md_wide {
padding: var(--space-20);
gap: var(--space-8);
--card-title: var(--text-16);
font-size: var(--text-14);
}
&.lg_wide {
padding: var(--space-24);
gap: var(--space-8);
--card-title: var(--text-18);
font-size: var(--text-16);
}
}
.card_title {
font-family: var(--font-heading);
font-size: var(--card-title);
font-weight: var(--weight-600);
color: var(--color-text);
}
.card_description {
line-height: var(--leading-normal);
color: var(--color-text-secondary);
}
&:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
@media (hover: hover) {
&:hover {
border-color: var(--color-accent);
}
}
}
<script setup lang="ts">
import { computed } from "vue";
type Size = "sm" | "md" | "lg";
interface ResponsiveSize {
base: Size;
mobile?: Size;
tablet?: Size;
desktop?: Size;
wide?: Size;
}
interface LinkCardProps {
href: string;
size: Size | ResponsiveSize;
title: string;
description: string;
}
const { size } = defineProps<LinkCardProps>();
const sizeClasses = computed(() => {
if (typeof size === "string") {
return size;
}
const classes = [`${size.base}_base`];
if (size.mobile) classes.push(`${size.mobile}_mobile`);
if (size.tablet) classes.push(`${size.tablet}_tablet`);
if (size.desktop) classes.push(`${size.desktop}_desktop`);
if (size.wide) classes.push(`${size.wide}_wide`);
return classes.join(" ");
});
</script>
<template>
<a :href="href" class="card" :class="sizeClasses">
<span class="card_title">{{ title }}</span>
<span class="card_description">{{ description }}</span>
</a>
</template>
<style scoped>
.card {
display: flex;
flex-direction: column;
border: 1px solid var(--color-border-card);
border-radius: var(--radius-12);
text-decoration: none;
/* font-size is the description's, inherited; the title has a scale of its own. */
&.sm {
padding: var(--space-16);
gap: var(--space-4);
--card-title: var(--text-14);
font-size: var(--text-14);
}
&.md {
padding: var(--space-20);
gap: var(--space-8);
--card-title: var(--text-16);
font-size: var(--text-14);
}
&.lg {
padding: var(--space-24);
gap: var(--space-8);
--card-title: var(--text-18);
font-size: var(--text-16);
}
&.sm_base {
padding: var(--space-16);
gap: var(--space-4);
--card-title: var(--text-14);
font-size: var(--text-14);
}
&.md_base {
padding: var(--space-20);
gap: var(--space-8);
--card-title: var(--text-16);
font-size: var(--text-14);
}
&.lg_base {
padding: var(--space-24);
gap: var(--space-8);
--card-title: var(--text-18);
font-size: var(--text-16);
}
@media (min-width: 480px) {
&.sm_mobile {
padding: var(--space-16);
gap: var(--space-4);
--card-title: var(--text-14);
font-size: var(--text-14);
}
&.md_mobile {
padding: var(--space-20);
gap: var(--space-8);
--card-title: var(--text-16);
font-size: var(--text-14);
}
&.lg_mobile {
padding: var(--space-24);
gap: var(--space-8);
--card-title: var(--text-18);
font-size: var(--text-16);
}
}
@media (min-width: 768px) {
&.sm_tablet {
padding: var(--space-16);
gap: var(--space-4);
--card-title: var(--text-14);
font-size: var(--text-14);
}
&.md_tablet {
padding: var(--space-20);
gap: var(--space-8);
--card-title: var(--text-16);
font-size: var(--text-14);
}
&.lg_tablet {
padding: var(--space-24);
gap: var(--space-8);
--card-title: var(--text-18);
font-size: var(--text-16);
}
}
@media (min-width: 1024px) {
&.sm_desktop {
padding: var(--space-16);
gap: var(--space-4);
--card-title: var(--text-14);
font-size: var(--text-14);
}
&.md_desktop {
padding: var(--space-20);
gap: var(--space-8);
--card-title: var(--text-16);
font-size: var(--text-14);
}
&.lg_desktop {
padding: var(--space-24);
gap: var(--space-8);
--card-title: var(--text-18);
font-size: var(--text-16);
}
}
@media (min-width: 1440px) {
&.sm_wide {
padding: var(--space-16);
gap: var(--space-4);
--card-title: var(--text-14);
font-size: var(--text-14);
}
&.md_wide {
padding: var(--space-20);
gap: var(--space-8);
--card-title: var(--text-16);
font-size: var(--text-14);
}
&.lg_wide {
padding: var(--space-24);
gap: var(--space-8);
--card-title: var(--text-18);
font-size: var(--text-16);
}
}
.card_title {
font-family: var(--font-heading);
font-size: var(--card-title);
font-weight: var(--weight-600);
color: var(--color-text);
}
.card_description {
line-height: var(--leading-normal);
color: var(--color-text-secondary);
}
&:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
@media (hover: hover) {
&:hover {
border-color: var(--color-accent);
}
}
}
</style>
Props
| Prop | Type | Required | Description |
|---|---|---|---|
| href | string | required | Link destination. |
| size | "sm" | "md" | "lg" | ResponsiveSize | required | Card size (padding and type). Fixed or responsive. |
| title | string | required | Visible card title. |
| description | string | required | Supporting line under the title. |