<div class="bux-card bux-card--gray" role="group" aria-roledescription="Card" aria-label="Card Heading">
<img class="bux-image" src="https://s3.amazonaws.com/bux.osu.edu/img/example-images/oxley_9599.jpg" alt="Three young people lay hands on the face of Thompson Library bust" />
<div class="bux-card__content">
<h3 class="bux-card__heading"><span>Card Heading</span>
<span class="bux-card__heading-icon" aria-hidden="true"></span>
</h3>
<div class="bux-card__body">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
</div>
<div class="bux-card__cta">
<a class="bux-card__link" href="#" rel="noopener">
<span class="bux-card__link__text">Call to Action</span>
</a>
</div>
</div>
</div>
{#
Card
Available variables:
- modifier: Sets the card variant. Allowed values: null, gray,
no-border, linked-headline, storytelling, featured,
horizontal, horizontal-storytelling
- card_heading_level: Integer. Sets the heading level of the card heading.
- card_image: Boolean. Set to true to display image.
- image_url: URL for the card image.
- image_alt_text: Alt text for the card image.
- card_heading: String for the card heading.
- card_name: String for the type of card for aria-label. Default: "Card".
- card_body: Content of the card body. Optional.
- card_url: URL for the card.
- card_cta: String for the card CTA.
- taxonomy: String for the storytelling card taxonomy.
- read_time: String for the storytelling card read time.
#}
{# --- Defaults & Checks --- #}
{%- set heading_level = card_heading_level|default(2) -%}
{%- set aria_role = card_name|default("Card") -%}
{%- set is_event = modifier in ['event', 'event-horizontal'] -%}
{%- set is_storytelling = modifier in ['storytelling', 'horizontal-storytelling'] -%}
{%- set is_horizontal = modifier in ['horizontal', 'horizontal-storytelling', 'event-horizontal'] -%}
{%- set is_featured = modifier == 'featured' -%}
{%- set is_linked = modifier == 'linked-headline' or is_event or is_storytelling -%}
{# --- Date Markup --- #}
{%- set date_markup -%}
{%- if event -%}
<div class="bux-card--event__date--wrapper">
<div class="bux-card--event__date">
<div class="bux-card--event__date--month">{{ event.date.month }}</div>
<div class="bux-card--event__date--day">{{ event.date.day }}</div>
</div>
{%- if event.date_end -%}
<div class="bux-card--event__date-divider">to</div>
<div class="bux-card--event__date">
<div class="bux-card--event__date--month">{{ event.date_end.month }}</div>
<div class="bux-card--event__date--day">{{ event.date_end.day }}</div>
</div>
{%- endif -%}
</div>
{%- endif -%}
{%- endset -%}
{# --- Card Header --- #}
{%- set card_header -%}
<h{{ heading_level }} class="bux-card__heading">
{%- if is_linked -%}
{%- set heading_link_class = is_storytelling ? 'bux-card__heading--storytelling-link' : 'bux-card__heading--link' -%}
{% embed '@link' with {
base_class: 'bux-card__link ' ~ heading_link_class,
link_url: card_url,
card_heading: card_heading
} only %}
{% block content %}
<span>{{ card_heading }}</span>
<span class="bux-card__heading-icon" aria-hidden="true"></span>
{% endblock %}
{% endembed %}
{%- else -%}
<span>{{ card_heading }}</span>
<span class="bux-card__heading-icon" aria-hidden="true"></span>
{%- endif -%}
</h{{ heading_level }}>
{%- endset -%}
{# --- Card Footer --- #}
{%- set card_footer -%}
{%- if is_linked and is_storytelling -%}
<span class="bux-card__read-time">{{ read_time }}</span>
{%- elseif not is_linked and card_cta -%}
{%- if is_featured -%}
{% include '@link' with {
base_class: 'bux-button bux-button--alt',
link_url: card_url,
link_text: card_cta,
show_underline: false
} only %}
{%- else -%}
<div class="bux-card__cta">
{% include '@link' with {
base_class: 'bux-card__link',
link_url: card_url,
link_text: card_cta,
show_underline: false
} only %}
</div>
{%- endif -%}
{%- endif -%}
{%- endset -%}
<div
class="bux-card{% if modifier %} bux-card--{{ modifier }}{% endif %}"
role="group"
aria-roledescription="{{ aria_role }}"
{% if card_heading %}aria-label="{{ card_heading }}"{% endif %}
>
{% if card_image %}
{% if event %}
<div class="bux-card__image">
{{ date_markup }}
{% endif %}
{% if is_horizontal %}
<div class="bux-card__image-wrapper">
{% endif %}
{% include '@image' %}
{% if is_horizontal %}
</div>
{% endif %}
{% if event %}
</div>
{% endif %}
{% elseif event %}
{{ date_markup }}
{% endif %}
<div class="bux-card__content">
{% if taxonomy %}
<span class="bux-card__taxonomy">{{ taxonomy }}</span>
{% endif %}
{{ card_header }}
{% if card_body %}
<div class="bux-card__body">
{{ card_body }}
</div>
{% endif %}
{% if is_event %}
<div class="bux-mar-top-sp-16">
{% include '@list-description-icons' with list_items %}
</div>
{% endif %}
{{ card_footer }}
</div>
</div>
site_name_prefix: Office of
site_name: Learning Relations Excellence
site_slogan: Additional text or site slogan
address_1: 100 Building Name
address_2: 1 Oval Mall
city: Columbus
state: OH
zip: '43210'
contact_email: email@osu.edu
contact_phone: 614-292-OHIO
contact_tty: 614-688-8605
image_url: https://s3.amazonaws.com/bux.osu.edu/img/example-images/oxley_9599.jpg
image_alt_text: Three young people lay hands on the face of Thompson Library bust
card_heading_level: 3
card_heading: Card Heading
card_body: >-
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.
card_url: '#'
card_cta: Call to Action
card_image: true
modifier: gray
.bux-grid.bux-card-collection {
align-items: stretch;
.bux-card {
height: 100%;
}
}
.bux-card {
border-top: 4px solid $scarlet;
color: $gray-dark-80;
position: relative;
display: flex;
flex-direction: column;
overflow: clip;
cursor: pointer;
}
.bux-card {
.bux-image {
align-self: flex-start;
object-fit: cover;
aspect-ratio: 16/9;
transition: transform 0.4s ease-in-out;
}
&:hover .bux-image {
transform: scale(1.05);
}
}
.bux-card__content {
padding: $sp-24;
background-color: $white;
border: 2px solid $gray-light-90;
border-top: none;
position: relative;
z-index: 2;
pointer-events: none;
flex: 1;
}
.bux-card--gray,
.bux-card--gray .bux-card__content {
background-color: $gray-light-90;
}
.bux-card--no-border,
.bux-card--storytelling,
.bux-card--horizontal,
.bux-card--horizontal-storytelling {
.bux-card__content {
border: 0;
padding: 0;
padding-top: $sp-24;
padding-right: $sp-24;
}
}
.bux-card--storytelling,
.bux-card--horizontal-storytelling {
border-top: none;
.bux-card__heading {
font-family: $serif;
font-size: $ts-md;
font-weight: 700;
}
}
.bux-card--horizontal-storytelling {
border-bottom: 2px solid $gray-light-60;
padding-bottom: $sp-32;
.bux-card__taxonomy {
margin-bottom: 3px;
}
.bux-card__heading {
margin-top: 5px;
}
}
.bux-card--event-horizontal {
border-top: none;
.bux-card__content {
border-left: 0;
border-right: 0;
padding: $sp-24 0;
}
&:last-child {
border-bottom: none;
}
}
.bux-card__read-time {
font-size: $ts-base;
color: $gray-dark-40;
margin-top: $sp-8;
&::before {
content: "\f302";
font-family: $icon;
font-size: $ts-sm;
margin-right: 6px;
margin-top: 1.5px;
float: left;
}
}
*.bux-card__heading {
font-size: $ts-22;
line-height: rem-calc(28);
font-weight: 700;
margin-bottom: $sp-8;
}
.bux-card__body {
font-size: $ts-18;
line-height: rem-calc(28);
font-weight: 400;
margin-top: $sp-8;
margin-bottom: $sp-16;
}
.bux-card__cta {
margin-top: $sp-8;
position: relative;
z-index: 2;
a {
color: $gray-dark-80;
pointer-events: all;
text-decoration: none;
display: table;
&::after {
content: "\f005";
font-size: $ts-sm;
font-family: $icon;
font-weight: 600;
color: $scarlet;
padding-left: $sp-8;
transition-duration: 0.05s;
display: table-cell;
vertical-align: middle;
}
&:hover {
color: $scarlet;
&::after {
padding-left: $sp-16;
transition-duration: 0.05s;
}
}
&:focus {
outline: 2px solid $focus;
outline-offset: $sp-4;
}
}
}
.bux-card--linked-headline,
.bux-card--storytelling,
.bux-card--horizontal-storytelling {
.bux-card__body {
margin-bottom: $sp-8;
}
}
.bux-card--linked-headline {
.bux-card__heading {
display: inline-block;
margin-bottom: 0;
width: 100%;
}
}
.bux-card__heading--link {
text-decoration: none;
color: $gray-dark-80;
pointer-events: all;
display: flex;
justify-content: space-between;
align-items: center;
padding-right: $sp-8;
transition-duration: 0.05s;
.bux-card__heading-icon {
display: flex;
align-items: center;
&:before {
content: "\f005";
font-size: $ts-md;
font-family: $icon;
color: $scarlet;
}
}
&:focus {
outline: 2px solid $focus;
}
}
.bux-card__heading--storytelling-link {
text-decoration: none;
color: $gray-dark-80;
pointer-events: all;
&:focus {
outline: 2px solid $focus;
}
}
.bux-card:hover {
.bux-card__heading--link {
color: $scarlet;
text-decoration: none;
padding-right: 0;
}
}
.bux-card--horizontal-storytelling:hover .bux-card__heading span {
text-decoration: none;
box-shadow: none;
text-shadow: none;
color: $gray-dark-80;
}
.bux-card--storytelling:hover .bux-card__heading span,
.bux-card--horizontal-storytelling:hover .bux-card__heading span {
color: $gray-dark-80;
text-decoration: none;
}
.bux-card--storytelling .bux-card__heading span:hover,
.bux-card--horizontal-storytelling .bux-card__heading span:hover {
color: $gray-dark-80;
}
.bux-card__taxonomy {
color: $scarlet;
text-transform: uppercase;
font-size: $ts-sm;
line-height: rem-calc(18);
margin-bottom: $sp-8;
display: inline-block;
}
.bux-card--featured {
display: block;
background-color: $white;
max-width: 100%;
margin-right: 0;
.bux-card__content {
padding: $sp-32;
}
.bux-button {
text-decoration: none;
display: inline-block;
}
&:hover .bux-button {
background-color: $gray-dark-60;
color: $white;
border-color: $gray-dark-60;
}
}
.bux-card__image {
position: relative;
}
.bux-card--event,
.bux-card--event-horizontal {
background-color: $white;
&__date {
background-color: $gray-light-90;
max-height: fit-content;
max-width: fit-content;
text-align: center;
color: $scarlet;
font-weight: 700;
font-size: $ts-base;
text-transform: uppercase;
padding: $sp-8 $sp-32;
line-height: 1.3;
&--month {
font-size: $ts-base;
}
&--day {
font-size: $ts-28;
}
&-divider {
background-color: $white;
padding: $sp-8;
}
}
.bux-card--event__date--wrapper {
display: flex;
align-items: center;
&:has(.bux-card--event__date-divider) .bux-card--event__date {
padding: $sp-8 $sp-24;
}
}
.bux-card__image {
.bux-card--event__date--wrapper {
position: absolute;
bottom: 0;
z-index: 10;
}
}
.bux-card__taxonomy {
color: $gray-dark-40;
}
.bux-icon-dl__item {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-bottom: $sp-16;
&:last-child {
margin-bottom: 0;
}
dd {
margin: 0;
margin-left: $sp-8;
}
}
}
@include breakpoint(md) {
.bux-card--horizontal,
.bux-card--horizontal-storytelling {
border-top: none;
flex-direction: row;
max-width: 100%;
.bux-card__heading {
font-size: $ts-md;
}
.bux-image {
max-width: 33%;
margin-right: $sp-32;
object-fit: cover;
}
.bux-card__content {
padding-top: 0;
}
}
.bux-card--event-horizontal {
border-top: none;
flex-direction: row;
max-width: 100%;
align-items: flex-start;
border-bottom: 2px solid $gray-light-80;
.bux-card__image {
display: contents;
.bux-card--event__date {
position: relative;
max-width: 100%;
}
.bux-card--event__date--wrapper {
position: relative;
flex-direction: column;
align-items: stretch;
padding-bottom: $sp-32;
}
.bux-card--event__date-divider {
text-align: center;
}
.bux-image {
order: 3;
flex: 1;
margin-bottom: $sp-24;
max-width: 25%;
}
}
.bux-card__content {
border: none;
align-self: stretch;
padding: 0 $sp-24 $sp-24;
}
}
.bux-card--horizontal-storytelling {
.bux-card__content {
padding-left: 0;
border: 0;
}
.bux-card__body {
margin-bottom: $sp-8;
}
}
.bux-card--horizontal,
.bux-card--horizontal-storytelling {
.bux-card__image-wrapper {
align-self: flex-start;
aspect-ratio: 16/9;
margin-right: $sp-32;
max-width: 33%;
overflow: hidden;
.bux-image {
max-width: 100%;
margin-right: 0;
}
}
}
.bux-card--event-horizontal {
.bux-card__image-wrapper {
@include breakpoint(md) {
order: 3;
flex: 1;
margin-bottom: 24px;
max-width: 25%;
align-self: flex-start;
aspect-ratio: 16/9;
overflow: hidden;
.bux-image {
max-width: 100%;
margin-right: 0;
}
}
}
}
}
{
/**
* This script is intended to make the entire card component clickable while still letting users select text.
* It works by adding a `pointerdown` listener to each card wrapper.
* - When the pointer is pressed down, we start tracking mouse movement and listen for a pointerup.
* - If the user moves the mouse more than the threshold, we assume they are selecting text and do nothing on pointerup.
* - If they don't exceed the threshold, we forward a click event to the internal link on pointerup.
*
* This lets the whole card recieve clicks, but also preserves normal text selection, right/middle clicks, image dragging, etc.
*/
const CARD_SELECTOR = ".bux-card";
const CARD_LINK_SELECTOR = "a.bux-card__link";
const POINTER_MOVE_THRESHOLD = 8; // in pixels
const addCardClickHandling = (card) => {
const link = card.querySelector(CARD_LINK_SELECTOR);
if (!link) return; // Bail if we can't find the link
let deltaX = 0;
let deltaY = 0;
// Prevent the link click from bubbling up to the card and looping
link.addEventListener("click", (e) => {
e.stopPropagation();
});
link.addEventListener("pointerdown", (e) => {
e.stopPropagation();
});
card.addEventListener("click", (e) => {
const exceedsThreshold =
deltaX > POINTER_MOVE_THRESHOLD || deltaY > POINTER_MOVE_THRESHOLD;
if (exceedsThreshold) {
e.preventDefault();
return;
} else {
const forwardedEvent = new PointerEvent("click", e);
link.dispatchEvent(forwardedEvent);
}
});
/**
* @param {PointerEvent} e
*/
card.addEventListener("pointerdown", (_e) => {
/* Resetting so we can track these in the pointermove handler,
then check if we've exceeded the threshold on pointerup */
deltaX = 0;
deltaY = 0;
card.addEventListener("pointermove", onCardPointerMove, {
passive: true,
});
card.addEventListener("pointerup", onCardPointerUp, {
once: true,
});
});
/**
* @param {PointerEvent} e
*/
const onCardPointerMove = (e) => {
deltaX += Math.abs(e.movementX);
deltaY += Math.abs(e.movementY);
};
/**
* @param {PointerEvent} e
*/
const onCardPointerUp = (e) => {
card.removeEventListener("pointermove", onCardPointerMove);
const exceedsThreshold =
deltaX > POINTER_MOVE_THRESHOLD || deltaY > POINTER_MOVE_THRESHOLD;
if (exceedsThreshold) {
/* When the user has moved the pointer more than the threshold, we allow normal browser behavior by not
clicking the link. This allows text selection, right/middle clicks on the image and CTA, image dragging, etc. */
return;
} else {
/* Since this event listener only gets attached from a pointerdown event on the card,
we can safely assume that the pointerup event should forward as a click if the
movement threshold was not exceeded. */
const forwardedEvent = new PointerEvent("click", e);
link.dispatchEvent(forwardedEvent);
}
};
};
Array.from(document.querySelectorAll(CARD_SELECTOR)).forEach(
addCardClickHandling,
);
}
{
/**
* This script is intended to make the entire card component clickable while still letting users select text.
* It works by adding a `pointerdown` listener to each card wrapper.
* - When the pointer is pressed down, we start tracking mouse movement and listen for a pointerup.
* - If the user moves the mouse more than the threshold, we assume they are selecting text and do nothing on pointerup.
* - If they don't exceed the threshold, we forward a click event to the internal link on pointerup.
*
* This lets the whole card receive clicks, but also preserves normal text selection, right/middle clicks, image dragging, etc.
*/
const CARD_SELECTOR = ".bux-card";
const CARD_LINK_SELECTOR = "a.bux-card__link";
const POINTER_MOVE_THRESHOLD = 8; // in pixels
const addCardClickHandling = (card: HTMLElement) => {
const link = card.querySelector(CARD_LINK_SELECTOR);
if (!link) return; // Bail if we can't find the link
let deltaX = 0;
let deltaY = 0;
// Prevent the link click from bubbling up to the card and looping
link.addEventListener("click", (event) => {
event.stopPropagation();
});
link.addEventListener("pointerdown", (event) => {
event.stopPropagation();
});
card.addEventListener("click", (event) => {
const exceedsThreshold =
deltaX > POINTER_MOVE_THRESHOLD || deltaY > POINTER_MOVE_THRESHOLD;
if (exceedsThreshold) {
event.preventDefault();
return;
} else {
const forwardedEvent = new PointerEvent("click", event);
link.dispatchEvent(forwardedEvent);
}
});
/**
* @param {PointerEvent} e
*/
card.addEventListener("pointerdown", () => {
/* Resetting so we can track these in the pointermove handler,
then check if we've exceeded the threshold on pointerup */
deltaX = 0;
deltaY = 0;
card.addEventListener("pointermove", onCardPointerMove, {
passive: true,
});
card.addEventListener("pointerup", onCardPointerUp, {
once: true,
});
});
/**
* @param {PointerEvent} event
*/
const onCardPointerMove = (event: MouseEvent) => {
deltaX += Math.abs(event.movementX);
deltaY += Math.abs(event.movementY);
};
/**
* @param {PointerEvent} event
*/
const onCardPointerUp = (event: MouseEvent) => {
card.removeEventListener("pointermove", onCardPointerMove);
const exceedsThreshold =
deltaX > POINTER_MOVE_THRESHOLD || deltaY > POINTER_MOVE_THRESHOLD;
if (exceedsThreshold) {
/* When the user has moved the pointer more than the threshold, we allow normal browser behavior by not
clicking the link. This allows text selection, right/middle-clicks on the image and CTA, image dragging, etc. */
return;
} else {
/* Since this event listener only gets attached from a pointerdown event on the card,
we can safely assume that the pointerup event should forward as a click if the
movement threshold was not exceeded. */
const forwardedEvent = new PointerEvent("click", event);
link.dispatchEvent(forwardedEvent);
}
};
};
for (const element of document.querySelectorAll(CARD_SELECTOR) as NodeListOf<HTMLElement>) { addCardClickHandling(element); }
}
No notes defined.