Use article for content that stands on its own
Sakura no Uta
OngoingOn my first day at the new school, I met a girl on the roof holding an old camera.
12 volumes
<script setup lang="ts">
import { Button, Card, Divider, Heading, Inline, Stack, Tag, Text } from '@hina-ui/vue'
</script>
<template>
<Card class="w-full max-w-sm">
<Stack>
<Stack gap="xs">
<Inline justify="between" align="start" :wrap="false">
<Heading :level="3" size="md">Sakura no Uta</Heading>
<Tag tone="success">Ongoing</Tag>
</Inline>
<Text tone="muted" size="sm">
On my first day at the new school, I met a girl on the roof holding an old camera.
</Text>
</Stack>
<Divider />
<Inline justify="between">
<Text tone="faint" size="sm">12 volumes</Text>
<Button variant="ghost" tone="neutral" size="sm">Open</Button>
</Inline>
</Stack>
</Card>
</template>
Usage
import { Card } from '@hina-ui/vue'
A card is a surface: its own fill, a hairline border and a resting shadow, one step above the page ground. Content goes into the default slot.
A card brings its own background, a hairline border and a resting shadow. Content goes straight into the default slot.
<script setup lang="ts">
import { Card, Text } from '@hina-ui/vue'
</script>
<template>
<Card class="w-full max-w-sm">
<Text>
A card brings its own background, a hairline border and a resting shadow. Content goes
straight into the default slot.
</Text>
</Card>
</template>
The card owns the surface, not the arrangement inside it. Leave direction and spacing to layout components such as Stack and Inline.
Examples
Padding
Padding is on by default and follows the current density. For content that should run to the edge — images, tables — set padded to false and let an inner container supply the padding instead. The card then clips its overflow, so the square corners of edge-to-edge content cannot cover the card radius.
The slope down to the station
The image runs to the edge, and an inner container supplies the padding instead.
<script setup lang="ts">
import { Card, Heading, Image, Stack, Text } from '@hina-ui/vue'
</script>
<template>
<Card :padded="false" class="w-full max-w-sm overflow-hidden">
<Image src="/sample.webp" alt="A slope on a summer afternoon" class="h-32 w-full" />
<Stack gap="xs" class="p-[var(--hn-panel-p)]">
<Heading :level="3" size="base">The slope down to the station</Heading>
<Text tone="muted" size="sm">
The image runs to the edge, and an inner container supplies the padding instead.
</Text>
</Stack>
</Card>
</template>
Add overflow-hidden when the content needs to be clipped to the corner radius.
Semantic element
as sets the rendered element. It defaults to div; reach for something else only when the semantics are more precise: article for content that stands on its own, section for a block within the page, and — when a set of cards is a list — ul on the container with li on each card.
Use section for a block within the page
<script setup lang="ts">
import { Card, Stack, Text } from '@hina-ui/vue'
</script>
<template>
<Stack class="w-full max-w-sm">
<Card as="article">
<Text>Use article for content that stands on its own</Text>
</Card>
<Card as="section">
<Text>Use section for a block within the page</Text>
</Card>
</Stack>
</template>
Clickable cards
A card carries no clickable styling of its own: no hover tint, no shadow step. When the whole card should be pressable, the caller composes it: as="button" for the element, hn-interactive for the pointer and focus ring, hn-state-layer for the hover ink, hn-press-lg to scale the press down to something suited to a large target, plus a Ripple.
<script setup lang="ts">
import { Card, Heading, Ripple, Stack, Text } from '@hina-ui/vue'
</script>
<template>
<Card as="button" class="hn-interactive hn-state-layer hn-press-lg w-full max-w-sm text-start">
<Ripple />
<Stack gap="xs">
<Heading :level="3" size="base">The whole card is one press target</Heading>
<Text tone="muted" size="sm">
Ink on hover and a ripple on press — the caller opts into both.
</Text>
</Stack>
</Card>
</template>
Don’t do this when only one part of the card is clickable — put a Button or Link there and leave the card static.
Density
Padding reads a density token, so setting data-density="compact" on any ancestor tightens every card inside it. Nothing needs to be passed to the card.
comfortable
20px of padding
compact
14px of padding
<script setup lang="ts">
import { Card, Inline, Stack, Text } from '@hina-ui/vue'
</script>
<template>
<Inline align="start" class="w-full max-w-lg">
<Card class="flex-1">
<Stack gap="xs">
<Text size="sm" tone="faint">comfortable</Text>
<Text>20px of padding</Text>
</Stack>
</Card>
<Card data-density="compact" class="flex-1">
<Stack gap="xs">
<Text size="sm" tone="faint">compact</Text>
<Text>14px of padding</Text>
</Stack>
</Card>
</Inline>
</template>
Accessibility
- A card renders a
divby default. It carries no role, and screen readers announce its content as ordinary content. - When the whole card is clickable it must become a
buttonor a link. Never attach a click handler to adiv: the keyboard can’t reach it and screen readers won’t announce it as a control. - Use
Headingfor titles inside a card and pick alevelthat continues the page’s existing heading outline.
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
padded | boolean | true | Whether the card has padding; follows density, and clips overflow when off |
as | string | 'div' | The rendered element |
as-child | boolean | false | Render no element of its own and merge the styles onto the sole child |
class | string | — | Classes appended to the root element |
Slots
| Slot | Description |
|---|---|
default | The card’s content |