Overview#
Agency is a bold website for a creative or digital agency, built as a WordPress block theme. The
fictional studio is Northbound, an independent brand and digital studio in London, Lisbon and New
York. The theme has a big-type home page with a services marquee, a client logo wall, recent work,
count-up numbers and a testimonial carousel; a services page with engagement models and an FAQ; a
work page filtered by industry; a full case study with a before/after slider; an about page with a
timeline, values and team portraits; a careers page with filterable roles and an application form;
a project-brief contact form 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/agency.
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 and fonts 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
agency.zipfrom freeframe.dev/templates/agency (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 Agency.
- Go to Appearance > Agency setup and click Create the demo pages. This creates the Home, Services, Work, Case study, About, Careers and Contact pages from the theme's page patterns, sets Home as the front page (Settings > Reading) and creates the header menu (Work, Services, About, Careers, Contact). Pages that already exist with the same slug are left alone, so it is safe to click it again.
- Visit your site. Edit any page in Pages, and the header, footer and templates in Appearance > Editor.
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 > Agency pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Agency.
| Page | Slug | Page pattern | Sections (pattern files in patterns/) |
|---|---|---|---|
| Home | / |
page-home.php |
home-hero (line-by-line headline, rotating word), featured-project, marquee, clients (logo wall), services-list, recent-work, process, numbers (count-up numbers and awards), testimonials (carousel) |
| Services | /services/ |
page-services.php |
services-hero (links to each service), disciplines (the four services), engagement-models (ways to work together, with prices), faq |
| Work | /work/ |
page-work.php |
work-hero, projects (industry filter and project cards) |
| Case study | /case-study/ |
page-case-study.php |
case-study (header, facts, challenge, before/after slider, approach, gallery, results, quote, credits), next-project |
| About | /about/ |
page-about.php |
about-hero, studio-image, story (timeline), values, team, about-banner |
| Careers | /careers/ |
page-careers.php |
careers-hero, perks, roles (team filter and expandable roles), apply (application form) |
| Contact | /contact/ |
page-contact.php |
contact-hero, contact-form (project brief form, email addresses, studios with local times) |
| 404 | any missing URL | templates/404.html |
404 |
Posts use templates/single.html; the blog, archives and search results use templates/index.html,
templates/archive.html and templates/search.html.
Project structure#
freeframe-agency/
style.css theme header (name, version, requirements, license); no styles
theme.json design tokens: palette, fonts, font sizes, spacing, layout widths, button
functions.php loads the files in inc/
inc/setup.php stylesheet and script, editor styles, the `js` class script, 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 > Agency 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 (arrows, client logos)
assets/img/ logo, favicon, social image, case study artwork, team portraits
assets/fonts/ Bricolage Grotesque, Inter and JetBrains Mono, with their licenses
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-agency.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 | Default | Used for |
|---|---|---|
| Paper | #f1efe9 |
the page background |
| Paper 2 | #e7e4dc |
sunken panels (process, open roles, image backgrounds) |
| Ink | #0e0e0c |
text, dark sections, primary buttons |
| Muted | #5f5c55 |
secondary text |
| Line | #d4d0c5 |
hairlines and borders |
| Cobalt, Cobalt 600 | #2b36f5, #1f28c9 |
the accent: links, highlights, the testimonials section, hovers |
| Lime | #d4ff3f |
the highlight on dark sections |
| White, Black | for your own blocks |
The theme's stylesheet reads these colours from the palette (bg-paper, text-ink, text-cobalt
and so on), so changing Cobalt in Styles recolours every accent at once. The page background and
text colour are set in Styles > Colors > Background / Text (Paper and Ink). The translucent
header background once the page scrolls is written as rgb(241 239 233 / .88) in src/theme.css
(.site-header.is-scrolled); change it there if you change Paper.
Typography#
Three fonts are bundled in assets/fonts/ and declared in theme.json
(settings.typography.fontFamilies), so no request goes to Google:
| Font | Preset | Used for |
|---|---|---|
| Bricolage Grotesque (variable, 400 to 800) | display |
headings and the big display type (display-xl, display-lg, font-display) |
| Inter (variable, 400 to 700) | sans |
body text |
| JetBrains Mono (400 and 500) | mono |
the small uppercase labels (eyebrow) |
Headings use the display font through Styles > Typography > Headings. To use other fonts,
install them in Styles > Typography > Fonts (the Font Library) and select them for Text and
Headings, or replace the fontFace entries in theme.json. The utility classes read the presets
(--wp--preset--font-family--display, --sans, --mono), so keep those slugs.
Font sizes are the presets in theme.json (xs 0.75rem to 9xl 8rem, the Tailwind scale). The
big headlines use fluid sizes written as classes (text-[clamp(2.4rem,10.4vw,11.5rem)]).
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 > Agency.
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 |
|---|---|---|
testimonials |
the pause / previous / next buttons, the dots and the status line | carousel controls driven by assets/js/main.js |
projects, roles |
the filter buttons, the result count and the "nothing found" message | filters, see Filters |
case-study |
the before/after slider | two images and a range input; the image paths are in the HTML |
apply, contact-form |
the forms and their thank-you panels | see Forms |
header |
the menu button | a button driven by assets/js/main.js |
A few blocks carry a class (Block settings > Advanced > Additional CSS class(es)) that the script or stylesheet reads:
revealfades a block in as it scrolls into view;[--d:90ms]delays it.line-maskspans in the big headlines slide each line in on load. To edit a headline, edit the text inside each line; to add a line, copy one in the Code editor.count-upon a paragraph counts its number up when it scrolls into view. Write the number as it should end up (120+,80%): the digits and the suffix are read from the text.ffd-<name>--<value>stores a data attribute the script needs:ffd-industry--fintechon a project card (the work page filter),ffd-department--designon a role (the careers filter),ffd-carouselandffd-slideon the testimonial carousel and its slides. Blocks can't storedata-attributes, soassets/js/main.jsturns these classes back into them.link-cardon a group makes the whole group clickable: the first link inside it stretches over the group (project cards, the featured project, services list).on-darkon a dark section makes focus rings lime instead of cobalt.
To add a project, duplicate a project card on the Work page in List View, change its image, texts
and link, and set its industry class (ffd-industry--retail). To add an industry, also add a filter
button in the filter's Custom HTML (<button type="button" class="chip" data-filter="retail" aria-pressed="false">Retail <span class="count">00</span></button>); the counts fill themselves in.
Images and artwork#
- Case study artwork, the studio illustration and team portraits are SVG files in
assets/img/, shown with Image blocks. Replace them with your own pictures from the Media Library; the size and crop come from the block's classes ([&_img]:aspect-[5/6],[&_img]:object-cover...). Image blocks converted from the HTML template have the classcontents, so the image itself takes part in the layout as it does there; keep it when you replace an image. - Logo: the header and footer show
assets/img/logo.svgand the name "Northbound." in a paragraph with a link (the dot is a coloured span). Edit them in Appearance > Editor > Patterns > Header (and Footer). The site name in browser tabs comes from **Settings > GeneralSite Title**.
- Icons: icons are Icon blocks (client logos, the arrows in project cards). Select one and pick
another icon from the Agency collection or WordPress's own. The arrow after a button label is
drawn by CSS: the class
icon-after-arrow-righton a Button block adds one. - Client logos on the home page are Icon blocks and paragraphs; replace them with Image blocks to show real logos.
- Social image:
assets/img/og-image.png(1200 × 630, sourceog-image.svg). WordPress doesn't output Open Graph tags by itself; see SEO and sharing. - Favicon:
assets/img/favicon.svgis 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: the desktop one and the one in the full-screen mobile menu, which numbers its links (01, 02...) with CSS (decorative: screen readers hear only the link text). Edit the menu once in Appearance > Editor > Navigation (the setup creates "Main menu"). A Navigation block that doesn't point at a menu shows the most recently created one; to pin a menu, select the Navigation block and choose it in the block settings, for both blocks.
The footer has the call to action, the logo, a Studio menu and a Follow menu (List blocks: click a
link to change it, press Enter to add one), the offices and the copyright line (the year is
always the current one). Each office shows its local time: the time is filled in by
assets/js/main.js from the data-tz attribute of the small span after the city name (an IANA
time zone such as Europe/London); change it in the paragraph's Code editor.
Features#
Header and mobile menu#
The header is fixed at the top and turns solid once the page scrolls (is-scrolled, set by
assets/js/main.js). Below 1024 px the menu button ([data-menu-button]) opens the full-screen
#mobile-menu: focus moves to its first link, page scrolling is locked, Escape closes it and
returns focus to the button, and it closes when the window grows past 1024 px.
Headline and rotating word#
On load the script adds is-loaded to <body>, which slides each line-mask line of the big
headline into place. The rotating word in the home hero ("ambitious founders.", "growing brands.",
"curious teams.") cycles every 2.6 seconds; a visually hidden sentence lists all three for screen
readers. With reduced motion, the headline appears immediately and the word doesn't rotate.
Marquee and logo wall#
The services marquee scrolls continuously (38 seconds per loop, --animate-marquee in
src/theme.css). Its words are written twice for a seamless loop and hidden from screen readers;
a visually hidden paragraph lists the services once. It pauses on hover and stops with reduced
motion. To change the services, edit both copies and the hidden paragraph.
Scroll reveal and count-up#
Blocks with reveal fade in as they scroll into view; when one reveals, everything above it
reveals too, so jumping to an anchor never leaves hidden content. Paragraphs with count-up count
up over 1.6 seconds. With reduced motion everything shows immediately.
Testimonial carousel#
The home page's carousel (the group with ffd-carousel) has three slides (groups with
slide ffd-slide, labelled "1 of 3"...), dots, previous / next buttons and a pause / play button.
It advances every 7 seconds and pauses while the pointer is over it or focus is inside it; Left and
Right arrow keys move between slides and a live region announces "Testimonial 2 of 3". Hidden
slides are aria-hidden and inert. With reduced motion it starts paused.
To add a testimonial, duplicate a slide in List View (change its aria-label in Block settings > Advanced, e.g. "4 of 4") and add a dot button in the dots' Custom HTML.
Filters (work and careers)#
A filter is a group of buttons with data-filter-group="<key>" (in Custom HTML) whose buttons have
data-filter="<value>"; items anywhere on the page with the class ffd-<key>--<value> (turned into
data-<key>="<value>") are shown or hidden. Each button's count shows how many items match, the
result count updates, and a message appears when nothing matches. The chosen filter is written to
the URL hash, so /work/#fintech or /careers/#engineering can be shared.
Before / after slider#
On the case study, the slider has two images and a native range input: dragging or the arrow keys
reveal the "before" image. Because it's a real <input type="range">, it works with keyboards and
screen readers. Its images are in the Custom HTML (assets/img/kite-before.svg and
case-kite.svg); replace the paths with images from your Media Library.
Careers roles and FAQ#
Each role and each FAQ answer is a Details block (class role): it expands with a click, Enter
or Space. The summary line's layout (the role's grid, the FAQ's row) comes from classes on the
block that start with [&>summary]:. The summary holds the role title, team, location and the "+"
as styled spans: edit the text in place. "Apply for this role" jumps to the form and preselects
the role (read from the role's title).
Contact budget slider#
The budget field is a range input with five steps; the script shows the matching label and sets
aria-valuetext. The labels are translatable strings in freeframe_agency_script_strings()
(inc/setup.php) and the steps list in assets/js/main.js; keep the input's max equal to the
number of steps minus one.
Back to top#
A button appears after scrolling about one screen down; it scrolls to the top (instantly with
reduced motion) and moves keyboard focus to the main content. It hides while the footer's own
"Back to top" link is on screen. It is created by assets/js/main.js; to remove it, delete the
"Back to top" part of that file and the .back-to-top styles in src/theme.css, then run
pnpm build.
Block styles#
In the block settings' Styles panel:
| Block | Styles |
|---|---|
| Button | Fill (default, the ink button), Outline, Lime, Outline (on dark), Chip |
Forms#
The project brief (Contact) and the application form (Careers) check what visitors type in the
browser (form[data-validate] in assets/js/main.js). Error messages appear next to each field,
the field gets aria-invalid and aria-describedby, and focus moves to the first problem. The
services tiles need at least one ticked box. A valid form hides itself and shows its thank-you
panel; "Send another" brings the form back. Nothing is sent anywhere: they are demo forms until
you connect them.
| Form | Page | Fields and rules |
|---|---|---|
| Project brief | Contact | Name, email, company (required); services (at least one); budget slider; ideal start (required); message (required, 20+ characters); how did you hear about us (optional) |
| Application | Careers | Name, email, role (required); portfolio link (required, a full https:// link); location (optional); why Northbound (required, 30+ characters); consent (required) |
Option 1: a form plugin#
The usual way on WordPress. Install a form plugin (WPForms, Gravity Forms, Fluent Forms, Contact
Form 7, Jetpack Forms...), build the form there, then delete the form's Custom HTML block on the
page and insert the plugin's block or shortcode in its place. The plugin sends the messages and
stores them. To make its fields look like the theme's, add the theme's classes where the plugin
allows custom classes: field on fields, label on labels, btn btn-primary on the submit button.
Option 2: a form service#
Keep the theme's form and send it to a service such as Formspree:
-
Create a form on the service and copy its endpoint URL.
-
Edit the page, open the form's Custom HTML block and add the endpoint to the form tag:
<form class="grid gap-10" novalidate data-validate action="https://formspree.io/f/your-id" method="post">. -
In
assets/js/main.js, find the comment// Demo only: nothing is sent.in the form's submit handler and replace the lines after it with:jsvar success = $('[data-success]', form.parentNode); fetch(form.action, { method: 'POST', body: new FormData(form), headers: { Accept: 'application/json' } }) .then(function (res) { if (!res.ok) throw new Error('Request failed'); form.hidden = true; success.hidden = false; success.focus(); }) .catch(function () { alert('Sorry, something went wrong. Please email us instead.'); });Make this change in a child theme (copy
assets/js/main.jsthere and load it instead), or it will be overwritten when you update the theme.
All fields have name attributes, and the services tiles share name="services", so the service
receives every ticked value.
Saved data (localStorage)#
This theme 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
"Work — Northbound Studio"); 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.
Accessibility#
Built in: a skip link to the main content (added by WordPress, styled by the theme), landmarks
(header, labelled navigation, main, labelled sections, footer), one h1 per page, visible focus
rings (lime on dark sections), the menu button with aria-expanded, an accessible carousel
(pause button, live region, inert hidden slides), filter buttons with aria-pressed and a live
result count, native details elements and range inputs, form errors announced through
aria-describedby and aria-invalid, alt text on artwork and 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-agency, 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-agency. All text in patterns,
templates and PHP goes through WordPress's translation functions; the script's strings (button
labels, form errors, filter counts, budget steps) are translated in PHP
(freeframe_agency_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-agency wp-content/themes/freeframe-agency/languages/freeframe-agency.potSave freeframe-agency-de_DE.mo (for example) in wp-content/languages/themes/ or in the theme's
languages/ folder. Page content created by Agency setup is copied into your pages in the site's
language at that moment; translate it in the editor like any other content.
Child themes#
Use a child theme for changes you want to keep when Agency is updated. Create
wp-content/themes/agency-child/style.css:
/*
Theme Name: Agency Child
Template: freeframe-agency
*/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.
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 > Agency.
- The header menu shows other pages, or an old menu. A Navigation block without a chosen menu shows the most recent one. Select it and choose "Main menu" (or yours) in the block settings, in both the desktop and the mobile Navigation block.
- A filter shows "0 results". The items need the class
ffd-<key>--<value>matching a filter button'sdata-filtervalue (for exampleffd-industry--fintechanddata-filter="fintech"). - Agency 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 > Agency pages.
- The forms don't send anything. They are demo forms; see Forms.
- The theme can't be installed on an older WordPress. Agency uses the Icon block's icon collections from WordPress 7.1; update WordPress first.
- 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.
License#
MIT, which is compatible with the GPL that WordPress requires (see LICENSE.txt in the theme
folder). Free for personal and commercial use. The case study artwork, studio illustration and
team portraits are included under the same license. Bricolage Grotesque, Inter and JetBrains Mono
are licensed under the SIL Open Font License 1.1 (OFL.txt in each folder of assets/fonts/).