Card Group
Responsive cards with shared header, content and action rows. Uses Grid for wrapping columns and Card for native subgrid alignment. All Card slots are optional; content uses the default slot. Omit the header when the title and description should stay together in the body; that body starts at the card’s content edge without an empty header band. Use the header slot only when separate heading alignment is useful. Put an ActionBar in the actions slot, with the main action in its primary slot, for trailing alignment.
Aligned card regions
Different heading and body lengths share header, content and action rows, then stack in a narrow container.
Fellowship archive
A short description of the archive.
Letters and records from the journey
Read correspondence, maps and field notes collected throughout the journey. Longer descriptions leave the actions aligned.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-card-group/ui-card-group.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-card/ui-card.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-text/ui-text.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-action-bar/ui-action-bar.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-button/ui-button.html"
>
<ui-card-group>
<ui-card>
<ui-text
slot="header"
as="h2"
variant="heading"
>Fellowship archive</ui-text>
<ui-text>A short description of the archive.</ui-text>
<ui-action-bar
slot="actions"
>
<ui-button
slot="primary"
preset="primary"
>Open archive</ui-button>
</ui-action-bar>
</ui-card>
<ui-card>
<ui-text
slot="header"
as="h2"
variant="heading"
>Letters and records from the journey</ui-text>
<ui-text>Read correspondence, maps and field notes collected throughout the journey. Longer descriptions leave the actions aligned.</ui-text>
<ui-action-bar
slot="actions"
>
<ui-button
slot="secondary"
variant="outline"
>Read more</ui-button>
<ui-button
slot="primary"
>Explore records</ui-button>
</ui-action-bar>
</ui-card>
</ui-card-group><script setup>
import { UiActionBar, UiButton, UiCard, UiCardGroup, UiText } from "@threadlabs/looma";
</script>
<template>
<UiCardGroup>
<UiCard>
<template #header>
<UiText
as="h2"
variant="heading"
>Fellowship archive</UiText>
</template>
<UiText>A short description of the archive.</UiText>
<template #actions>
<UiActionBar>
<template #primary>
<UiButton
preset="primary"
>Open archive</UiButton>
</template>
</UiActionBar>
</template>
</UiCard>
<UiCard>
<template #header>
<UiText
as="h2"
variant="heading"
>Letters and records from the journey</UiText>
</template>
<UiText>Read correspondence, maps and field notes collected throughout the journey. Longer descriptions leave the actions aligned.</UiText>
<template #actions>
<UiActionBar>
<template #secondary>
<UiButton
variant="outline"
>Read more</UiButton>
</template>
<template #primary>
<UiButton>Explore records</UiButton>
</template>
</UiActionBar>
</template>
</UiCard>
</UiCardGroup>
</template>import { UiActionBar, UiButton, UiCard, UiCardGroup, UiText } from "@threadlabs/looma";
export default function Example() {
return <UiCardGroup>
<UiCard
slots={{
header: <UiText
as="h2"
variant="heading"
>Fellowship archive</UiText>,
actions: <UiActionBar
slots={{
primary: <UiButton
preset="primary"
>Open archive</UiButton>,
}}
/>,
}}
>
<UiText>A short description of the archive.</UiText>
</UiCard>
<UiCard
slots={{
header: <UiText
as="h2"
variant="heading"
>Letters and records from the journey</UiText>,
actions: <UiActionBar
slots={{
secondary: <UiButton
variant="outline"
>Read more</UiButton>,
primary: <UiButton>Explore records</UiButton>,
}}
/>,
}}
>
<UiText>Read correspondence, maps and field notes collected throughout the journey. Longer descriptions leave the actions aligned.</UiText>
</UiCard>
</UiCardGroup>;
}/** @jsxImportSource preact */
import { UiActionBar, UiButton, UiCard, UiCardGroup, UiText } from "@threadlabs/looma";
export default function Example() {
return <UiCardGroup>
<UiCard
slots={{
header: <UiText
as="h2"
variant="heading"
>Fellowship archive</UiText>,
actions: <UiActionBar
slots={{
primary: <UiButton
preset="primary"
>Open archive</UiButton>,
}}
/>,
}}
>
<UiText>A short description of the archive.</UiText>
</UiCard>
<UiCard
slots={{
header: <UiText
as="h2"
variant="heading"
>Letters and records from the journey</UiText>,
actions: <UiActionBar
slots={{
secondary: <UiButton
variant="outline"
>Read more</UiButton>,
primary: <UiButton>Explore records</UiButton>,
}}
/>,
}}
>
<UiText>Read correspondence, maps and field notes collected throughout the journey. Longer descriptions leave the actions aligned.</UiText>
</UiCard>
</UiCardGroup>;
}<script>
import { UiActionBar, UiButton, UiCard, UiCardGroup, UiText } from "@threadlabs/looma";
</script>
{#snippet header()}
<UiText
as="h2"
variant="heading"
>Fellowship archive</UiText>
{/snippet}
{#snippet actions()}
<UiActionBar
slots={{ primary }}
/>
{/snippet}
{#snippet primary()}
<UiButton
preset="primary"
>Open archive</UiButton>
{/snippet}
{#snippet cardHeader()}
<UiText
as="h2"
variant="heading"
>Letters and records from the journey</UiText>
{/snippet}
{#snippet cardActions()}
<UiActionBar
slots={{ secondary, primary: actionBarPrimary }}
/>
{/snippet}
{#snippet secondary()}
<UiButton
variant="outline"
>Read more</UiButton>
{/snippet}
{#snippet actionBarPrimary()}
<UiButton>Explore records</UiButton>
{/snippet}
<UiCardGroup>
<UiCard
slots={{ header, actions }}
>
<UiText>A short description of the archive.</UiText>
</UiCard>
<UiCard
slots={{ header: cardHeader, actions: cardActions }}
>
<UiText>Read correspondence, maps and field notes collected throughout the journey. Longer descriptions leave the actions aligned.</UiText>
</UiCard>
</UiCardGroup>import { Component } from "@angular/core";
import { UiActionBar, UiButton, UiCard, UiCardGroup, UiText } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiCardGroup, UiCard, UiText, UiActionBar, UiButton],
template: `
<div
ui-card-group
>
<ng-template #header>
<ng-template
ui-text
[as]="'h2'"
[variant]="'heading'"
>Fellowship archive</ng-template>
</ng-template>
<ng-template #actions>
<div
ui-action-bar
>
<ng-container ngProjectAs="[slot='primary']">
<ng-template
ui-button
[preset]="'primary'"
>Open archive</ng-template>
</ng-container>
</div>
</ng-template>
<ng-template
ui-card
[slots]="{ header: header, actions: actions }"
>
<ng-template
ui-text
>A short description of the archive.</ng-template>
</ng-template>
<ng-template #cardHeader>
<ng-template
ui-text
[as]="'h2'"
[variant]="'heading'"
>Letters and records from the journey</ng-template>
</ng-template>
<ng-template #cardActions>
<div
ui-action-bar
>
<ng-container ngProjectAs="[slot='secondary']">
<ng-template
ui-button
[variant]="'outline'"
>Read more</ng-template>
</ng-container>
<ng-container ngProjectAs="[slot='primary']">
<ng-template
ui-button
>Explore records</ng-template>
</ng-container>
</div>
</ng-template>
<ng-template
ui-card
[slots]="{ header: cardHeader, actions: cardActions }"
>
<ng-template
ui-text
>Read correspondence, maps and field notes collected throughout the journey. Longer descriptions leave the actions aligned.</ng-template>
</ng-template>
</div>
`,
})
export default class Example {}Column limit and nested content
Grid's column limit and minimum width apply without page-specific layout rules. A nested card keeps its independent stacked layout.
Research
A supporting note inside the card.
Correspondence
A separate collection of letters.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-card-group/ui-card-group.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-card/ui-card.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-text/ui-text.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-action-bar/ui-action-bar.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-button/ui-button.html"
>
<ui-card-group
columns="2"
min="md"
gap="xl"
>
<ui-card>
<ui-text
slot="header"
as="h2"
variant="heading"
>Research</ui-text>
<ui-card
variant="subtle"
padding="sm"
>
<ui-text>A supporting note inside the card.</ui-text>
</ui-card>
<ui-action-bar
slot="actions"
>
<ui-button
slot="primary"
preset="primary"
>Read notes</ui-button>
</ui-action-bar>
</ui-card>
<ui-card>
<ui-text
slot="header"
as="h2"
variant="heading"
>Correspondence</ui-text>
<ui-text>A separate collection of letters.</ui-text>
<ui-action-bar
slot="actions"
>
<ui-button
slot="primary"
preset="primary"
>Browse letters</ui-button>
</ui-action-bar>
</ui-card>
</ui-card-group><script setup>
import { UiActionBar, UiButton, UiCard, UiCardGroup, UiText } from "@threadlabs/looma";
</script>
<template>
<UiCardGroup
:columns="2"
min="md"
gap="xl"
>
<UiCard>
<template #header>
<UiText
as="h2"
variant="heading"
>Research</UiText>
</template>
<UiCard
variant="subtle"
padding="sm"
>
<UiText>A supporting note inside the card.</UiText>
</UiCard>
<template #actions>
<UiActionBar>
<template #primary>
<UiButton
preset="primary"
>Read notes</UiButton>
</template>
</UiActionBar>
</template>
</UiCard>
<UiCard>
<template #header>
<UiText
as="h2"
variant="heading"
>Correspondence</UiText>
</template>
<UiText>A separate collection of letters.</UiText>
<template #actions>
<UiActionBar>
<template #primary>
<UiButton
preset="primary"
>Browse letters</UiButton>
</template>
</UiActionBar>
</template>
</UiCard>
</UiCardGroup>
</template>import { UiActionBar, UiButton, UiCard, UiCardGroup, UiText } from "@threadlabs/looma";
export default function Example() {
return <UiCardGroup
columns={2}
min="md"
gap="xl"
>
<UiCard
slots={{
header: <UiText
as="h2"
variant="heading"
>Research</UiText>,
actions: <UiActionBar
slots={{
primary: <UiButton
preset="primary"
>Read notes</UiButton>,
}}
/>,
}}
>
<UiCard
variant="subtle"
padding="sm"
>
<UiText>A supporting note inside the card.</UiText>
</UiCard>
</UiCard>
<UiCard
slots={{
header: <UiText
as="h2"
variant="heading"
>Correspondence</UiText>,
actions: <UiActionBar
slots={{
primary: <UiButton
preset="primary"
>Browse letters</UiButton>,
}}
/>,
}}
>
<UiText>A separate collection of letters.</UiText>
</UiCard>
</UiCardGroup>;
}/** @jsxImportSource preact */
import { UiActionBar, UiButton, UiCard, UiCardGroup, UiText } from "@threadlabs/looma";
export default function Example() {
return <UiCardGroup
columns={2}
min="md"
gap="xl"
>
<UiCard
slots={{
header: <UiText
as="h2"
variant="heading"
>Research</UiText>,
actions: <UiActionBar
slots={{
primary: <UiButton
preset="primary"
>Read notes</UiButton>,
}}
/>,
}}
>
<UiCard
variant="subtle"
padding="sm"
>
<UiText>A supporting note inside the card.</UiText>
</UiCard>
</UiCard>
<UiCard
slots={{
header: <UiText
as="h2"
variant="heading"
>Correspondence</UiText>,
actions: <UiActionBar
slots={{
primary: <UiButton
preset="primary"
>Browse letters</UiButton>,
}}
/>,
}}
>
<UiText>A separate collection of letters.</UiText>
</UiCard>
</UiCardGroup>;
}<script>
import { UiActionBar, UiButton, UiCard, UiCardGroup, UiText } from "@threadlabs/looma";
</script>
{#snippet header()}
<UiText
as="h2"
variant="heading"
>Research</UiText>
{/snippet}
{#snippet actions()}
<UiActionBar
slots={{ primary }}
/>
{/snippet}
{#snippet primary()}
<UiButton
preset="primary"
>Read notes</UiButton>
{/snippet}
{#snippet cardHeader()}
<UiText
as="h2"
variant="heading"
>Correspondence</UiText>
{/snippet}
{#snippet cardActions()}
<UiActionBar
slots={{ primary: actionBarPrimary }}
/>
{/snippet}
{#snippet actionBarPrimary()}
<UiButton
preset="primary"
>Browse letters</UiButton>
{/snippet}
<UiCardGroup
columns={2}
min="md"
gap="xl"
>
<UiCard
slots={{ header, actions }}
>
<UiCard
variant="subtle"
padding="sm"
>
<UiText>A supporting note inside the card.</UiText>
</UiCard>
</UiCard>
<UiCard
slots={{ header: cardHeader, actions: cardActions }}
>
<UiText>A separate collection of letters.</UiText>
</UiCard>
</UiCardGroup>import { Component } from "@angular/core";
import { UiActionBar, UiButton, UiCard, UiCardGroup, UiText } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiCardGroup, UiCard, UiText, UiActionBar, UiButton],
template: `
<div
ui-card-group
[columns]="2"
[min]="'md'"
[gap]="'xl'"
>
<ng-template #header>
<ng-template
ui-text
[as]="'h2'"
[variant]="'heading'"
>Research</ng-template>
</ng-template>
<ng-template #actions>
<div
ui-action-bar
>
<ng-container ngProjectAs="[slot='primary']">
<ng-template
ui-button
[preset]="'primary'"
>Read notes</ng-template>
</ng-container>
</div>
</ng-template>
<ng-template
ui-card
[slots]="{ header: header, actions: actions }"
>
<ng-template
ui-card
[variant]="'subtle'"
[padding]="'sm'"
>
<ng-template
ui-text
>A supporting note inside the card.</ng-template>
</ng-template>
</ng-template>
<ng-template #cardHeader>
<ng-template
ui-text
[as]="'h2'"
[variant]="'heading'"
>Correspondence</ng-template>
</ng-template>
<ng-template #cardActions>
<div
ui-action-bar
>
<ng-container ngProjectAs="[slot='primary']">
<ng-template
ui-button
[preset]="'primary'"
>Browse letters</ng-template>
</ng-container>
</div>
</ng-template>
<ng-template
ui-card
[slots]="{ header: cardHeader, actions: cardActions }"
>
<ng-template
ui-text
>A separate collection of letters.</ng-template>
</ng-template>
</div>
`,
})
export default class Example {}Body content with optional headers
Omit the header slot to keep each title and description in natural flow. ActionBar keeps primary actions at the trailing edge and aligned across cards.
Archive
A short description follows its own title.
Letters and records from the journey
A longer title and description remain one continuous body while the action aligns with its neighbour.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-card-group/ui-card-group.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-card/ui-card.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-stack/ui-stack.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-text/ui-text.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-action-bar/ui-action-bar.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-button/ui-button.html"
>
<ui-card-group
columns="2"
min="md"
>
<ui-card>
<ui-stack
gap="s"
>
<ui-text
as="h2"
variant="heading"
>Archive</ui-text>
<ui-text>A short description follows its own title.</ui-text>
</ui-stack>
<ui-action-bar
slot="actions"
>
<ui-button
slot="primary"
preset="primary"
>Open archive</ui-button>
</ui-action-bar>
</ui-card>
<ui-card>
<ui-stack
gap="s"
>
<ui-text
as="h2"
variant="heading"
>Letters and records from the journey</ui-text>
<ui-text>A longer title and description remain one continuous body while the action aligns with its neighbour.</ui-text>
</ui-stack>
<ui-action-bar
slot="actions"
>
<ui-button
slot="primary"
preset="primary"
>Browse records</ui-button>
</ui-action-bar>
</ui-card>
</ui-card-group><script setup>
import { UiActionBar, UiButton, UiCard, UiCardGroup, UiStack, UiText } from "@threadlabs/looma";
</script>
<template>
<UiCardGroup
:columns="2"
min="md"
>
<UiCard>
<UiStack
gap="s"
>
<UiText
as="h2"
variant="heading"
>Archive</UiText>
<UiText>A short description follows its own title.</UiText>
</UiStack>
<template #actions>
<UiActionBar>
<template #primary>
<UiButton
preset="primary"
>Open archive</UiButton>
</template>
</UiActionBar>
</template>
</UiCard>
<UiCard>
<UiStack
gap="s"
>
<UiText
as="h2"
variant="heading"
>Letters and records from the journey</UiText>
<UiText>A longer title and description remain one continuous body while the action aligns with its neighbour.</UiText>
</UiStack>
<template #actions>
<UiActionBar>
<template #primary>
<UiButton
preset="primary"
>Browse records</UiButton>
</template>
</UiActionBar>
</template>
</UiCard>
</UiCardGroup>
</template>import { UiActionBar, UiButton, UiCard, UiCardGroup, UiStack, UiText } from "@threadlabs/looma";
export default function Example() {
return <UiCardGroup
columns={2}
min="md"
>
<UiCard
slots={{
actions: <UiActionBar
slots={{
primary: <UiButton
preset="primary"
>Open archive</UiButton>,
}}
/>,
}}
>
<UiStack
gap="s"
>
<UiText
as="h2"
variant="heading"
>Archive</UiText>
<UiText>A short description follows its own title.</UiText>
</UiStack>
</UiCard>
<UiCard
slots={{
actions: <UiActionBar
slots={{
primary: <UiButton
preset="primary"
>Browse records</UiButton>,
}}
/>,
}}
>
<UiStack
gap="s"
>
<UiText
as="h2"
variant="heading"
>Letters and records from the journey</UiText>
<UiText>A longer title and description remain one continuous body while the action aligns with its neighbour.</UiText>
</UiStack>
</UiCard>
</UiCardGroup>;
}/** @jsxImportSource preact */
import { UiActionBar, UiButton, UiCard, UiCardGroup, UiStack, UiText } from "@threadlabs/looma";
export default function Example() {
return <UiCardGroup
columns={2}
min="md"
>
<UiCard
slots={{
actions: <UiActionBar
slots={{
primary: <UiButton
preset="primary"
>Open archive</UiButton>,
}}
/>,
}}
>
<UiStack
gap="s"
>
<UiText
as="h2"
variant="heading"
>Archive</UiText>
<UiText>A short description follows its own title.</UiText>
</UiStack>
</UiCard>
<UiCard
slots={{
actions: <UiActionBar
slots={{
primary: <UiButton
preset="primary"
>Browse records</UiButton>,
}}
/>,
}}
>
<UiStack
gap="s"
>
<UiText
as="h2"
variant="heading"
>Letters and records from the journey</UiText>
<UiText>A longer title and description remain one continuous body while the action aligns with its neighbour.</UiText>
</UiStack>
</UiCard>
</UiCardGroup>;
}<script>
import { UiActionBar, UiButton, UiCard, UiCardGroup, UiStack, UiText } from "@threadlabs/looma";
</script>
{#snippet actions()}
<UiActionBar
slots={{ primary }}
/>
{/snippet}
{#snippet primary()}
<UiButton
preset="primary"
>Open archive</UiButton>
{/snippet}
{#snippet cardActions()}
<UiActionBar
slots={{ primary: actionBarPrimary }}
/>
{/snippet}
{#snippet actionBarPrimary()}
<UiButton
preset="primary"
>Browse records</UiButton>
{/snippet}
<UiCardGroup
columns={2}
min="md"
>
<UiCard
slots={{ actions }}
>
<UiStack
gap="s"
>
<UiText
as="h2"
variant="heading"
>Archive</UiText>
<UiText>A short description follows its own title.</UiText>
</UiStack>
</UiCard>
<UiCard
slots={{ actions: cardActions }}
>
<UiStack
gap="s"
>
<UiText
as="h2"
variant="heading"
>Letters and records from the journey</UiText>
<UiText>A longer title and description remain one continuous body while the action aligns with its neighbour.</UiText>
</UiStack>
</UiCard>
</UiCardGroup>import { Component } from "@angular/core";
import { UiActionBar, UiButton, UiCard, UiCardGroup, UiStack, UiText } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiCardGroup, UiCard, UiStack, UiText, UiActionBar, UiButton],
template: `
<div
ui-card-group
[columns]="2"
[min]="'md'"
>
<ng-template #actions>
<div
ui-action-bar
>
<ng-container ngProjectAs="[slot='primary']">
<ng-template
ui-button
[preset]="'primary'"
>Open archive</ng-template>
</ng-container>
</div>
</ng-template>
<ng-template
ui-card
[slots]="{ actions: actions }"
>
<div
ui-stack
[gap]="'s'"
>
<ng-template
ui-text
[as]="'h2'"
[variant]="'heading'"
>Archive</ng-template>
<ng-template
ui-text
>A short description follows its own title.</ng-template>
</div>
</ng-template>
<ng-template #cardActions>
<div
ui-action-bar
>
<ng-container ngProjectAs="[slot='primary']">
<ng-template
ui-button
[preset]="'primary'"
>Browse records</ng-template>
</ng-container>
</div>
</ng-template>
<ng-template
ui-card
[slots]="{ actions: cardActions }"
>
<div
ui-stack
[gap]="'s'"
>
<ng-template
ui-text
[as]="'h2'"
[variant]="'heading'"
>Letters and records from the journey</ng-template>
<ng-template
ui-text
>A longer title and description remain one continuous body while the action aligns with its neighbour.</ng-template>
</div>
</ng-template>
</div>
`,
})
export default class Example {}API
Package: @threadlabs/looma · Native root: div
Attributes
| Name | Description | Property | Type | Default | Allowed values |
|---|---|---|---|---|---|
columns | Maximum columns when there is room. Omit for no cap; cards wrap as the container narrows. | columns | integer | — | — |
gap | Space between cards, using Grid’s spacing scale. Grid’s public --ui-grid-gap custom property also applies. | gap | keyword | l | xs, s, m, l, xl |
min | Minimum card column width: sm is 12rem, md 16rem and lg 20rem. A narrower container still fits one column. | min | keyword | lg | sm, md, lg |
Framework props
| Name | Type | Default | Allowed values |
|---|---|---|---|
columns | number | — | — |
gap | "xs" | "s" | "m" | "l" | "xl" | l | xs, s, m, l, xl |
min | "sm" | "md" | "lg" | lg | sm, md, lg |
Events
No events.
Slots
| Name | Purpose |
|---|---|
default | Default child content. |
Design tokens
Set shared theme properties on a theme boundary. Component hooks apply only to their owning component.
Component tokens
No component tokens.
Shared tokens
No shared tokens.