15 KiB
Team Grid Block Design
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build a new team-grid ACF block for the Team page (and any other page that needs a team grid) that renders a responsive grid of team members. Each member shows their photo (rounded rectangle), name, and title. The bio is collapsed by default and expands vertically with a smooth animation when the photo, name, or title is clicked.
Architecture: One new ACF block (acf/team-grid) with a single repeater of members (image, name, title, bio). The block uses native HTML <details>/<summary> for the click-to-expand interaction, animated with the grid-template-rows: 0fr → 1fr CSS pattern (no JavaScript). The block has no built-in background — the editor wraps it in an existing Section block when a dark/light background is needed. The page template is unchanged; the editor adds the block to the Team page in Gutenberg.
Tech Stack: WordPress 6.x, PHP 8.x, Tailwind CSS v4, Playwright, axe-core, PHPCS (WordPress standard).
Global Constraints
- Tabs for PHP indentation (project standard).
static/dist/theme.cssis committed (not gitignored) per project convention.- PHPCS uses the WordPress coding standard;
composer lintmust pass for all touched files. - All design colors use the project's existing CSS custom properties from
styles/base/colors.css. No hardcoded hex values for theme colors. - Reuse existing design tokens where possible (e.g. the
text-cwc-blue-01heading color,var(--color-secondary)accent if needed). - The headshot uses
rounded-mdto match other image cards in the project (post-list,services-list). - All commits follow the project's emoji-prefixed commit-message style (e.g.
✨ feat: …). - This work does not edit
header.php,footer.php,views/partials/page-hero.php, orviews/partials/page-hero-services.php. page.phpis not modified.- No JavaScript files are added. The
<details>/<summary>interaction is fully native. - Reuse the same
regACFBlocks()auto-registration pattern asservices-list.
Reference Mockup
team-example.png shows a dark-blue section with 8 team members in a 2-column grid. Each member has a circular headshot, name (bold), title (italic), and a multi-line bio below. All bios are visible in the mockup.
Intent of the design (per the user):
- The bio should be a
<details>element that opens vertically when the photo, name, or title is clicked. The mockup shows the open state for every member for context; the live behavior is collapsed by default. - The headshot should be a rectangle with rounded corners that match other images in the project —
rounded-mdis the project standard. The mockup's circular headshots are a visual approximation; the live design uses rounded rectangles. - The block has no built-in background. The editor wraps it in a Section block (
acf/section) when a dark or light background is needed. - Layout is responsive: 1 column on mobile, 2 on tablet, 3 on desktop (the user's "3-2-1" choice).
File Structure
| File | Responsibility | Created/Modified |
|---|---|---|
acf/group_team_grid.json |
ACF field group for the acf/team-grid block. |
Create |
views/blocks/team-grid/team-grid.php |
Block template (PHP) — renders the grid. | Create |
views/blocks/team-grid/team-grid.css |
Block styles (CSS) — layout, photo, summary, animation. | Create |
views/blocks/team-grid/block.json |
Block registration metadata. | Create |
styles/blocks/index.css |
Import the new block's CSS. | Modify |
tests/team-page.spec.js |
Playwright tests for the rendered block. | Create |
static/dist/theme.css |
Rebuilt via npm run build. |
Modified |
Design Details
1. ACF field group: acf/team-grid
Block name (registration key): acf/team-grid
Editor title: "Team Grid"
Category: common
Description (in block.json): "A responsive grid of team members. Click a card to expand the bio."
ACF field group (acf/group_team_grid.json)
Group key: group_team_grid
Group title: Team Grid
Position: normal
Menu order: 0
Location: block == acf/team-grid
Fields:
members(repeater, layout: block, button label: "Add Team Member")image(image, return_format: array, required: 0) — Headshot.name(text, required: 0) — Person's name (e.g. "Christopher Werner").title(text, required: 0) — Person's role (e.g. "Principal Consultant & CEO").bio(wysiwyg, tabs: visual, toolbar: basic, media_upload: 0, required: 0) — Bio body copy.
The block is intentionally minimal. No per-card size, no global layout toggle, no CTA field, no group-level background color. Layout derives from CSS (grid-cols-1 sm:grid-cols-2 lg:grid-cols-3).
Block registration (views/blocks/team-grid/block.json)
{
"name": "acf/team-grid",
"title": "Team Grid",
"description": "A responsive grid of team members. Click a card to expand the bio.",
"category": "common",
"icon": "groups",
"keywords": ["team", "people", "staff", "bio"],
"acf": {
"mode": "preview",
"renderTemplate": "team-grid.php"
},
"supports": {
"anchor": true,
"align": false
}
}
The icon: "groups" is a WordPress Dashicon that visually represents a group of people.
Block template (views/blocks/team-grid/team-grid.php)
<?php
/**
* Block Name: Team Grid
*
* A responsive grid of team members. Each card uses a native <details>
* element so the bio expands when the photo, name, or title is clicked.
*
* @package CWC
*/
namespace CWC;
$members = get_field( 'members' );
if ( empty( $members ) ) {
return;
}
$classes = 'team-grid container my-section mx-auto';
$wrapper = blockWrapperAttributes( $classes, $is_preview );
?>
<section <?php echo wp_kses_post( $wrapper ); ?>>
<div class="team-grid__inner grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-12">
<?php foreach ( $members as $member ) : ?>
<article class="team-grid__card">
<details class="team-grid__details">
<summary class="team-grid__summary">
<?php if ( ! empty( $member['image'] ) ) : ?>
<img
class="team-grid__photo"
src="<?php echo esc_url( $member['image']['url'] ); ?>"
alt="<?php echo esc_attr( $member['image']['alt'] ?? '' ); ?>"
loading="lazy"
/>
<?php endif; ?>
<?php if ( ! empty( $member['name'] ) ) : ?>
<h3 class="team-grid__name">
<?php echo wp_kses_post( $member['name'] ); ?>
</h3>
<?php endif; ?>
<?php if ( ! empty( $member['title'] ) ) : ?>
<p class="team-grid__title">
<?php echo wp_kses_post( $member['title'] ); ?>
</p>
<?php endif; ?>
</summary>
<div class="team-grid__bio-wrap">
<div class="team-grid__bio-inner">
<?php if ( ! empty( $member['bio'] ) ) : ?>
<div class="team-grid__bio">
<?php echo wp_kses_post( $member['bio'] ); ?>
</div>
<?php endif; ?>
</div>
</div>
</details>
</article>
<?php endforeach; ?>
</div>
</section>
Notes:
- Native
<details>/<summary>: the summary is the click target (photo + name + title); the body is the bio wrap. - The
<details>element is closed by default — noopenattribute. The user opens it on click. - The bio uses
wysiwygso editors can add links or basic formatting. Output is sanitized viawp_kses_post. team-grid__bio-wrapis a 1-row grid container that animatesgrid-template-rowsfrom0frto1fr. The__bio-innerinside hasoverflow: hidden; min-height: 0so the bio is clipped during the transition.
Block styles (views/blocks/team-grid/team-grid.css)
/* Team Grid block */
.team-grid {
&__card {
@apply flex flex-col;
}
&__details {
@apply flex flex-col;
}
/* summary is the click target. Strip the default disclosure triangle. */
&__summary {
@apply flex flex-col items-center text-center cursor-pointer list-none;
&::-webkit-details-marker {
display: none;
}
&::marker {
display: none;
content: '';
}
}
&__photo {
@apply w-32 h-32 sm:w-40 sm:h-40 lg:w-48 lg:h-48 object-cover rounded-md mb-4;
}
&__name {
@apply text-cwc-blue-01 text-22px font-bold leading-tight mb-1;
}
&__title {
@apply text-gray-600 text-16px font-light italic mb-3;
}
/* Animation: animate grid-template-rows 0fr -> 1fr. */
&__bio-wrap {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 300ms ease;
}
&__details[open] &__bio-wrap {
grid-template-rows: 1fr;
}
&__bio-inner {
overflow: hidden;
min-height: 0;
}
&__bio {
@apply text-gray-700 text-16px font-light leading-snug pt-2;
}
}
Notes:
- The summary is a flex column with
items-centerso the photo, name, and title stack centered. This matches the mockup. - The default
<summary>marker (▶ triangle) is hidden withlist-style: noneplus vendor-prefixed::-webkit-details-markerand::markerreset. - The
&__details[open] &__bio-wrapselector uses CSS nesting to target the bio wrap when the parent details is open. This compiles to.team-grid__details[open] .team-grid__bio-wrap { grid-template-rows: 1fr; }. - The
team-grid__details[open] &__bio-wrapsyntax uses CSS nesting:&inside&__details[open]is the previous compound selector. Tailwind v4 (lightningcss) unwraps this correctly. - Photo sizes:
w-32 h-32(8rem / 128px) on mobile,w-40 h-40(10rem / 160px) on small,w-48 h-48(12rem / 192px) on large.
2. CSS import wiring (styles/blocks/index.css)
Append at the end of styles/blocks/index.css:
@import '../../views/blocks/team-grid/team-grid.css';
After the edit, the file ends with:
@import '../../views/blocks/services-list/services-list.css';
@import '../../views/blocks/team-grid/team-grid.css';
3. Page integration
page.php is unchanged. The editor adds a Team Grid block to the Team page (/team/) in Gutenberg. To get the dark-blue background, the editor wraps the Team Grid block in a Section block (acf/section) and sets the Section's background_color to the dark blue.
No PHP template change is required.
4. CSS Build
Run npm run build to compile Tailwind v4 → static/dist/theme.css. The new utilities (e.g. text-22px, text-16px, w-32 h-32, rounded-md) must appear in the dist.
5. Tests (tests/team-page.spec.js)
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test.describe("Team Grid block on /team/", () => {
test("renders the photo, name, and title for each member", async ({ page }) => {
await page.goto("/team/");
const cards = page.locator(".team-grid__card");
await expect(cards.first()).toBeVisible();
const firstCard = cards.first();
await expect(firstCard.locator(".team-grid__photo")).toBeVisible();
await expect(firstCard.locator(".team-grid__name")).toBeVisible();
await expect(firstCard.locator(".team-grid__title")).toBeVisible();
});
test("bio is hidden by default and expands on summary click", async ({ page }) => {
await page.goto("/team/");
const firstDetails = page.locator(".team-grid__details").first();
// Bio is closed by default.
await expect(firstDetails).not.toHaveAttribute("open", "");
// Click the summary; the details should open and the bio should be visible.
await firstDetails.locator(".team-grid__summary").click();
await expect(firstDetails).toHaveAttribute("open", "");
await expect(firstDetails.locator(".team-grid__bio")).toBeVisible();
// Click again; the details should close.
await firstDetails.locator(".team-grid__summary").click();
await expect(firstDetails).not.toHaveAttribute("open", "");
});
test("no axe violations on the team-grid block", async ({ page }) => {
await page.goto("/team/");
const results = await new AxeBuilder({ page })
.include(".team-grid")
.analyze();
expect(results.violations).toEqual([]);
});
});
Out of Scope
- The Team page template —
page.phpis unchanged. The editor adds the block in Gutenberg. - The Section block wrapper for the dark-blue background — the editor composes this in Gutenberg.
- A custom open/close icon or arrow indicator — the click target is the entire summary (photo + name + title), so no chevron is needed.
- Animations beyond the bio's expand/collapse transition. No hover transforms, no entry animations.
- Per-card size variants — all cards are the same size.
- A
data-team-member-idslug for deep-linking to a specific member's expanded bio. - Dark-mode overrides — the block is color-agnostic and inherits the parent Section's color scheme.
- Per-page header/footer changes.
Self-Review Notes
- Spec coverage: One new ACF block (4 files), one CSS import, dist rebuild, one test file. Every spec section is covered.
- Placeholder scan: No TBDs. Every step has a concrete file path, exact content, exact command, and expected output.
- Type consistency: Class names use the existing BEM convention (
__card,__details,__summary,__photo,__name,__title,__bio-wrap,__bio-inner,__bio). Field names (members,image,name,title,bio) are used in the JSON, the PHP, and the tests. - Scope check: Single focused build, no decomposition needed. Block is small (≤4 files), testable in isolation, and composable on the Team page (and any other page that needs a team grid).
- Ambiguity check: The "rounded rectangle" headshot is concretely specified as
rounded-md. The "open vertically" animation is concretely specified asgrid-template-rows: 0fr → 1frwith a 300ms transition. The "3-2-1" responsive grid is concretely specified. The "no background" decision is concretely scoped to the Section block wrapper (editor's responsibility, not this block's).