Files
CWC/docs/superpowers/specs/2026-07-25-team-grid-block-design.md
T

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.css is committed (not gitignored) per project convention.
  • PHPCS uses the WordPress coding standard; composer lint must 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-01 heading color, var(--color-secondary) accent if needed).
  • The headshot uses rounded-md to 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, or views/partials/page-hero-services.php.
  • page.php is not modified.
  • No JavaScript files are added. The <details>/<summary> interaction is fully native.
  • Reuse the same regACFBlocks() auto-registration pattern as services-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 projectrounded-md is 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 — no open attribute. The user opens it on click.
  • The bio uses wysiwyg so editors can add links or basic formatting. Output is sanitized via wp_kses_post.
  • team-grid__bio-wrap is a 1-row grid container that animates grid-template-rows from 0fr to 1fr. The __bio-inner inside has overflow: hidden; min-height: 0 so 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-center so the photo, name, and title stack centered. This matches the mockup.
  • The default <summary> marker (▶ triangle) is hidden with list-style: none plus vendor-prefixed ::-webkit-details-marker and ::marker reset.
  • The &__details[open] &__bio-wrap selector 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-wrap syntax 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.php is 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-id slug 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 as grid-template-rows: 0fr → 1fr with 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).