Svelte 5 component · fractalsvelte-ui · Layout
ClickableCard
An interactive card that functions as a button or anchor link with elevation hover lift and active press feedback. Supports media slots, custom headers, and external link handling.
Install
ClickableCard is a Svelte 5 clickable card component in @fractaldesign/fractalsvelte-ui. It works in SvelteKit and in any Svelte 5 app.
pnpm add @fractaldesign/fractalsvelte-uiImport
import { ClickableCard } from '@fractaldesign/fractalsvelte-ui';Usage
<ClickableCard>Click me</ClickableCard>Props
Also accepts every attribute of HTMLButtonAttributes, spread onto
the root element.
| Name | Type | Default |
|---|---|---|
href | string Target URL turning the card into an anchor link | — |
target | string Anchor target (e.g. '_blank') | — |
rel | string Anchor rel attribute | — |
disabled | boolean Whether interactive state is disabled | false |
elevation | CardElevation Resting elevation shadow tier | 'sm' |
hoverElevation | CardElevation Hover elevation depth | 'md' |
lift | boolean Whether to animate an upward lift on hover | true |
padding | Space 'none' | '3xs' | '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' Inner content padding token | 'lg' |
radius | 'sm' | 'md' | 'lg' | 'xl' Corner radius scale token | 'lg' |
title | string Primary card title | — |
description | string Secondary explanatory description | — |
class | string Additional CSS classes | '' |
onclick | (event: MouseEvent) => void Optional click event handler | — |
Snippets
| Name | Type | Default |
|---|---|---|
media | Snippet Top media or image slot | — |
header | Snippet Custom full header slot | — |
headerExtra | Snippet Auxiliary header element on the right | — |
footer | Snippet Bottom footer slot | — |
children | Snippet Content body slot | — |
Source
Show the ClickableCard.svelte source
<script lang="ts">
import type { Snippet } from 'svelte';
import type { HTMLButtonAttributes, HTMLAnchorAttributes } from 'svelte/elements';
import type { Space } from '../../types';
import type { CardElevation } from './Card.svelte';
interface Props extends HTMLButtonAttributes {
/** Target URL turning the card into an anchor link */
href?: string;
/** Anchor target (e.g. '_blank') */
target?: string;
/** Anchor rel attribute */
rel?: string;
/** Whether interactive state is disabled */
disabled?: boolean;
/** Resting elevation shadow tier */
elevation?: CardElevation;
/** Hover elevation depth */
hoverElevation?: CardElevation;
/** Whether to animate an upward lift on hover */
lift?: boolean;
/** Inner content padding token */
padding?: Space;
/** Corner radius scale token */
radius?: 'sm' | 'md' | 'lg' | 'xl';
/** Primary card title */
title?: string;
/** Secondary explanatory description */
description?: string;
/** Additional CSS classes */
class?: string;
/** Optional click event handler */
onclick?: (event: MouseEvent) => void;
/** Top media or image slot */
media?: Snippet;
/** Custom full header slot */
header?: Snippet;
/** Auxiliary header element on the right */
headerExtra?: Snippet;
/** Bottom footer slot */
footer?: Snippet;
/** Content body slot */
children?: Snippet;
}
let {
href,
target,
rel,
disabled = false,
elevation = 'sm',
hoverElevation = 'md',
lift = true,
padding = 'lg',
radius = 'lg',
title,
description,
class: className = '',
onclick,
media,
header,
headerExtra,
footer,
children,
...restProps
}: Props = $props();
const isAnchor = $derived(Boolean(href));
const hasHeader = $derived(Boolean(header || title || description || headerExtra));
</script>
{#if isAnchor}
<a
{href}
{target}
rel={target === '_blank' ? (rel ?? 'noopener noreferrer') : rel}
class="f-clickable-card {className}"
data-elevation={elevation}
data-hover-elevation={hoverElevation}
data-lift={lift ? 'true' : 'false'}
data-padding={padding}
data-radius={radius}
aria-disabled={disabled || undefined}
tabindex={disabled ? -1 : undefined}
{...restProps as HTMLAnchorAttributes}
>
{#if media}
<div class="f-clickable-media">
{@render media()}
</div>
{/if}
{#if hasHeader}
<div class="f-clickable-header">
{#if header}
{@render header()}
{:else}
<div class="f-clickable-header-content">
{#if title}
<div class="f-clickable-title">{title}</div>
{/if}
{#if description}
<div class="f-clickable-desc">{description}</div>
{/if}
</div>
{#if headerExtra}
<div class="f-clickable-header-extra">
{@render headerExtra()}
</div>
{/if}
{/if}
</div>
{/if}
{#if children}
<div class="f-clickable-body">
{@render children()}
</div>
{/if}
{#if footer}
<div class="f-clickable-footer">
{@render footer()}
</div>
{/if}
</a>
{:else}
<button
type="button"
class="f-clickable-card {className}"
data-elevation={elevation}
data-hover-elevation={hoverElevation}
data-lift={lift ? 'true' : 'false'}
data-padding={padding}
data-radius={radius}
{disabled}
{onclick}
{...restProps}
>
{#if media}
<div class="f-clickable-media">
{@render media()}
</div>
{/if}
{#if hasHeader}
<div class="f-clickable-header">
{#if header}
{@render header()}
{:else}
<div class="f-clickable-header-content">
{#if title}
<div class="f-clickable-title">{title}</div>
{/if}
{#if description}
<div class="f-clickable-desc">{description}</div>
{/if}
</div>
{#if headerExtra}
<div class="f-clickable-header-extra">
{@render headerExtra()}
</div>
{/if}
{/if}
</div>
{/if}
{#if children}
<div class="f-clickable-body">
{@render children()}
</div>
{/if}
{#if footer}
<div class="f-clickable-footer">
{@render footer()}
</div>
{/if}
</button>
{/if}
<style>
@layer fractalsvelte-ui.components {
.f-clickable-card {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
border: 1px solid var(--f-border);
background: var(--f-surface);
color: var(--f-text);
text-align: left;
text-decoration: none;
font-family: inherit;
cursor: pointer;
user-select: none;
overflow: hidden;
box-sizing: border-box;
transition: transform var(--f-duration-fast) var(--f-ease),
box-shadow var(--f-duration-fast) var(--f-ease),
border-color var(--f-duration-fast) var(--f-ease),
background var(--f-duration-fast) var(--f-ease);
}
.f-clickable-card:hover:not(:disabled):not([aria-disabled='true']) {
border-color: var(--f-border-strong);
background: var(--f-hover);
}
.f-clickable-card[data-lift='true']:hover:not(:disabled):not([aria-disabled='true']) {
transform: translateY(-2px);
}
.f-clickable-card:active:not(:disabled):not([aria-disabled='true']) {
transform: scale(0.99);
}
.f-clickable-card:focus-visible {
outline: 2px solid var(--f-ring);
outline-offset: 2px;
}
.f-clickable-card:disabled,
.f-clickable-card[aria-disabled='true'] {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
/* Resting Elevations */
.f-clickable-card[data-elevation='none'] {
box-shadow: none;
}
.f-clickable-card[data-elevation='xs'] {
box-shadow: var(--f-shadow-xs);
}
.f-clickable-card[data-elevation='sm'] {
box-shadow: var(--f-shadow-sm);
}
.f-clickable-card[data-elevation='md'] {
box-shadow: var(--f-shadow-md);
}
.f-clickable-card[data-elevation='lg'] {
box-shadow: var(--f-shadow-lg);
}
.f-clickable-card[data-elevation='xl'] {
box-shadow: var(--f-shadow-xl);
}
/* Hover Elevations */
.f-clickable-card[data-hover-elevation='none']:hover:not(:disabled) {
box-shadow: none;
}
.f-clickable-card[data-hover-elevation='xs']:hover:not(:disabled) {
box-shadow: var(--f-shadow-xs);
}
.f-clickable-card[data-hover-elevation='sm']:hover:not(:disabled) {
box-shadow: var(--f-shadow-sm);
}
.f-clickable-card[data-hover-elevation='md']:hover:not(:disabled) {
box-shadow: var(--f-shadow-md);
}
.f-clickable-card[data-hover-elevation='lg']:hover:not(:disabled) {
box-shadow: var(--f-shadow-lg);
}
.f-clickable-card[data-hover-elevation='xl']:hover:not(:disabled) {
box-shadow: var(--f-shadow-xl);
}
/* Radii */
.f-clickable-card[data-radius='sm'] {
border-radius: var(--f-radius-sm);
}
.f-clickable-card[data-radius='md'] {
border-radius: var(--f-radius-md);
}
.f-clickable-card[data-radius='lg'] {
border-radius: var(--f-radius-lg);
}
.f-clickable-card[data-radius='xl'] {
border-radius: var(--f-radius-xl);
}
/* Padding */
.f-clickable-card[data-padding='none'] .f-clickable-header,
.f-clickable-card[data-padding='none'] .f-clickable-body,
.f-clickable-card[data-padding='none'] .f-clickable-footer {
padding: 0;
}
.f-clickable-card[data-padding='3xs'] .f-clickable-header,
.f-clickable-card[data-padding='3xs'] .f-clickable-body,
.f-clickable-card[data-padding='3xs'] .f-clickable-footer {
padding: var(--f-space-3xs);
}
.f-clickable-card[data-padding='2xs'] .f-clickable-header,
.f-clickable-card[data-padding='2xs'] .f-clickable-body,
.f-clickable-card[data-padding='2xs'] .f-clickable-footer {
padding: var(--f-space-2xs);
}
.f-clickable-card[data-padding='xs'] .f-clickable-header,
.f-clickable-card[data-padding='xs'] .f-clickable-body,
.f-clickable-card[data-padding='xs'] .f-clickable-footer {
padding: var(--f-space-xs);
}
.f-clickable-card[data-padding='sm'] .f-clickable-header,
.f-clickable-card[data-padding='sm'] .f-clickable-body,
.f-clickable-card[data-padding='sm'] .f-clickable-footer {
padding: var(--f-space-sm);
}
.f-clickable-card[data-padding='md'] .f-clickable-header,
.f-clickable-card[data-padding='md'] .f-clickable-body,
.f-clickable-card[data-padding='md'] .f-clickable-footer {
padding: var(--f-space-md);
}
.f-clickable-card[data-padding='lg'] .f-clickable-header,
.f-clickable-card[data-padding='lg'] .f-clickable-body,
.f-clickable-card[data-padding='lg'] .f-clickable-footer {
padding: var(--f-space-lg);
}
.f-clickable-card[data-padding='xl'] .f-clickable-header,
.f-clickable-card[data-padding='xl'] .f-clickable-body,
.f-clickable-card[data-padding='xl'] .f-clickable-footer {
padding: var(--f-space-xl);
}
.f-clickable-card[data-padding='2xl'] .f-clickable-header,
.f-clickable-card[data-padding='2xl'] .f-clickable-body,
.f-clickable-card[data-padding='2xl'] .f-clickable-footer {
padding: var(--f-space-2xl);
}
.f-clickable-media {
width: 100%;
overflow: hidden;
line-height: 0;
}
.f-clickable-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--f-space-md);
}
.f-clickable-header-content {
display: flex;
flex-direction: column;
gap: var(--f-space-3xs);
flex: 1;
min-width: 0;
}
.f-clickable-title {
font-size: var(--f-text-lg);
font-weight: 600;
color: var(--f-text);
line-height: 1.3;
}
.f-clickable-desc {
font-size: var(--f-text-sm);
color: var(--f-text-muted);
line-height: 1.45;
}
.f-clickable-header-extra {
display: inline-flex;
align-items: center;
flex-shrink: 0;
}
.f-clickable-body {
flex: 1;
font-size: var(--f-text-base);
color: var(--f-text);
line-height: 1.5;
}
.f-clickable-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: var(--f-space-sm);
border-top: 1px solid var(--f-border-subtle);
background: var(--f-sunken);
}
}
</style>Related components
Container A responsive centered container that constrains content width to standardized breakpoints or fluid dimensions.DataTable A dense, responsive tabular data presentation component supporting sortable columns, zebra striping,…Divider A separator line for partitioning adjacent content blocks horizontally or vertically.Flex A flexible flexbox layout utility providing granular control over direction, wrapping, and distribution.Grid A CSS grid layout container featuring track templates for fixed column counts, auto-fit, and auto-fill.Section A semantic page section wrapper offering eyebrow labels, headings, subtitles, and header action slots.