# yaml-language-server: $schema=https://opencomponents.dev/schemas/contract.json
# The Card contract, with every rule from its checklist.
# The page explains why each one exists: https://opencomponents.dev/raw/docs/components/card.md

component: Card
summary: Groups what people need to know about one thing, like a project or a product, under a heading. With an href, its heading is a link stretched over the whole card.
standard: https://opencomponents.dev/docs/components/card
element: article # or section, div or li, with as
role: article # listitem as an li. A section is a region only once it has a name
props:
  as: { type: [article, section, div, li], default: article }
  heading: { type: string } # the heading slot takes precedence
  headingLevel: { type: [2, 3, 4, 5, 6], default: 3 }
  href: { type: string } # links the card through its heading
slots:
  default: The body, like a sentence or two and some details.
  heading: The heading, when it's more than plain text, or a router's link, which is stretched too.
  eyebrow: A short line shown above the heading, like a category, which comes after it in the DOM.
  media: An image, shown at the top, which comes after the heading in the DOM.
  actions: Buttons that act on the card, like Save, which sit above the stretched link.
  footer: A last line of details about the card, like who made it or when.
states:
  hover: ":has(> [data-slot=header] > [data-slot=heading] a[href]:hover), in @media (hover: hover)"
  focus: "[data-slot=heading] a[href]:focus-visible, drawn around the card by its ::after"
keyboard:
  Tab: Moves focus to the card's link, then to its actions.
  Enter: Follows the link.
parts: [header, heading, link, eyebrow, media, body, actions, footer] # data-slot, in this order. The link is in the heading
tokens:
  theme: ["--color--neutral-text", "--color--focus"]
  variables: [--card--padding, --card--gap, --card--radius, --card--border-color, --card--background, --card--media--aspect-ratio]
  layer: components # in Tailwind's order: theme, base, components, utilities
