Overview#
Accrue is a website for an accounting firm, CPA practice or tax preparer, built as a WordPress block
theme. The fictional brand is Fernley Whitcomb CPAs, a seven-person firm on Cherry Street in
Chattanooga, Tennessee, serving households and businesses on both sides of the Georgia and Alabama
state lines. The theme has a home page with a live next-deadline card, firm figures, a numbered list
of services, industries, a Tennessee tax note, testimonials and getting-started steps; a services
page and one service page that shows any of the seven services; a pricing page with bookkeeping
packages and a fee estimator; a federal and Tennessee tax deadline calendar worked out by rule, with
a countdown and a saved document checklist; a team page with illustrated portraits; insights with a
category filter and an article page; a consultation booking page with a rolling slot grid; 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/accrue.
Headings, copy, cards, lists, buttons and images are core WordPress blocks arranged in block
patterns, so you edit them in the block editor without touching code. The parts that are worked out
in the browser (the deadline card and calendar, the checklist, the fee estimator, the service and
article bodies, the insights grid and the booking form) work as in the HTML version: they are small
Custom HTML blocks driven by assets/js/main.js, which reads the firm's data from
assets/js/data.js. Colors and fonts are design tokens in theme.json: change them in
Appearance > Editor > Styles and the whole site follows. Every date and time is worked out in
America/New_York (Eastern time), whatever the visitor's own time zone is.
Quick start#
Requirements: WordPress 7.1 or newer and PHP 7.4 or newer. No plugins are needed.
- Download
accrue.zipfrom freeframe.dev/templates/accrue (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 Accrue.
- Go to Appearance > Accrue setup and click Create the demo pages. This creates the Home, Services, Individual tax returns (the service page), Pricing and fee estimator, Tax deadline calendar and document checklist, Our team, Insights, the article page and Book a consultation 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.
- 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 > Accrue pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Accrue.
| Page | Slug | Page pattern | Sections (pattern files in patterns/) |
|---|---|---|---|
| Home | / |
page-home.php |
home-hero (with the next-deadline card), firm-numbers, home-services, who-we-serve, tennessee, testimonials, getting-started (with the first open consultation time), home-cta |
| Services | /services/ |
page-services.php |
services-header, services-grid (cards built from SERVICES), services-fit, engagement, services-cta |
| Individual tax returns | /service/ |
page-service.php |
service-detail (filled from SERVICES by ?s=), service-cta |
| Pricing and fee estimator | /pricing/ |
page-pricing.php |
pricing-header, bookkeeping-packages, fee-estimator, return-prices, other-prices, pricing-cta |
| Tax deadline calendar and document checklist | /deadlines/ |
page-deadlines.php |
deadline-calendar (countdown, filters, year switch, month list, holidays), document-checklist, deadlines-cta |
| Our team | /team/ |
page-team.php |
team-header, partners, staff, credentials, team-cta |
| Insights | /insights/ |
page-insights.php |
insights-list (category filter, featured article and grid built from ARTICLES), deadline-cta |
| When an S Corporation Election Makes Sense | /article/ |
page-article.php |
article (filled from ARTICLES by ?a=) |
| Book a consultation | /contact/ |
page-contact.php |
contact-header, booking (form, success panel, office hours and contact details), visit (map and directions), client-portal (#portal) |
| 404 | any missing URL | templates/404.html |
404 |
The service and article pages work as in the HTML version: one page each, filled in the browser
from assets/js/data.js. /service/?s=payroll shows Payroll, /article/?a=working-across-the-state-line
shows that article; with no parameter they show the most-booked service and the latest article, and
an unknown slug shows them with a notice. Every link the theme writes to a service or article uses
these addresses. 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-accrue/
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 scripts (with the script's strings, page and image URLs),
editor styles, the `js` class script, theme color and favicon,
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 > Accrue 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/data.js the firm's data: FIRM, SERVICES, DEADLINE_RULES, CHECKLISTS, ESTIMATOR,
BOOKING, CATEGORIES, ARTICLES (window.ACCRUE)
assets/js/main.js deadlines, countdowns, checklist, estimator, service and article pages,
insights filter, booking, header, reveal and back to top
assets/icons/ SVG icons for the Icon block
assets/img/services/ one illustration per service (480 × 300)
assets/img/articles/ one cover per article (640 × 400)
assets/img/ favicon (the logo mark), social image
assets/fonts/ Hahmlet and Geologica, 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-accrue.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.txtCustomizing#
Brand and colors#
The colors are in theme.json (settings.color.palette) and in Appearance > Editor > Styles >
Colors > Palette:
| Color | Default | Used for |
|---|---|---|
| Paper | #f5f1e6 |
the page background (oat linen) |
| Paper 2 | #ebe5d3 |
alternate bands, past deadlines |
| Card | #fffcf4 |
cards and forms |
| Line, Line strong | #d9d2bd, #b9b096 |
hairlines and field borders |
| Ink | #1f2316 |
text and dark bands (olive black) |
| Ink 2, Ink 3 | #2c3220, #3a4129 |
raised panels and borders on ink |
| Muted | #575a48 |
secondary text on light backgrounds |
| Sage | #c9ceb1 |
secondary text on ink |
| Olive | #4f5b1c |
buttons, links, the logo |
| Olive deep, Olive soft | #3b4513, #e1e5c8 |
hover, tints and highlighted cards |
| Marigold | #e8b13a |
highlighter strokes and accent buttons (always with ink text) |
| Marigold deep, Marigold soft | #d49a1f, #f8e7bb |
numbers, quote marks, the "next" deadline |
| Due, Due soft | #9a3b1e, #f6dccf |
errors, "due soon" tags, moved-date notes |
| Done | #3d6b2f |
ready states |
| White, Black | for your own blocks |
The theme's stylesheet reads these colors from the palette (bg-olive, text-muted...), so
changing Olive in Styles recolors every button and link at once. Keep Muted dark enough for text on
Paper (4.5:1) and never use Marigold for text on a light background. The SVG illustrations, the
portraits, the map and the 404 drawing carry their own colors in their files and markup; the logo
mark is assets/img/favicon.svg.
Components worth knowing in src/theme.css: folder (the file-folder card with a tab, the
template's motif; folder-ink and folder-olive variants), mark (the marigold highlighter under
a word in a headline), display and section-title (headline sizes), eyebrow and kicker
(labels), on-dark (light text and marigold accents on an ink or olive band), card, tag,
chip, receipt (the zigzag edge under the estimator total). Add them to a block in Block settings
Advanced > Additional CSS class(es).
Typography#
Two fonts are bundled in assets/fonts/ and declared in theme.json
(settings.typography.fontFamilies), so no request goes to Google: Hahmlet (500 to 700; preset
serif) for headlines, numbers in cards and the team initials, and Geologica (300 to 700;
preset sans) for everything else. To use other fonts, install them in Styles > Typography >
Fonts (the Font Library) and select them, or replace the fontFace entries in theme.json. The
utility classes read the presets (--wp--preset--font-family--serif, --sans), so keep those
slugs. The map on the contact page names Geologica directly (font-family="Geologica, sans-serif").
Font sizes are the presets in theme.json (xs 0.75rem to 9xl 8rem, the Tailwind scale).
Figures use tabular, lining numerals through the num class.
Content#
Edit pages in Pages: click any text to change it, select a button to change its link. 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 > Accrue.
What the script fills in or drives is in Custom HTML blocks: edit those in the block's HTML
view, and keep their ids and data- attributes.
| Pattern | Custom HTML | What it is |
|---|---|---|
home-hero |
the next-deadline card | see Countdown and next-deadline line |
who-we-serve |
the six industry icons | small SVG drawings |
partners, staff |
the portraits | illustrated SVG portraits (see Images and artwork) |
services-grid |
the empty list the service cards are built into | see Service pages |
service-detail |
the breadcrumb, the notice, the picture and the empty lists for what's included, who it's for, the timeline, the FAQ and related services | see Service pages |
fee-estimator |
the form and the result panel | see Fee estimator |
return-prices |
the price table | edit the rows in HTML view |
deadline-calendar |
the countdown card, the filter chips and year switch, the month list, the holiday list | see Deadline calendar |
document-checklist |
the list buttons and the checklist card | see Document checklist |
insights-list |
the category chips, the featured article and the grid | see Insights and articles |
article |
the breadcrumb, the notice, the cover, the body and "Keep reading" | see Insights and articles |
booking |
the form, its success panel and the office hours table | see Consultation booking and Forms |
visit |
the map | an SVG map of downtown Chattanooga |
404 |
the "misfiled" drawing | an SVG drawing |
header |
the menu button | driven by the script |
Some blocks carry classes (Block settings > Advanced > Additional CSS class(es)) that the script
reads. Blocks can't store data- attributes, so the script turns classes starting with ffd- into
them when the page loads: ffd-svc-name on the service page's heading becomes data-svc-name, and
ff-hidden becomes the hidden attribute. Keep those classes when you restyle a block.
revealfades a block in as it scrolls into view.link-cardon a group makes the whole group clickable: the first link inside it stretches over the group (the logo in the header and footer).
Everything data-driven is in assets/js/data.js, inside window.ACCRUE, the same file as in the
HTML version:
| Constant | What it holds |
|---|---|
FIRM |
name, phone ((423) 555-0138), phoneHref, email (hello@fernleywhitcomb.example), timeZone (America/New_York), refPrefix (FW) |
SERVICES |
seven services keyed by slug: name, short, from (price line), art, intro, included[], forWho[], timeline[{ when, step }], faq[{ q, a }], related[] (slugs) |
SERVICE_ORDER |
display order: individual-tax, business-tax, bookkeeping, payroll, advisory, nonprofit, irs-notices |
DEFAULT_SERVICE |
individual-tax, shown when ?s= is missing or unknown |
TAXPAYER_TYPES |
calendar filters: all, individual, employer, passthrough, ccorp, nonprofit |
DEADLINE_RULES |
24 deadline rules (see Deadline calendar) |
CHECKLISTS, CHECKLIST_ORDER |
four lists: individual (11 items), selfemployed (11), business (11), nonprofit (9) |
ESTIMATOR |
the fee estimator's prices and limits (see Fee estimator) |
BOOKING |
consultation slots, days shown, closures, meeting types (see Consultation booking) |
CATEGORIES |
article categories: all, tennessee, individuals, self-employed, small-business, nonprofits, irs-notices |
ARTICLES |
seven articles, newest first: slug, title, date (YYYY-MM-DD), cat, author, mins, art, dek, body[] |
Change data.js in a child theme (see Child themes) so an update doesn't
overwrite it. The home page's numbered service list, the pricing packages and the footer's service
links are blocks with the same names and prices; when you rename a service, update those too.
Adding a service
// assets/js/data.js, inside SERVICES
'estate-tax': {
name: 'Trusts & estates',
short: 'Fiduciary returns and estate tax filings for executors and trustees.',
from: 'From $1,200 a return',
art: 'assets/img/services/estate-tax.svg',
intro: '…',
included: ['…'], forWho: ['…'],
timeline: [{ when: 'Month 1', step: '…' }],
faq: [{ q: '…', a: '…' }],
related: ['individual-tax', 'advisory']
}Then add 'estate-tax' to SERVICE_ORDER and put estate-tax.svg in assets/img/services/ (the
theme passes the URL of every SVG in that folder and in assets/img/articles/ to the script, keyed
by the art path). The service appears on the Services page, in the booking form's service menu
and at /service/?s=estate-tax. Article bodies are arrays of blocks: { p: '…' }, { h2: '…' },
{ ul: ['…', '…'] } or { quote: '…' }. Keep ARTICLES newest first: the first one is the
featured article and the fallback.
Images and artwork#
All art is SVG drawn for this template; there are no photos.
assets/img/services/<slug>.svg: 480 × 300 (8:5). Replace with photos at the same ratio (and change theartpath indata.js); cards crop withobject-cover.assets/img/articles/<name>.svg: 640 × 400 (8:5); the article page shows it cropped to 16:7.- The team portraits are inline SVG in Custom HTML blocks on the Team page (inline so their
initials use Hahmlet; each has
role="img"and anaria-label). To use a photo, replace the block with an Image block at 6:7 (staff) or filling the 13rem column (partners), with real alt text. - The industry icons on the home page, the map and the 404 drawing are inline SVG in Custom HTML blocks; replace any of them with an Image block if you prefer.
- Icons are Icon blocks: select one and pick another from the Accrue collection or WordPress's own.
- The social image is
assets/img/og-image.png(1200 × 630, sourceog-image.svg); the faviconassets/img/favicon.svg(the logo mark) 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 pattern starts with the dark top bar: its first line is replaced by the next tax deadline
when the page loads (the link written there is what shows without JavaScript).
The header has two Navigation blocks showing the same menu: the desktop one and the one in the
mobile menu. Edit the menu once in Appearance > Editor > Navigation (the setup creates "Main
menu": Services, Pricing, Deadlines, Team, Insights, Contact). WordPress marks the current page's
link with aria-current="page"; on the service page the script marks Services, and on the article
page Insights, as the HTML version does (the page URLs are in freeframe_accrue_page_urls() in
inc/setup.php). 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's Services and The firm menus are List blocks: click a link to change it, press Enter to
add one. The copyright year is written by PHP. The phone number and email appear in the top bar,
the footer, the contact page and the call-to-action bands; search for them when you change them
(and in FIRM in data.js).
Features#
Deadline calendar#
The deadline calendar page, code in sections 4 and 7 of assets/js/main.js (deadlines(y),
businessDay(), paintDeadlines()). Each entry in DEADLINE_RULES is a rule, not a date:
{ id: 'individual', short: 'Individual returns', month: 4, day: 15, rule: 'federal',
who: ['individual'], title: 'Individual returns for {prev}, or an extension; …', forms: 'Form 1040, Form 4868' }For every calendar year the page builds the date from month and day, then moves it to the next
business day:
rule: 'federal': past Saturdays, Sundays, federal holidays and DC Emancipation Day (April 16, observed on the Friday before or Monday after when it falls on a weekend). April 15, 2028 (a Saturday) moves to Tuesday, April 18, 2028: Monday April 17 is the observed Emancipation Day.rule: 'tn': past Saturdays, Sundays, federal holidays (not Columbus Day or Emancipation Day) and Good Friday, for Tennessee franchise & excise deadlines.
{prev} and {year} in titles become the previous year and the calendar year. Moved deadlines show
a note such as "Moved from Sat, Oct 31: weekend." Holidays are computed by holidays(y) for any
year (fixed-date holidays observed on the Friday before a Saturday and the Monday after a Sunday,
Good Friday from Easter); which ones move deadlines is set by HOLIDAY_SETS in main.js.
The calendar opens on the year of the next deadline; the year switch shows the current and the next
year. Each entry is marked Passed, Next, or "in N days" (red within 30 days). A deadline counts as
ahead until midnight Eastern on its due date. The filter chips (data-type-filter) and year buttons
(data-year-offset) are buttons with aria-pressed; the count line (data-dl-count) is a polite
live region. Choices are written to the URL (?type=, ?year=).
Expected values at Friday, October 30, 2026, 10:00 AM Eastern:
- Count line: "24 deadlines in 2026 · 4 still ahead".
- Next deadline: Monday, November 2, 2026, "Third-quarter payroll return", Form 941 · IRS, moved from Sat, Oct 31; countdown 3 days, 14 hours, 0 minutes. Then Mon, Nov 16 and Tue, Dec 15 (two).
- With
?type=individual: "6 deadlines in 2026 · 0 still ahead"; next is Friday, January 15, 2027.
To turn a deadline off, delete its rule. To add a fiscal-year deadline, add a rule with its own month and day. Nothing here goes stale: the dates are worked out for whatever year it is.
Countdown and next-deadline line#
Every page's top bar shows the next deadline for everyone, for example "Next deadline Mon, Nov 2
· Form 941 (Q3) in 3 days" (the header paragraph with the class ffd-next-deadline). The home page
card (data-deadline-teaser) shows the next deadline's date, label, detail, days and hours left,
and the three after it. The calendar page has the full countdown (data-dl-next: days, hours,
minutes until 11:59 PM Eastern on the due date, with an sr-only sentence for screen readers; the
big numbers are aria-hidden). All of them refresh every 30 seconds, except when ?now= is set.
Document checklist#
The checklist on the deadline calendar page (/deadlines/#checklist), section 8 of main.js. Four
lists from CHECKLISTS, switched with the buttons [data-checklist-tab] (or ?list=<id> on load).
Each item is a real checkbox inside a <label>. The progress text (data-check-progress, for
example "1 of 11 ready") is a polite live region. "Clear this list" (data-check-reset) unticks the
current list only; when every item is ticked, a "Everything's here" note appears. Ticks are saved in
the browser (see Saved data).
Fee estimator#
The fee estimator on the pricing page (/pricing/#estimator), section 10 of main.js
(estimate(inp)). Settings in ESTIMATOR:
| Setting | Value |
|---|---|
entities |
sole, partnership ($1,440 a year), scorp ($1,560 a year + $40/month), ccorp ($1,800 a year), nonprofit ($1,680 a year + $100/month) |
tiers |
Foundation up to 100 transactions $350, Standard up to 250 $525, Expanded up to 500 $775, Full-charge up to 1,000 $1,150 |
overBlock, overBlockFee |
above 1,000: $95 for each further 250 transactions or part of 250 |
payrollBase, payrollPerEmployee |
$65 + $9 per employee, when there is at least one employee |
extraStateFee |
$45 a month for each state after Tennessee |
cleanupPerMonth |
$175 per month behind, a one-time fee shown separately |
limits |
transactions 0–2,000 in steps of 25, employees 0–60, states 1–6, cleanup 0–24 months |
defaults |
scorp, 200 transactions, 4 employees, 1 state, 0 months |
Monthly estimate = bookkeeping tier + payroll + extra states + entity extra + annual return fee ÷ 12,
rounded to the dollar. Defaults give $796/month ("$9,552 a year");
/pricing/?entity=nonprofit&tx=1300&emp=12&states=3&cleanup=6 gives $1,843/month with "$1,050
once, for 6 months of catch-up". Every change rewrites the URL, so an estimate can be shared; invalid
parameters fall back to the defaults. The package cards above the estimator are blocks: when you
change tiers, change their prices too.
Service pages#
/service/?s=<slug>, section 9 of main.js. Valid slugs are the keys of SERVICES. The slug is
checked with Object.prototype.hasOwnProperty.call(SERVICES, slug); a missing slug shows
DEFAULT_SERVICE, an unknown one shows it with the notice "We couldn't find a service called …".
The page title, breadcrumb, heading, intro, price line, picture, booking links
(/contact/?service=<slug>), what's included, who it's for, the timeline, the FAQ (native
<details>) and related services are filled in from the data. The Services page builds its grid of
cards into data-services-grid, followed by a dark "Not sure which?" tile.
Insights and articles#
On the Insights page the category chips (data-cat-filter) filter ARTICLES; with "All" the
newest article is the large featured card and the others are in the grid. The count reads "7
articles" or, for example, "2 articles in Small business". ?cat=<key> sets it on load and is kept
in the URL. /article/?a=<slug> shows an article (unknown or missing slugs show the newest, with a
notice when a slug was given); "Keep reading" lists three other articles, same category first.
Articles keep their real publication dates; they are past content and never move.
Consultation booking#
The booking page, section 12 of main.js. Settings in BOOKING:
| Setting | Value |
|---|---|
slots |
Monday–Thursday 9, 10, 11 AM, 1, 2, 3, 4 PM; Friday 9, 10, 11 AM, 1, 2 PM; Saturday 9, 10, 11 AM (tax season only); lunch 12–1 |
taxSeason |
02-01 to 04-15 (Saturdays are offered in this window) |
minutes |
30 |
days |
10 open days, starting tomorrow (Eastern) |
demoTaken |
45: the share of slots shown as already booked, stable for each date and time |
closures |
the office holidays (observed dates) |
meetings |
office, video, phone |
Sundays, Saturdays outside tax season and closure days are skipped, so the grid always shows the next
ten open days. /contact/?service=<slug> preselects a service (?service=not-sure selects "Not sure
yet"). The first open time on the home and contact pages (data-first-slot) is the earliest free
slot: at 10:00 AM Eastern on Friday, October 30, 2026, Mon, Nov 2 at 9:00 AM. The office status
("Open now", "Closed now", "Closed today for …") uses the hours in section 13 of main.js; the
hours table is in the booking pattern. When you connect a real calendar, replace slotTaken()
with its availability.
Previewing a date#
Add ?now=YYYY-MM-DDTHH:MM (Eastern wall-clock time) to any page to see the site at that moment:
the top bar, countdowns, calendar, office status, first open time and booking grid all use it, and
live refresh stops. Examples: /deadlines/?now=2028-04-01T10:00&year=2028 (the April 18, 2028
shift), /contact/?now=2027-02-25T09:00 (tax-season Saturdays in the grid).
Header, mobile menu, reveal and back to top#
The header is sticky and gains a shadow after scrolling. Below 1024 px the menu button
(#menu-toggle, aria-expanded, aria-controls="mobile-menu") opens the mobile menu; Escape
closes it and returns focus to the button, and it also closes on a link click or when the window
grows. Blocks with reveal fade in as they scroll into view (off with reduced motion). A
back-to-top button appears after about one screen of scrolling; to remove it, delete the "Back to
top" part at the end of assets/js/main.js 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 olive button), Marigold, Outline, Outline (on dark) |
The class is-small on a Button block makes it the smaller header size, and num gives it tabular
figures (phone numbers).
Forms#
The only form that collects details is the consultation request on the booking page
(form[data-booking], in the booking pattern's Custom HTML). The fee estimator is a form for
layout only; it never submits. The booking form doesn't send anything until you connect it.
Validation (inline errors with aria-invalid): a service is required; a time is required ("Choose a
day and a time."); the name needs at least two characters; the email must look like an address; the
phone is optional but, if given, needs 10 digits (or 11 starting with 1). On success the form is
replaced by the success panel: "Thank you, <first name>. Your consultation request is in.", a
reference FW- + five characters (a hash of name, email, date and time; for "Junie Example",
"junie@example.com", Nov 2 at 9:00 AM it is FW-BJRFB), the time, service and meeting type, and
what happens next. Focus moves to the success heading.
To connect it, for example to Formspree, replace the success branch in the submit handler in
assets/js/main.js (look for "To connect a real backend"; in a child theme):
const res = await fetch('https://formspree.io/f/your-id', {
method: 'POST',
body: new FormData(bookForm),
headers: { Accept: 'application/json' }
});
if (!res.ok) { /* show an error message and keep the form */ return; }
// then show [data-booking-success] as the demo doesMake the listener async. The fields are service, meeting, slot_date (YYYY-MM-DD),
slot_time (HH:MM, Eastern), name, email, phone, client (new or existing) and
notes. Show the success state only when the service answers OK. You can also replace the form's
Custom HTML block with a form plugin's block; add the classes label, field and btn-olive to
its labels, fields and button so it matches.
Saved data (localStorage)#
| Key | Shape | Used by |
|---|---|---|
accrue:checklist |
{ "list": "individual", "ticks": { "individual": ["i-w2"], "selfemployed": [], "business": [], "nonprofit": [] } } |
the document checklist |
This is the HTML version's key. On read (readChecklist()), list must be an own key of
CHECKLISTS, ticks lists that aren't arrays are ignored, and only item ids that exist in that list
are kept, so stale or tampered data can't break the page. Before each change the saved value is
re-read, so two open tabs don't overwrite each other's lists. All storage goes through one
safeStorage object: localStorage, or an in-memory stand-in when the browser blocks storage, in
which case ticks last for the page view. To reset, use "Clear this list", or remove the key in your
browser's developer tools (Application > Local Storage). Nothing else is stored; the estimator keeps
its state in the URL.
SEO and sharing#
Page titles come from WordPress: the page title, an em dash and the site name (for example "Our team
— Fernley Whitcomb CPAs"); the home page shows the site name and tagline (Settings > General).
The service and article pages also set document.title for the chosen service or article. 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. If search traffic
to individual services or articles matters, give each its own page (or post) instead of the ?s= and
?a= pages, and add AccountingService structured data with your real address and hours (most SEO
plugins can add it).
Accessibility#
Built in: a skip link to the main content (added by WordPress, styled by the theme), landmarks
(header, labelled navigation, main, footer), one h1 per page, labelled sections, visible focus
outlines (olive on light, marigold on dark), the menu button with aria-expanded, filter chips,
the year switch, checklist tabs and booking days and times as buttons with aria-pressed, booked
times disabled and announced as "booked", live regions for the deadline count, checklist progress,
estimator result and chosen booking time, countdown digits hidden from screen readers with a
sentence instead, form errors linked with aria-describedby and aria-invalid, 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, label any field you add, and keep aria-controls and aria-labelledby ids in step.
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, template or the script's markup,
otherwise the class has no style; keep each class name whole in the script (not built from pieces).
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-accrue, 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-accrue. All text in patterns,
templates and PHP goes through WordPress's translation functions; the script's own strings (day and
month names, holiday names, deadline notes, estimator lines, booking labels, form errors and
messages) are translated in PHP (freeframe_accrue_script_strings() in inc/setup.php). The firm's
content in assets/js/data.js (services, deadline titles, checklist items, articles) is edited
there directly. Create a translation with Loco Translate, or with WP-CLI and Poedit:
wp i18n make-pot wp-content/themes/freeframe-accrue wp-content/themes/freeframe-accrue/languages/freeframe-accrue.potSave freeframe-accrue-es_ES.mo (for example) in wp-content/languages/themes/ or in the theme's
languages/ folder. Page content created by Accrue setup is copied into your pages in the site's
language at that moment; translate it in the editor like any other content. Amounts are formatted in
US dollars and dates in US style by the script (money(), shortDate(), longDate()).
Child themes#
Use a child theme for changes you want to keep when Accrue is updated (your services, prices,
deadlines and articles in particular). Create wp-content/themes/accrue-child/style.css:
/*
Theme Name: Accrue Child
Template: freeframe-accrue
*/Templates, parts, patterns and theme.json in the child theme override the parent's files of the
same name. To use your own data.js, dequeue the parent's data script (freeframe-accrue-data) and
enqueue yours under the same handle in the child's functions.php:
add_action( 'wp_enqueue_scripts', function () {
wp_deregister_script( 'freeframe-accrue-data' );
wp_register_script( 'freeframe-accrue-data', get_stylesheet_directory_uri() . '/data.js', array(), '1.0', array( 'in_footer' => true, 'strategy' => 'defer' ) );
}, 15 );Do the same for assets/js/main.js (handle freeframe-accrue), keeping the inline
window.freeframeAccrue strings and URLs (or copy them). The parent's stylesheet only contains the
classes the parent uses: if your child theme uses 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 > Accrue.
- The calendar shows the wrong year or "Passed" everywhere. The page uses the visitor's clock in
Eastern time. Check for a leftover
?now=in the URL; it freezes the date. - A deadline is on a different date than the IRS announced. Disaster relief and one-off IRS
notices can postpone deadlines for some areas. Those aren't rules; add a note to the page or
temporarily change the rule's
monthandday. - Service or article links go to the wrong page. The script links to
/service/,/article/,/contact/and the other demo slugs. If you renamed those pages, change their URLs infreeframe_accrue_page_urls()ininc/setup.php(in a child theme, filter the inline script). - A new service has no picture. Put its SVG in
assets/img/services/with the name used in itsartpath; only files in that folder andassets/img/articles/are passed to the script. - Checklist ticks disappear after a reload. The browser is blocking storage (private mode, a sandboxed preview, strict privacy settings); ticks then last for the page view only.
- Accrue 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 > Accrue pages.
- 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#
Split license: the PHP code and block markup are licensed under the GNU GPL, version 2 or later, and the CSS,
JavaScript, images and design under the Freeframe Commercial License (unlimited sites for you and your
clients; no resale or redistribution as a theme). The full terms, with your licensee details, are in
LICENSE.txt and at https://freeframe.dev/license#commercial. Third-party components keep their own licenses.
Fonts: Hahmlet and Geologica are licensed under the SIL Open Font License 1.1 (OFL.txt in each folder of
assets/fonts/). Fernley Whitcomb CPAs, its people, clients, testimonials, phone number and email address are
fictional placeholders.