@swisper.ai/cards-ui
v0.3.3
Published
Swisper cards beautiful ui elements
Readme
@swisper.ai/cards-ui
Swisper cards UI elements. The components hold layout and collapse state only: the app owns the data and any edit state, and passes it in as props. Styled with Tailwind v4 utilities against the @swisper.ai/design-system theme.
Boundary
Code belongs here if it is a card or something that sizes or arranges cards: the collapsible result card and its parts, and the containers that fit a card to the space around it. A static box that only groups content belongs in @swisper.ai/ui (Card). What a card's content is built from (Typography, Chip, Banner, Avatar and the like) is exported here too, under the name it has in @swisper.ai/ui: each is one component that allows only what belongs in a card, so a card needs no import from @swisper.ai/ui. The primitives and their parts stay in @swisper.ai/ui. The chat surface around the cards (messages, composer, ChatContainer) belongs in @swisper.ai/web-chat-ui.
Install
npm install @swisper.ai/cards-ui| Peer | Range | Why |
| ----------- | ---------- | -------------------- |
| react | >=18.0.0 | Components and hooks |
| react-dom | >=18.0.0 | Rendering |
It depends on @swisper.ai/ui, whose peers (@mui/material, @mui/system, @mui/icons-material, @emotion/react, @emotion/styled) apply too.
Setup
- Install the package and the peers above.
- Load the styles once, at the app's entry:
import '@swisper.ai/cards-ui/index.css';- Render a card. Inside a chat, wrap it in
FlexibleContainerso it lines up with the messages:
import {
Card,
CardBody,
CardCollapsibleButton,
CardActions,
CardContent,
CardHeader,
CardHeaderContent,
CardTitle,
FlexibleContainer,
} from '@swisper.ai/cards-ui';
export function BookingCard() {
return (
<FlexibleContainer>
<Card aria-labelledby="booking-title">
<CardHeader>
<CardHeaderContent>
<CardTitle id="booking-title">Booking details</CardTitle>
</CardHeaderContent>
<CardActions>
<CardCollapsibleButton aria-label="Toggle booking details" />
</CardActions>
</CardHeader>
<CardBody>
<CardContent>Zürich HB to Interlaken Ost, Tue, Oct 15</CardContent>
</CardBody>
</Card>
</FlexibleContainer>
);
}FlexibleContainer and a closed Card read --chat-window-width and --message-list-content-width, which ChatContainer from @swisper.ai/web-chat-ui publishes. So do twoColumnLayoutCardVariants and threeColumnLayoutCardVariants, the width recipes for the card around a column layout, which also read --spacing-message. Elsewhere, set them on an ancestor or the card takes its parent's width.
Styles
@swisper.ai/cards-ui/index.css is a prebuilt stylesheet: Tailwind's preflight, the design-system theme and every utility the @swisper.ai packages use. Load it once. If your app also loads the prebuilt index.css of another @swisper.ai package, or builds its own Tailwind CSS, the same rules ship twice and whichever file loads last wins where they conflict.
API
Props and live examples are in the Echo DS Storybook, under Cards.
| Export | Why it exists |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Card | A structured result the user reviews or acts on, that can collapse to its header so the user keeps reading around it. level={2} nests one card in another. CardFullScreenButton takes it fullscreen |
| CardHeader, CardIcon, CardHeaderContent, CardTitle, CardSubTitle | The header names the object; on a collapsible card it is the toggle |
| CardActions, CardActionButton, CardCollapsibleButton, CardFullScreenButton, CardMenuButton | Header buttons. CardActionButton never toggles the card; CardCollapsibleButton is the keyboard path to toggle it; CardFullScreenButton takes the card fullscreen and back; CardMenuButton triggers a ui Menu |
| CardBody, CardContent, CardPreview, CardMedia | CardBody is the part that collapses; CardPreview is shown only while closed |
| CardFields, CardAttributes, CardAttribute, CardAttributeLabel, CardAttributeValue | A label/value summary, such as e-mail headers |
| CardFooter, CardFooterLeft, CardFooterRight, CardFooterActions | The decisions about the object: an optional block of your own on the left, actions on the right |
| List, ListItem | A column of selectable rows for a card (hotels, connections). The whole row selects; isSelected / onSelect are yours. Escape drops focus from the list |
| ListItemMedia, ListItemContent, ListItemFooter, ListItemMarker | The row's grid: a picture or icon, the text column, a full-width footer, and a badge on a corner (placement; listItemMarkerVariants sets classes per corner) |
| ListItemHeader, ListItemLeading, ListItemOrder, ListItemTitle, ListItemTrailing, ListItemActions | The header line: anything before and after the title, a numbered OrderChip, and the actions at the end |
| ListItemDescription, ListItemMeta, ListItemMetaValue, ListItemCaption | The facts under the header: lines of bullet-separated values, and a quieter caption line; they describe the row |
| CollapsibleList, CollapsibleListItem | A column of disclosure rows. The header toggles the body. Rows start closed and open independently. For a row the user selects, use List |
| CollapsibleListItemHeader, CollapsibleListItemMarker, CollapsibleListItemMedia, CollapsibleListItemText, CollapsibleListItemTitle, CollapsibleListItemDescription, CollapsibleListItemMeta, CollapsibleListItemChevron | The always-visible row: a corner badge, a picture, the title and description, a trailing fact, and the chevron. The header is the button, named by the title |
| CollapsibleListItemContent | The body that folds away. While closed its children unmount, unless forceMount is set. It shows the expanded row's background unless given its own |
| collapsibleListItemVariants, collapsibleListItemHeaderVariants, collapsibleListItemContentVariants | Build a function className for the row, the header and the body: your classes, plus classes per state ({ expanded, collapsed, disabled }), merged over the default look. expanded on the row replaces its accent background |
| ActionsMenuButton, ListItemAction | The standard ⋮ menu, shown on the hovered and selected row (isAlwaysVisible for every row); ListItemAction wraps any other control in a row so it doesn't select it |
| PrimaryButton, SecondaryButton, TertiaryButton, DangerButton | The text buttons of a card's body, footer and list rows: ui's Button with the variant and width fixed, so cards look alike. size is md by default; pass sm in a list row. DangerButton is the text-only danger-subtle. For full width or the filled danger, use Button from @swisper.ai/ui |
| IconButton | An icon-only action in a list row or a card's body: ui's IconButton fixed to the tertiary look, sm by default. In the card header use CardActionButton |
| Link | A link in a card's content, such as a website or a phone number: ui's Link under the card's name. startIcon puts an icon before the text; hasUnderline={false} drops the hover underline, for a phone number |
| Typography | Text in a card's content: ui's Typography with the body-*, label-* and caption-* styles only. A card's headings are CardTitle and CardSubTitle, so there is no title-*; ui's deprecated aliases (body, body-sm) are left out too |
| Chip | A short label, such as an attribute or a status: ui's Chip in the sizes 3xs, 2xs and md, outlined and info by default. For a larger chip, use Chip from @swisper.ai/ui |
| Avatar | A person or an organisation: ui's Avatar as one component. name gives the image its alt and the initials shown without a picture; fallback replaces the initials, badge puts a count on the corner |
| MetaList | One line of secondary facts (3 nights • 2 guests): ui's MetaList drawn from an items array, with the separators put in between. An entry that is null, undefined, false or '' is left out |
| Tooltip, withTooltip, TooltipProvider | A hint on hover or focus: ui's Tooltip under the card's name. withTooltip is a Storybook decorator that puts a story inside a TooltipProvider. The app mounts one TooltipProvider above its cards; no card mounts one |
| ScrollArea | A part of a card that scrolls in a fixed height: ui's ScrollArea, which draws its own scrollbar |
| VisuallyHidden | Text a screen reader reads and the screen does not show, such as "(opens in new tab)" in a Link |
| Banner | A status message in a card (a booking went through, a payment failed): ui's Banner as one component. title, description and timestamp are props, hasCloseButton shows the close button, and actions takes the card's buttons |
| EmptyState | What a card shows where its content would be when there is none: ui's EmptyState as one component, with icon, title, description and actions props. The title is an h3, under the card's own title |
| Skeleton | A placeholder shape for a part of a card that is still loading. className gives it the size of what it stands in for |
| Spinner | The sign that a card is waiting: ui's Spinner in the sizes sm, md and lg |
| Form | A card's form: a <form> with the browser's validation bubbles off and no layout of its own. Render it as <Card asChild><Form> so the footer buttons are inside the form |
| FieldGroup, FieldGroupTitle, FieldGroupDescription, FieldGroupFields | A section of a form: a <fieldset> named by its title (the <legend>), an optional description, and its fields stacked |
| FormFieldsDivider | The line between two FieldGroups: ui's Divider, always horizontal |
| Field, FieldLabel, FieldInfo, FieldDescription, FieldError | The layout of one control with its label, info tooltip, description and error, stacked or on a row. Layout only: you pass htmlFor, aria-describedby and aria-invalid. FieldError renders nothing while empty, unless forceMount |
| FormFieldInputProps | The props every field below takes: name (required), id, label, description, errorMessage, info, infoLabel, orientation, hasError, isDisabled, isReadOnly, isRequired, className. The fields render the Field layout and connect the ids themselves |
| ComboBoxField | A searchable single-select field for 8 or more options (countries, currencies), driven by an options array. For grouped rows or a custom trigger, compose ui's ComboBox parts inside Field |
| SelectField | A single-select field for 7 or fewer options (a room type, a language), driven by an options array. For grouped rows or a custom trigger, compose ui's Select parts inside Field |
| CounterField | A count field the user steps or types in a card (guests, rooms), its label beside it: ui's CounterField fixed to the medium size and the width of its number. For a floating label, another size or full width, use CounterField from @swisper.ai/ui |
| TextField | One line of text in a card's form (a name, a street, a postal code), its label above it: ui's TextField fixed to the medium size. For a floating label, another size or a field outside a card, use TextField from @swisper.ai/ui |
| EmailField | An email address: TextField with type="email", the email keyboard and the email autofill token. It does not validate: set errorMessage |
| SwitchField | An on/off setting that applies at once (travelling with pets), its label beside it: ui's Switch fixed to the medium size. For another size or an icon in the thumb, use Switch from @swisper.ai/ui inside Field |
| CheckboxField | A yes/no answer sent with the form (accept the conditions), the box before its label on one row, so it takes no orientation: ui's Checkbox fixed to the medium size. No indeterminate state |
| RadioGroupField | One choice out of 2 to 5 options worth comparing (a fare), driven by an options array of { value, label, description?, isDisabled? }, always stacked. For a row of options, compose ui's RadioGroup parts inside Field |
| FlexibleContainer | Sizes a card against the chat window: fixed to the reading column, fluid to its content, full to the window; inline inside a message body |
| ListLayout, ListLayoutItems, ListLayoutShowMore, useListLayoutContext | Shows the first few cards of a list and mounts the rest only behind a "show more" button. |
| TwoColumnLayout, TwoColumnLayoutAside, TwoColumnLayoutMain, twoColumnLayout*Variants | A card body with an aside beside a main pane, stacked when the card is narrow. |
| ThreeColumnLayout, ThreeColumnLayoutList, ThreeColumnLayoutMain, ThreeColumnLayoutDetails, ThreeColumnLayoutPanel, threeColumnLayout*Variants | A card body shaped like a collection, a canvas and a selected item (places on a map), which switches panes on its own measured width. |
| PhotoViewerRoot, PhotoViewerTrigger, PhotoViewerDialog, PhotoViewerModal, PhotoViewerOverlay | A full-screen viewer for a set of photos, shaped like ui's Dialog. The root holds the open state and startAt; uncontrolled with a trigger, or controlled for one viewer shared by several thumbnails |
| PhotoViewerTitle, PhotoViewerClose, PhotoViewerCarousel, PhotoViewerItem, PhotoViewerPrevious, PhotoViewerNext, PhotoViewerFooter, PhotoViewerCounter | What goes in the modal: a hidden title that names the dialog, the close button, one item per photo (src, alt), the arrows, and a footer with the 2 / 6 counter and any buttons of your own |
| PhotoThumb | The thumbnail that opens the viewer: the first photo and the number of photos. Wrap it in PhotoViewerTrigger asChild |
| Carousel, CarouselItem | A row of a card's large photos that scrolls sideways, with no arrows: the photo cut off at the edge says more follow. Each item is a button (src, aria-label); handle onClick, usually to open the viewer at that photo |
| Gallery, GalleryHero, GalleryList, GalleryThumb | One large photo over a strip of thumbnails that picks it. A thumb's src is one URL or [thumbnail, preview]; the gallery's value is the preview URL. The hero follows focus through the strip and is optional |
Troubleshooting
| Symptom | Cause | Fix |
| ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| "Invalid hook call", or a context is missing | The app resolves two copies of React | Add resolve: { dedupe: ['react', 'react-dom'] } to the Vite config |
| Your own base styles (fonts, headings, resets) get overridden | The prebuilt index.css is loaded next to your own Tailwind build, and its preflight wins | Load only one of the two |
| "Duplicate identifier 'Card'", or the wrong Card renders | @swisper.ai/ui exports Card, CardHeader, CardTitle, CardContent and CardFooter too | Alias one import: import { Card as UiCard } from '@swisper.ai/ui' |
| A card ignores the reading column and spans its parent | No ancestor sets --chat-window-width / --message-list-content-width | Render it inside ChatContainer, or set both variables on an ancestor |
| "Duplicate identifier 'List'", or the wrong ListItem renders | @swisper.ai/ui exports List, ListItem, ListItemTitle, ListItemDescription and ListItemAction too | Alias one import: import { List as UIList } from '@swisper.ai/ui' |
| A button in a row also selects the row, or can't be clicked | It isn't wrapped in ListItemAction, so it sits under the row's overlay button | Wrap it: <ListItemAction><SecondaryButton size="sm" … /></ListItemAction> |
| variant or fullWidth is a type error on PrimaryButton, its siblings or IconButton | The card buttons fix those | Use Button or IconButton from @swisper.ai/ui for that button |
| "Duplicate identifier 'IconButton'", or the wrong IconButton renders | @swisper.ai/ui exports IconButton too | Alias one import: import { IconButton as UIIconButton } from '@swisper.ai/ui' |
| "Duplicate identifier 'Carousel'", or the wrong CarouselItem renders | @swisper.ai/ui exports Carousel and CarouselItem too | Alias one import: import { Carousel as UICarousel } from '@swisper.ai/ui' |
| "Duplicate identifier 'Link'", or the wrong Link renders | @swisper.ai/ui exports Link too | Alias one import: import { Link as UILink } from '@swisper.ai/ui' |
| "Duplicate identifier 'Typography'", or a variant or size that @swisper.ai/ui accepts is a type error | @swisper.ai/ui exports Typography, Chip, Avatar, MetaList, Tooltip, ScrollArea, VisuallyHidden, Banner, EmptyState, Skeleton and Spinner too, with more variants and with parts | In a card, import them from @swisper.ai/cards-ui only. Outside a card, or for a value the card leaves out, alias the other one: import { Typography as UITypography } from '@swisper.ai/ui' |
| Tooltip on a CardActionButton throws | No TooltipProvider above the card | Mount TooltipProvider from @swisper.ai/ui at the app root |
| CardFullScreenButton does nothing; onFullScreenChange(false) fires | The browser refused: no user gesture, an iframe without allow="fullscreen", or iPhone Safari | Trigger it from a click, and add allow="fullscreen" to the host iframe |