rules:
  - id: card/contrast
    layer: ui
    level: must
    scope: component
    requirement: "The text, the eyebrow and the footer included, has a contrast of 4.5:1 against the card, and the focus ring has 3:1 against the page"
    basis: "WCAG 1.4.3, 1.4.11 (AA)"
    check: "axe `color-contrast`, contrast checker"
  - id: card/text-below-media
    layer: ui
    level: should
    scope: component
    requirement: "Text sits below the media rather than on top of it, so its contrast doesn't depend on the image"
    basis: "Open Components"
    check: "Review"
  - id: card/focus-visible
    layer: ui
    level: must
    scope: component
    requirement: "When its link has keyboard focus, an `outline` at least 2px thick shows around the whole card, outside its border"
    basis: "WCAG 2.4.7 (AA), 2.4.13 (AAA)"
    check: "Keyboard"
  - id: card/resizable
    layer: ui
    level: must
    scope: component
    requirement: "Nothing in the card has a fixed height, so it grows with its text, and long words wrap instead of overflowing it"
    basis: "Open Components, beyond WCAG 1.4.4, 1.4.12 (AA)"
    check: "200% zoom"
  - id: card/stable-size
    layer: ui
    level: must
    scope: component
    requirement: "No state changes the card's size or position"
    basis: "Open Components"
    check: "Visual regression test"
  - id: card/media-ratio
    layer: ui
    level: should
    scope: component
    requirement: "The media keeps its space with an aspect ratio, so cards line up and the page doesn't jump as images load"
    basis: "Open Components"
    check: "Visual regression test"
  - id: card/state-selectors
    layer: ui
    level: should
    scope: component
    requirement: "Hover and focus are styled from the link, so the card only looks interactive where pressing it follows the link"
    basis: "Open Components"
    check: "Review"
  - id: card/hover-capable
    layer: ui
    level: should
    scope: component
    requirement: "Hover styles only apply inside `@media (hover: hover)`"
    basis: "Open Components"
    check: "Review"
  - id: card/forced-colors
    layer: ui
    level: must
    scope: component
    requirement: "The border and the focus ring stay visible in forced colours mode"
    basis: "Open Components"
    check: "Emulation"
  - id: card/more-contrast
    layer: ui
    level: should
    scope: component
    requirement: "The border is drawn at full strength when `prefers-contrast: more` is set"
    basis: "Open Components"
    check: "Emulation"
  - id: card/reduced-motion
    layer: ui
    level: must
    scope: usage
    requirement: "Nothing moves on hover or focus when `prefers-reduced-motion: reduce` is set"
    basis: "WCAG 2.3.3 (AAA)"
    check: "Emulation"
  - id: card/reflow
    layer: ui
    level: must
    scope: usage
    requirement: "Cards in a grid get narrower rather than overflow, so the page never scrolls sideways at 320px wide"
    basis: "WCAG 1.4.10 (AA)"
    check: "Emulation"
  - id: card/quiet-actions
    layer: ui
    level: should
    scope: usage
    requirement: "There are at most two actions, neither of them a `solid` Button"
    basis: "Open Components"
    check: "Review"
  - id: card/semantic-element
    layer: ux
    level: must
    scope: component
    requirement: "It's an `<article>` by default, or a `<section>`, `<div>` or `<li>` with `as`, and never a link or a button"
    basis: "Open Components, beyond WCAG 1.3.1 (A)"
    check: "Unit test"
  - id: card/list-of-cards
    layer: ux
    level: should
    scope: usage
    requirement: "A set of cards is a list, with each card an `<li>`"
    basis: "WCAG 1.3.1 (A)"
    check: "Review"
  - id: card/heading
    layer: ux
    level: must
    scope: both
    requirement: "Every card has a heading that says what it's about, at the level that fits the page's outline"
    basis: "WCAG 1.3.1 (A), 2.4.6 (AA)"
    check: "Unit test, review"
  - id: card/heading-first
    layer: ux
    level: should
    scope: component
    requirement: "The heading comes first in the DOM, and the media and the eyebrow are shown above it with CSS alone"
    basis: "Open Components"
    check: "Unit test"
  - id: card/stretched-link
    layer: ux
    level: must
    scope: component
    requirement: "A card that's a link has one link, in its heading, stretched over the card with a pseudo-element, and never a wrapping `<a>`, `role=\"link\"` or a click handler on the card"
    basis: "Open Components, beyond WCAG 2.1.1, 4.1.2 (A)"
    check: "Unit test"
  - id: card/link-name
    layer: ux
    level: must
    scope: component
    requirement: "The link is named by the heading alone"
    basis: "WCAG 2.4.4 (A)"
    check: "Unit test"
  - id: card/link-purpose
    layer: ux
    level: should
    scope: usage
    requirement: "Headings say where the link goes, differ from card to card, and there's no separate \"Read more\" link"
    basis: "WCAG 2.4.4 (A), 2.4.9 (AAA)"
    check: "Review"
  - id: card/actions-above
    layer: ux
    level: must
    scope: component
    requirement: "Actions and other links in the card sit outside the stretched link and above it, so pressing one runs it"
    basis: "Open Components"
    check: "Unit test, review"
  - id: card/no-nesting
    layer: ux
    level: must
    scope: both
    requirement: "The link holds the heading's text alone, with nothing interactive inside it"
    basis: "HTML"
    check: "Unit test"
  - id: card/keyboard
    layer: ux
    level: must
    scope: component
    requirement: "`Tab` reaches the card's link once and then its actions, and `Enter` follows the link"
    basis: "WCAG 2.1.1, 2.4.3 (A)"
    check: "Unit test"
  - id: card/media-alt
    layer: ux
    level: must
    scope: usage
    requirement: "An image that only illustrates the heading has `alt=\"\"`, and one that shows more has alt text that says what"
    basis: "WCAG 1.1.1 (A)"
    check: "axe `image-alt`, review"
  - id: card/static-media
    layer: ux
    level: must
    scope: usage
    requirement: "The media holds nothing people can focus, since it's shown above the heading but comes after it in the DOM"
    basis: "WCAG 1.3.2, 2.4.3 (A)"
    check: "Keyboard"
  - id: card/dom-order
    layer: ux
    level: must
    scope: usage
    requirement: "Cards come in the same order in the DOM as on screen, without `order`, `row-reverse` or a `dense` grid"
    basis: "Open Components, beyond WCAG 1.3.2, 2.4.3 (A)"
    check: "Keyboard"
  - id: card/focus-not-obscured
    layer: ux
    level: must
    scope: usage
    requirement: "Sticky headers and footers never cover a focused card, for example thanks to `scroll-padding`"
    basis: "WCAG 2.4.11 (AA), 2.4.12 (AAA)"
    check: "Keyboard"
  - id: card/no-truncation
    layer: ux
    level: should
    scope: usage
    requirement: "Headings aren't cut off with an ellipsis or a line clamp"
    basis: "Open Components"
    check: "Review"
  - id: card/announce-results
    layer: ux
    level: should
    scope: usage
    requirement: "Cards that load after something people did, like a search, are announced with a status message"
    basis: "WCAG 4.1.3 (AA)"
    check: "Screen reader"
  - id: card/mirrors-in-rtl
    layer: ux
    level: should
    scope: component
    requirement: "It uses logical properties, so it mirrors itself in right-to-left layouts"
    basis: "Open Components"
    check: "Review"
  - id: card/shared-vocabulary
    layer: dx
    level: must
    scope: component
    requirement: "It takes `href` like the Button and `actions` like the Input, and names its other slots after its parts: `heading`, `eyebrow`, `media` and `footer`"
    basis: "Open Components"
    check: "Review"
  - id: card/no-attribute-names
    layer: dx
    level: should
    scope: component
    requirement: "No prop shares its name with an HTML attribute that does something else, like `title`"
    basis: "Open Components"
    check: "Review"
  - id: card/typed
    layer: dx
    level: must
    scope: component
    requirement: "Props and slots are typed, with unions rather than `string` or `number`, so `headingLevel` only takes 2 to 6"
    basis: "Open Components"
    check: "Type check"
  - id: card/root-element
    layer: dx
    level: must
    scope: component
    requirement: "The root element is the card itself, so attributes, listeners and refs land on it"
    basis: "Open Components"
    check: "Unit test"
  - id: card/router-links
    layer: dx
    level: should
    scope: component
    requirement: "A link in the `heading` slot, like a router's, is stretched over the card like the one `href` renders"
    basis: "Open Components"
    check: "Unit test, review"
  - id: card/stateless
    layer: dx
    level: should
    scope: component
    requirement: "It keeps no state of its own"
    basis: "Open Components"
    check: "Review"
  - id: card/role-and-name
    layer: ax
    level: must
    scope: component
    requirement: "It can be found by its heading, with `getByRole(\"heading\", { name })`, and a card that's a link by `getByRole(\"link\", { name })`"
    basis: "WCAG 4.1.2 (A)"
    check: "Unit test"
  - id: card/deterministic-dom
    layer: ax
    level: must
    scope: component
    requirement: "The same props render the same markup, with no generated IDs, and parts only render when they're given"
    basis: "Open Components"
    check: "Unit test"
  - id: card/data-attributes
    layer: ax
    level: should
    scope: component
    requirement: "Parts are marked with `data-slot`"
    basis: "Open Components"
    check: "Unit test"
