Freeframe

Documentation

Agency

v1.0.0Updated Oct 8, 2026A bold, high-end website for a creative or digital agency.

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.

  1. Download agency.zip from freeframe.dev/templates/agency (the WordPress tab).
  2. 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.
  3. 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.
  4. 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:

sh
pnpm install
pnpm dev     # builds the CSS, then starts WordPress with the theme at http://127.0.0.1:9400

pnpm 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.txt

Customising#

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:

  • reveal fades a block in as it scrolls into view; [--d:90ms] delays it.
  • line-mask spans 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-up on 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--fintech on a project card (the work page filter), ffd-department--design on a role (the careers filter), ffd-carousel and ffd-slide on the testimonial carousel and its slides. Blocks can't store data- attributes, so assets/js/main.js turns these classes back into them.
  • link-card on a group makes the whole group clickable: the first link inside it stretches over the group (project cards, the featured project, services list).
  • on-dark on 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 class contents, 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.svg and 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 > General

    Site 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-right on 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, source og-image.svg). WordPress doesn't output Open Graph tags by itself; see SEO and sharing.
  • Favicon: assets/img/favicon.svg is used until you set a Site Icon in Settings > General.

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.

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:

  1. Create a form on the service and copy its endpoint URL.

  2. 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">.

  3. 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:

    js
    var 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.js there 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/:

sh
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:

sh
wp i18n make-pot wp-content/themes/freeframe-agency wp-content/themes/freeframe-agency/languages/freeframe-agency.pot

Save 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:

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 build after 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's data-filter value (for example ffd-industry--fintech and data-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/).

Search documentation

Search the guides and every template's documentation