Overview#
Counsel is a complete website for a law firm or professional services practice, built as a
WordPress block theme. The fictional firm is Hartwell & Sloane, attorneys at law in Boston,
Hartford and Providence. The theme has a home page with count-up figures, practice areas filtered
by audience, a practice area page (services, process, representative results, FAQ and team), an
attorney directory with name search and a practice filter, an attorney profile, case results
filtered by outcome and practice area with the required disclaimers, insights filtered by topic, a
long-form article with a reading progress bar and a table of contents that follows the scroll, a
contact page with a consultation form and an office map, and a 404 page. It is version 1.0.0 (see
template.json and style.css), works with WordPress 7.1 and PHP 7.4 or newer, and is the
WordPress version of the HTML template of the same name. See it live at
freeframe.dev/templates/counsel.
Everything you see is made of core WordPress blocks arranged in block patterns, so you edit text,
links, buttons, lists and images in the block editor without touching code. Colours, the fonts
and font sizes are design tokens in theme.json: change them in Appearance > Editor > Styles and
the whole site follows.
Quick start#
Requirements: WordPress 7.1 or newer and PHP 7.4 or newer. No plugins are needed.
- Download
counsel.zipfrom freeframe.dev/templates/counsel (the WordPress tab). - In your WordPress admin go to Appearance > Themes > Add New Theme > Upload Theme, choose the zip and click Install Now, then Activate. The theme is listed as Freeframe Counsel.
- Go to Appearance > Counsel setup and click Create the demo pages. This creates the Home, Practice areas, practice area, Attorneys, attorney, Results, Insights, article and Contact pages from the theme's page patterns, sets Home as the front page (Settings > Reading) and creates the header menu. Pages that already exist with the same slug are left alone, so it is safe to click it again.
- In Settings > General, set the Site Title to your firm's name and the Tagline to a short description: the header and footer show the title, and the home page's browser title is "Title — Tagline".
- Review the legal notices (see Legal notices).
You can also try the theme without installing anything: the Try it in WordPress button on freeframe.dev opens it in WordPress Playground (WordPress running in your browser) with the demo pages already set up.
To work on the theme's files locally you need Node.js 24 and pnpm; WordPress itself runs in WordPress Playground, so there is no PHP, web server or database to install:
pnpm install
pnpm dev # builds the CSS, then starts WordPress with the theme at http://127.0.0.1:9400pnpm dev signs you in as admin (password password) and creates the demo pages. The site is
rebuilt from scratch every time you start it; your edits to theme files are live, edits made in the
WordPress admin are not kept.
Pages#
Each page is a page pattern (in Patterns > Counsel pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Counsel.
| Page | Slug | Page pattern | Sections (pattern files in patterns/) |
|---|---|---|---|
| Home | / |
page-home.php |
home-hero (hero and courthouse illustration), firm-numbers (count-up), home-practices, how-we-work, home-attorneys, testimonials, home-insights, consult-cta |
| Practice areas | /practice-areas/ |
page-practice-areas.php |
practice-areas-header, practice-areas-list (audience filter and cards), practice-areas-help |
| Employment Law | /practice-area/ |
page-practice-area.php |
practice-header, practice-services, practice-process, practice-results, practice-faq, practice-team, practice-cta |
| Attorneys | /attorneys/ |
page-attorneys.php |
attorneys-header, attorneys-directory (search, practice filter, cards), attorneys-cta |
| Priya Raman | /attorney/ |
page-attorney.php |
attorney-header, attorney-about (about, experience, credentials), attorney-insights |
| Results | /results/ |
page-results.php |
results-header, results-list (outcome and practice filters, disclaimer), consult-cta |
| Insights | /insights/ |
page-insights.php |
insights-header, insights-list (topic filter and cards), briefing (newsletter) |
| Non-compete agreements are changing | /article/ |
page-article.php |
article (header, table of contents, text, author), keep-reading |
| Contact | /contact/ |
page-contact.php |
contact-header, consultation (form, offices, map) |
| 404 | any missing URL | templates/404.html |
404 |
The practice area, the attorney profile and the article are examples: duplicate their page for
each practice, attorney or article (copy all blocks into a new page), and point the cards'
links at it. WordPress posts use templates/single.html (see
Insights: pages or posts); the blog, archives and search results use
templates/index.html, templates/archive.html and templates/search.html.
Project structure#
freeframe-counsel/
style.css theme header (name, version, requirements, license); no styles
theme.json design tokens: palette, fonts, font sizes, layout widths, button
functions.php loads the files in inc/
inc/setup.php stylesheet and script (with their strings and page URLs), editor
styles, the `js` class script, theme colour, pattern categories
inc/icons.php registers assets/icons/*.svg for the Icon block
inc/icon-labels.php the icons' names shown in the editor
inc/block-styles.php button block styles
inc/site-setup.php Appearance > Counsel setup: creates the demo pages and the menu
templates/ page.html (pages), page-with-title.html, single.html, index.html,
archive.html, search.html, 404.html
parts/ header.html and footer.html (they show the header and footer patterns)
patterns/ every section and page (PHP files with block markup)
assets/css/theme.css the compiled stylesheet (do not edit: built from src/)
assets/css/editor.css the same for the block editor (generated by src/editor-css.mjs)
assets/js/main.js the site's JavaScript
assets/icons/ SVG icons for the Icon block
assets/img/ favicon, social image, courthouse, attorney portraits, article covers
assets/fonts/ IBM Plex Sans and Libre Caslon Display and their licenses (OFL.txt)
src/theme.css the Tailwind CSS source of assets/css/theme.css
src/icons.css icons drawn before or after text with CSS (generated)
src/editor-css.mjs writes assets/css/editor.css after each build
languages/ freeframe-counsel.pot, the template for translations
blueprint.json WordPress Playground setup used by `pnpm dev`
screenshot.png the picture shown in Appearance > Themes
package.json the CSS build and the local WordPress (Node.js tooling)
readme.txt, README.md, DOCS.md, CHANGELOG.md, LICENSE.txtCustomising#
Brand and colours#
The colours are in theme.json (settings.color.palette) and in Appearance > Editor > Styles >
Colors > Palette:
| Colour | Used for |
|---|---|
| Contrast (ink), Ink 2 | text, the navy sections and the primary button; raised panels on navy |
| Base (paper), Paper 2, Surface | the ivory page background, sunken panels, cards |
| Line, Line dark | hairlines on ivory and on navy |
| Muted, Mist | secondary text on ivory and on navy |
| Gold, Gold ink, Gold soft | brass rules, icons and borders; brass text on light backgrounds (AA); brass text and buttons on navy |
| Danger | form errors |
The theme's stylesheet reads every colour from the palette (bg-ink, text-gold-ink... are the
presets), so changing Gold ink in Styles recolours every eyebrow and link at once. Keep the text
colours (Gold ink on Base, Gold soft and Mist on Contrast) at a 4.5:1 contrast.
Typography#
The fonts are Libre Caslon Display (headings and figures, font-display) and IBM Plex Sans (body
text, weights 400 to 700), bundled in assets/fonts/ and declared in theme.json
(settings.typography.fontFamilies), so no request goes to Google. To use other fonts, install
them in Appearance > Editor > Styles > Typography > Fonts (the Font Library) and select them,
or replace the fontFace entries in theme.json. The utility classes use the families
--wp--preset--font-family--ibm-plex-sans (font-sans) and
--wp--preset--font-family--libre-caslon-display (font-display), so rename those slugs only
together with src/theme.css.
Font sizes are the presets in theme.json (xs 0.75rem to 9xl 8rem, the Tailwind scale). The
stylesheet uses them too: changing Large in Styles changes every text-lg in the design.
Content#
Edit pages in Pages: click any text to change it, select a button to change its link, select an image and use Replace. Sections are groups; open List View (the stacked lines icon at the top left) to select a section, move it, duplicate it or delete it. To add a section from another page, use the block inserter (+) > Patterns > Counsel. Search for "Hartwell", "Sloane", the phone numbers and the office addresses when you make it yours: they appear in the pages, the header and the footer.
Some content is in Custom HTML blocks, because blocks can't express it: edit those in the block's HTML view.
| Pattern | Custom HTML | What it is |
|---|---|---|
header |
the menu button | a button driven by assets/js/main.js |
practice-areas-list, insights-list, results-list |
the filter chips (with their counts) | see Filterable lists |
attorneys-directory, results-list |
the search field, the practice select, the "Clear filters" button | see Filterable lists |
consultation |
the consultation form and its thank-you message; the office map | see Forms; the map is an SVG illustration with labels |
briefing |
the newsletter sign-up | see Forms |
Blocks can't store data- attributes, so the theme writes them as classes on blocks and the first
lines of assets/js/main.js turn them back into attributes before anything else runs:
ffd-name--value becomes data-name="value" (an underscore stands for a space, so
ffd-practices--ip_corporate is data-practices="ip corporate"), ffa-name--value becomes the
attribute name="value" (ffa-role--note), and ff-hidden hides a block until the script shows
it. The cards of the filterable lists carry them:
| Page | Card classes | Values |
|---|---|---|
| Practice areas | ffd-item ffd-audience--business |
business or individual |
| Attorneys | ffd-item ffd-practices--ip_corporate |
practice keys, separated by _ |
| Results | ffd-item ffd-type--settlement ffd-practices--employment |
outcome (verdict, settlement, transaction, appeal, dismissal, resolution) and practice key |
| Insights | ffd-item ffd-topic--employment |
business, employment, intellectual-property, estate-planning, real-estate |
The practice keys are corporate, litigation, employment, ip, real-estate,
estate-planning, family and injury (the values of the practice selects). An attorney's search
text is their name and title, read from the card when the page loads (the HTML version writes it
in a data-search attribute), so a card you add or rename is found without any extra setting.
Cards that link to a page (practice areas, attorneys, insights) are groups with the class
link-card: the link is on the card's heading and covers the whole card. Change it on the
heading. To add an attorney, duplicate a card in List View, replace the portrait, name, title and
practices, set the ffd-practices--… class and link the name to the attorney's page.
The home page's figures count up from zero: each is a span inside the figure's paragraph with
data-count data-to="1200" data-suffix="+" (edit it in the paragraph's HTML view, Edit as
HTML), and its text is the final value.
reveal fades a block in as it scrolls into view (classes like [--d:80ms] stagger neighbouring
cards).
Images and artwork#
All artwork is SVG in assets/img/, shown with Image blocks:
| Files | Proportions | Where |
|---|---|---|
attorney-<name>.svg |
5:6 (400 × 480) | attorney cards, profile, article author |
insight-0.svg … insight-5.svg |
16:10 (640 × 400) | article covers |
facade.svg |
640 × 560 | home hero (courthouse) |
favicon.svg |
square | header and footer logo, browser tab icon |
og-image.svg / og-image.png |
1200 × 630 | social sharing image |
Replace them with photos of the same proportions from the Media Library; the crop comes from the
picture boxes. The office map on the Contact page is an SVG in a Custom HTML block (its labels
use the page's font); replace it with an Image block or your map provider's embed. Icons are Icon
blocks: pick another icon from the Counsel collection (the theme's icons) or WordPress's own.
The logo in the header and footer is assets/img/favicon.svg next to the Site Title block
(edit it in Appearance > Editor > Patterns > Header and Footer). The favicon is used until you
set a Site Icon in Settings > General.
Navigation, header and footer#
The header and footer are template parts (parts/header.html, parts/footer.html) showing the
header and footer patterns. Edit them in Appearance > Editor > Patterns > Header / Footer.
The header has two Navigation blocks showing the same menu ("Main menu", created by the setup:
Practice areas, Attorneys, Results, Insights, Contact): the desktop one and the one in the mobile
menu. Edit it once in Appearance > Editor > Navigation. WordPress marks the link to the current
page; an attorney profile, the article and the practice area page mark Attorneys, Insights and
Practice areas (their first section carries the class ff-section-attorneys, ff-section-insights
or ff-section-practice-areas, read by assets/js/main.js). The top bar (consultation offer,
phone, offices) is part of the header pattern.
The footer's columns are List blocks (practice areas, the firm) and groups (offices); the copyright year is added by PHP.
Legal notices#
The footer of every page carries an attorney-advertising and not-legal-advice notice (a group with
role="note" in the footer pattern). The results list, the article and the consultation form
add their own disclaimers and the conflict-check notice. Review all of them against your bar's
advertising rules before publishing: requirements differ between jurisdictions. Keep the
conflict-check notice on the consultation form, and don't ask for confidential details before
you've confirmed there's no conflict of interest.
Features#
Filterable lists#
The Practice areas, Attorneys, Results and Insights pages share one filter in
assets/js/main.js. A group with the class ffd-filter-list holds the cards (ffd-item) and any
of these controls:
| Control | Markup | Filters on |
|---|---|---|
| Chips | <button data-chip data-value="…" aria-pressed> (Custom HTML) |
the card value named by the list's ffd-key--… class (ffd-key--audience) |
| Select | <select data-filter-select data-key="practices"> (Custom HTML) |
the card value named by the select's data-key |
| Search | <input type="search" data-filter-search> (Custom HTML) |
each card's name and title |
| Count | a data-filter-count span in a paragraph |
the number of visible cards, announced to screen readers |
| Empty state | a group with ffd-filter-empty ff-hidden |
shown when nothing matches |
| Reset | <button data-filter-reset> |
clears every control and focuses the search |
A card matches if any of its values matches (ip corporate); the value all matches everything.
The counts on the chips ("For businesses 5") are text: update them when you add cards.
Count-up figures#
Spans with data-count animate from 0 to data-to (with data-suffix) over 1.4 seconds when they
scroll into view. With reduced motion they show the final value at once, and the text already
holds the final value without JavaScript.
Reading progress and table of contents#
On the article, the bar at the top of the window (a group with ffd-reading-progress) fills as you
read the text (the group with ffd-article-body), and the table of contents' links (with
data-toc-link, pointing at the headings' anchors) are marked aria-current for the section you
are reading. To add a section, give its heading an anchor (Block settings > Advanced > HTML
anchor) and add a link to #that-anchor in the contents list, with data-toc-link (edit the
list item as HTML).
Insights: pages or posts#
The Insights page and the article are pages, edited like any other content. This keeps the demo
exactly like the HTML version (cards filtered by topic, an article with a table of contents and an
author box) and keeps everything in one place. Each article is its own page: duplicate the
article page, write it, and add a card to the Insights page (with its ffd-topic--… class).
If you prefer WordPress posts (categories, authors, feeds, the post list in the admin), publish
them in Posts: templates/single.html uses the article's layout (category as an eyebrow, the
title, author and date, the same text styles). Show them with a Query Loop block, for example in
place of the Insights cards, or set a posts page in Settings > Reading (it uses
templates/index.html). The topic filter and the table of contents only work with the theme's
blocks.
Header, mobile menu, scroll reveals and back to top#
- The header gets its shadow once the page scrolls (
is-scrolled). - The menu button (
data-menu-button) opens#mobile-menu, updatesaria-expandedand its label, moves focus to the first link, and Esc closes it and returns focus. - Blocks with the
revealclass fade in as they scroll into view; revealing one also reveals everything before it. With reduced motion, or without JavaScript, everything shows at once. - A "Back to top" button appears after about one screen of scrolling; it scrolls to the top
(instantly with reduced motion) and moves focus to the main content. To remove it, delete the
"Back to top" part of
assets/js/main.jsand the.back-to-topstyles insrc/theme.css, then runpnpm build.
Block styles#
In the block settings' Styles panel:
| Block | Styles |
|---|---|
| Button | Fill (default, navy), Outline (WordPress's own, drawn as the template's outline button), Gold, Outline (on navy) |
The arrow after a button's text is drawn by CSS (the class icon-after-arrow) and moves on hover.
The FAQ's questions are Details blocks (the first one open, as in the HTML version).
Forms#
| Form | Page | Checks |
|---|---|---|
| Consultation request | Contact | name, email, who it's for (a required choice), practice area, a message of at least 20 characters and the acknowledgement; phone, office and other parties are optional |
| Newsletter | Insights |
Every form[data-validate] is handled in assets/js/main.js: fields are checked on blur and on
submit with plain-language messages under each field (aria-invalid and aria-describedby are
set for you), a group marked data-required-group="Your message" must have one option chosen,
focus moves to the first problem, and on success the form is hidden and its thank-you message
(data-success) is shown and focused; "Send another enquiry" (data-reset-form) brings it back.
Nothing is sent until you connect the forms.
Option 1: a form plugin#
Install a form plugin (WPForms, Gravity Forms, Fluent Forms, Contact Form 7, Jetpack Forms...),
build the form there (keep the acknowledgement checkbox and the conflict-check notice), then
replace the form's Custom HTML block with the plugin's block. Give its fields the class field,
its labels field-label and its button btn btn-primary to match the theme.
Option 2: a form service#
Keep the theme's form and send it to a service such as Formspree:
-
Add the endpoint to the form in its Custom HTML block:
<form action="https://formspree.io/f/your-id" method="POST" … data-validate>. -
In a child theme's copy of
assets/js/main.js, replace the demo part of the submit handler:jsfetch(form.action, { method: 'POST', body: new FormData(form), headers: { Accept: 'application/json' } }) .then(function (res) { if (!res.ok) throw new Error('Request failed'); var success = $('[data-success]', form.parentNode); form.hidden = true; success.hidden = false; success.focus(); }) .catch(function () { alert('Sorry, your request could not be sent. Please call us instead.'); }); -
Update the thank-you messages, which say the forms are demos.
Saved data (localStorage)#
Counsel doesn't store anything in the browser.
SEO and sharing#
Page titles come from WordPress: the page title, an em dash and the site name (for example
"Attorneys — Hartwell & Sloane"); the home page shows the site name and tagline (Settings >
General). The theme doesn't output meta descriptions or Open Graph tags; use an SEO plugin such
as Yoast SEO, Rank Math or The SEO Framework, and give it assets/img/og-image.png (or your own
1200 × 630 image) as the default social image. WordPress already generates a sitemap at
/wp-sitemap.xml. For a firm, add your offices as structured data (LegalService with addresses
and phone numbers) with the SEO plugin, and keep them identical on your Google Business Profiles.
Accessibility#
Built in: a skip link to the main content (added by WordPress, styled by the theme), landmarks
(header, navigation with labels, main, footer, a labelled legal notice), one h1 per page,
visible focus rings, the mobile menu button with aria-expanded, filter chips with aria-pressed
and live counts, a table of contents that marks the current section, form errors announced
through aria-describedby and aria-invalid, a described map, alt text on portraits, and
reduced-motion support for every animation. The demo passes axe (WCAG 2.1 A and AA).
When you edit: keep one Heading 1 per page, give images alt text (or mark them decorative), keep
link text meaningful, and label any field you add to a form (<label for="...">).
Building and deploying#
You only need this if you change the theme's files. The stylesheet assets/css/theme.css is
compiled with Tailwind CSS from src/theme.css and the classes used in patterns/, parts/,
templates/, inc/ and assets/js/:
pnpm install
pnpm build # compile assets/css/theme.css and assets/css/editor.css
pnpm watch # recompile assets/css/theme.css on every change (run pnpm build before you commit)Run pnpm build after adding a utility class to a pattern or template, otherwise the class has no
style. Classes you type in the editor's Additional CSS class(es) field are only styled if the
stylesheet already contains them.
To deploy, zip the theme folder (named freeframe-counsel, without node_modules/) and upload it
in Appearance > Themes, or copy the folder to wp-content/themes/ on your server.
Translations#
The theme is translation-ready with the text domain freeframe-counsel. All text in patterns,
templates and PHP goes through WordPress's translation functions; the script's strings (menu
labels, form errors) are translated in PHP (freeframe_counsel_script_strings() in
inc/setup.php). Create a translation with Loco Translate, or with WP-CLI and Poedit:
wp i18n make-pot wp-content/themes/freeframe-counsel wp-content/themes/freeframe-counsel/languages/freeframe-counsel.potSave freeframe-counsel-fr_FR.mo (for example) in wp-content/languages/themes/ or in the
theme's languages/ folder. Page content created by Counsel setup is copied into your pages in
the site's language at that moment; translate it in the editor like any other content. The
filters' values (data-value, ffd-… classes) stay in English; translate only their labels.
Child themes#
Use a child theme for changes you want to keep when Counsel is updated. Create
wp-content/themes/counsel-child/style.css:
/*
Theme Name: Counsel Child
Template: freeframe-counsel
*/Templates, parts, patterns and theme.json in the child theme override the parent's files of the
same name. The parent's stylesheet only contains the classes the parent uses: if your child theme's
patterns use new utility classes, compile a stylesheet for the child (copy package.json and
src/theme.css, point the @source lines at the child's folders) and enqueue it. To change the
script, copy assets/js/main.js into the child theme and load it in place of the parent's
(wp_deregister_script( 'freeframe-counsel' ) and wp_enqueue_script() on wp_enqueue_scripts,
priority 30), keeping the data the parent prints before it (window.freeframeCounselL10n,
window.freeframeCounselPages).
Troubleshooting#
- The pages look unstyled or new classes do nothing. The stylesheet only contains classes it
found when it was built. Run
pnpm buildafter editing patterns, then reload. - "This block contains unexpected or invalid content". A pattern or page was edited by hand with markup that doesn't match the block. Click Attempt recovery, or re-insert the section from Patterns > Counsel.
- A card doesn't show up in a filter. Check its
ffd-itemclass and its value class (ffd-practices--…,ffd-topic--…): the values must match the chips'data-valueor the select's options. - The header menu shows other pages, or an old menu. Select each Navigation block and choose "Main menu" (or yours) in the block settings.
- Counsel setup says the pages exist, but they are empty. It never overwrites existing pages. Delete (and empty the trash of) the pages with those slugs, or open each page and insert its page pattern from Patterns > Counsel pages.
- The forms don't send anything. They are demo forms; see Forms.
- A page shows the title twice or no title. Pages use a template without a title, because the page patterns start with their own heading. For a plain text page (privacy policy, terms), choose the Page with title template in the page's settings.
Differences from the HTML version#
The pages, copy, design and interactions are the same. What WordPress changes:
- Straight apostrophes typed in text blocks are shown curly, as WordPress does everywhere.
- Cards that are links have the link on their heading, stretched over the card (the HTML version wraps the whole card in a link), so screen readers announce the heading as the link's name.
- Data and ARIA attributes on content blocks are written as classes and restored by the script (see Content); the attorneys' search text is read from their cards.
License#
MIT, which is compatible with the GPL that WordPress requires (see LICENSE.txt in the theme
folder). Free for personal and commercial use, including the SVG artwork. IBM Plex Sans and Libre
Caslon Display are licensed under the SIL Open Font License 1.1 (assets/fonts/*/OFL.txt).
Hartwell & Sloane and everyone in this theme are fictional.