Freeframe

Documentation

Solace

v1.0.0Updated Nov 13, 2026A calm, warm website for a therapy and counseling practice, with a therapist match quiz and a sliding-scale fee checker.

Overview#

Solace is a multi-page website template for a therapy and counseling practice. The demo is Budd Bay Counseling, a fictional group practice at 412 Legion Way SE in Olympia, Washington, with six clinicians (a psychologist, an LICSW, an LMFT, two LMHCs and a supervised associate) who see adults, couples, teens, children and families in person and by video. This version is built with Nuxt 4, Vue 3, TypeScript and Tailwind CSS v4, and is generated as a fully static site. This is version 1.0.0 (see template.json and CHANGELOG.md). Template page and live demo: freeframe.dev/templates/solace.

The demo never goes stale: office hours, holidays, the next free consultation, reply-by dates and group start dates are all worked out in the browser from today's date in Pacific time, from rules rather than fixed dates.

Mental health copy needs care. The template keeps to general information, makes no promises about outcomes, shows no client testimonials or reviews, asks people not to put health details in forms, and points to the real crisis routes (call or text 988, call 911) on every page. Keep it that way when you edit, and have your own clinicians review the wording.

Quick start#

Requires Node.js 24 and pnpm.

sh
pnpm install
pnpm dev        # http://localhost:3000

Open http://localhost:3000. Edits to pages, components, data and app/assets/css/main.css reload as you save. pnpm generate writes the static site to .output/public/.

Script What it does
pnpm dev Development server with hot reload
pnpm generate Static site in .output/public/ (every page prerendered, plus 404.html)
pnpm build A Node.js server build in .output/ (pages still prerendered)
pnpm preview Serve the last build locally
pnpm lint / pnpm typecheck ESLint / TypeScript and Vue template checks

pnpm install also runs nuxt prepare, which writes the generated types and ESLint config to .nuxt/.

Pages#

Route File Purpose and notable features
/ app/pages/index.vue Hero with the next free consultation, how it works, services, clinician teaser, what to expect, fees at a glance, groups starting soon, crisis panel, map with open-now hours
/approach app/pages/approach.vue Ten approaches (CBT, ERP, EMDR, DBT-informed skills, ACT, Gottman, EFT, play, family, mindfulness) in tabs, each listing who offers it; /approach#emdr opens one
/services app/pages/services.vue Adults, couples, teens/children/families and video sessions, each with the clinicians who offer it; groups with rolling start dates and an interest-list form
/team app/pages/team.vue Directory with filters (synced to the URL) and a saved shortlist
/clinician app/pages/clinician.vue Profile from ?id=delacorte: bio, own words, weekly hours, next free consultation, fees, plans, training, related clinicians
/fees app/pages/fees.vue Fee table, sliding-scale calculator, insurance checker with out-of-network estimate, Good Faith Estimate
/match app/pages/match.vue Five-question quiz that suggests up to three clinicians with reasons
/faq app/pages/faq.vue Searchable accordion in five topics; /faq#money opens a topic
/contact app/pages/contact.vue Three-step request with live summary, office hours and holiday notice, reply-by date, map and directions
(any unknown URL) app/error.vue Friendly 404 with links, generated as 404.html

Project structure#

solace/
  app/
    app.vue                 the layout and the current page (keyed so a link to the same page starts it afresh)
    layouts/default.vue     skip link, crisis bar, header, footer, toast and back-to-top around every page
    pages/                  one file per page (index, approach, services, team, clinician, fees, match, faq, contact)
    error.vue               the 404 page
    components/
      CrisisBar.vue         "In crisis right now?" bar and the #crisis-panel disclosure
      SiteHeader.vue        logo, main menu, mobile menu
      SiteFooter.vue        footer with crisis routes, office hours, address and links
      OfficeHours.vue       the weekly hours list with today's row highlighted
      OpenStatus.vue, HolidayNote.vue, ReplyBy.vue   open-now pill, holiday notice, reply-by line
      ModalityTeam.vue      "Who offers it here" on /approach
      ServiceTeam.vue       clinician chips on /services
      StatusPill.vue        "Accepting new clients" or "Waitlist · …"
      NameLinks.vue         clinician names as links ("A, B and C")
      ChoiceCards.vue       radio and checkbox cards (quiz and consultation request)
      StepNav.vue           the numbered progress list
      AppIcon.vue           small line icons
      ToastMessage.vue, BackToTop.vue
    composables/
      useNow.ts             the shared Pacific-time clock
      useShortlist.ts       the saved shortlist and the localStorage keys
      usePageSeo.ts         title, description, canonical and Open Graph tags
      useCurrentPage.ts     aria-current for menu links
    data/
      site.ts               practice data: hours, holidays, clinicians, fees, sliding scale, plans, groups, quiz
      nav.ts                the header menus
    utils/
      calendar.ts           Pacific dates, holidays, office hours, reply-by dates, ?now= preview
      practice.ts           next consultation, group dates, sliding scale, matchClinicians()
      choices.ts            labels for the format and payment answers
      safeStorage.ts        localStorage with an in-memory fallback
      url.ts, validation.ts, toast.ts   address helpers, email and phone checks, toast()
    plugins/
      browser-state.client.ts   starts the clock and loads the shortlist once the page has loaded
      reveal.client.ts          scroll reveals
      same-page.client.ts       links to the page already showing start it afresh, like a page load
      keep-hash.client.ts       keeps the #hash exactly as opened
      stable-build.server.ts    reproducible builds
    router.options.ts       query strings written like the HTML version
    assets/css/main.css     Tailwind CSS v4: @font-face, @theme tokens and components (buttons, cards, choices...)
    assets/fonts/           Gilda Display and Rosario woff2 files and their OFL licenses
  public/img/               SVG artwork, portraits, map, favicon and og-image.png
  nuxt.config.ts            base path, head defaults, SITE_URL, prerendered routes, Tailwind
  template.json, README.md, DOCS.md, CHANGELOG.md

Customizing#

Brand and colors#

The design tokens are in the @theme block near the top of app/assets/css/main.css. Every color becomes a Tailwind utility (bg-clay-600, text-fir-800, border-line...).

Token Use
--color-clay-50 … -800 Brand terracotta. clay-600 for primary buttons, clay-700 for links and eyebrows
--color-sage-50 … -700 Calm fills, "accepting" pills, selected states
--color-fir-600 … -950 Deep ink green: crisis bar, footer, dark panels, secondary buttons
--color-tide-100, -300, -600 Water blue for artwork and small accents
--color-ink, --color-body, --color-muted Heading, body and secondary text
--color-oat, --color-oat-dark, --color-cream, --color-line Backgrounds and borders

To change the brand color, replace the clay scale:

css
@theme {
  --color-clay-600: #7b4a8c; /* buttons */
  --color-clay-700: #643b72; /* links, eyebrows */
}

Keep white text on clay-600 and fir-800 at 4.5:1 contrast or more. A few colors are also written inside the SVG files and the logo mark (in SiteHeader.vue, SiteFooter.vue and public/img/favicon.svg); change those by hand if you rebrand. The browser theme color is the theme-color meta tag in nuxt.config.ts.

Typography#

Headings use Gilda Display (an editorial serif, regular weight only) and body text uses Rosario (a humanist sans, weights 400–700 plus italic). Both are self-hosted from app/assets/fonts/ with @font-face rules at the top of main.css, using the same Latin files Google Fonts serves the HTML version, so text wraps the same way. The @theme tokens use the real family names:

css
--font-display: "Gilda Display", ui-serif, Georgia, "Times New Roman", serif;
--font-sans: "Rosario", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

To swap a font, put its woff2 file in app/assets/fonts/, change the @font-face rule (family name, src, weight range) and update the token.

Content#

Page copy is written in each page file (app/pages/<page>.vue). Everything the pages calculate from lives in app/data/site.ts:

Constant What it holds Used on
PRACTICE Name, timeZone: "America/Los_Angeles", phone, email, intake coordinator, refPrefix: "BBC", replyBusinessDays: 1, consultLeadDays: 2 every page
HOURS Session hours by weekday (0 = Sunday, null = closed) open-now pill
PHONE_HOURS Front-desk days; also defines business days for reply-by dates contact, groups
HOLIDAYS, EXTRA_CLOSED Federal holiday rules and one-off closures ("YYYY-MM-DD") hours, groups, consultations
CLIENTS, CONCERNS, FORMATS, TIMES Labels for who, what, how and when filters, quiz, profiles
CLINICIANS One object per clinician (below) team, profiles, quiz, contact, home
MODALITIES Approach names, keyed like the tabs on /approach profiles
FEES Fee rows with CPT codes, three tiers, group and workshop fees, cancellation fees, profiles, quiz
SLIDING Poverty guideline base and per-person amount, fee bands fees
PLANS, OON Insurance plans and the out-of-network estimate fees, team, quiz, contact
GROUP_RULES, GROUPS Groups and their start-date rules services, home
WHO, MATCH Quiz answers and points match

The weekly hours lists are written out in ROWS in app/components/OfficeHours.vue; keep them in step with HOURS.

Adding a clinician

Add an entry to CLINICIANS and a 320 x 400 portrait to public/img/. The key is used in URLs (/clinician?id=underhill), so use lowercase letters only.

ts
underhill: {
  name: "Sorrel Underhill", first: "Sorrel", short: "Sorrel", credential: "LMHC",
  role: "Licensed mental health counselor", pronouns: "they/them", tier: "licensed",
  clients: ["adults"], concerns: ["anxiety", "grief"], modalities: ["act", "cbt"],
  formats: ["inperson", "video"], days: { 1: ["09:00", "17:00"], 4: ["12:00", "20:00"] },
  accepting: "yes", insurance: ["soundshore"], sliding: false,
  languages: ["English"], since: 2020,
  focus: "One line for cards", summary: "Two sentences for the directory.",
  bio: ["Paragraph one.", "Paragraph two."], words: "A sentence in their own words.",
  training: ["MA, Counseling", "LMHC, Washington State"], art: "portrait-underhill.svg",
},
  • tier picks the column of FEES.tiers (psychologist, licensed or associate).
  • days are session hours. A shift that starts before DAYTIME_UNTIL ("15:00") counts as daytime, one that ends after EVENING_FROM ("17:00") counts as evenings, and day 6 counts as Saturdays.
  • accepting is "yes" or "waitlist"; a waitlisted clinician needs wait: "about 6 weeks".
  • insurance lists keys of PLANS; sliding: true means they hold sliding-scale spots.

The directory, profile, quiz, contact form, approach tabs and service lists all pick the new clinician up (pnpm typecheck checks the fields). Also mention them in the home and team page copy where the number of clinicians appears ("Six therapists").

Fees and the sliding scale

FEES.tiers holds the fee per row id (consult, intake, individual, couples, extended). The table on /fees is built from it. The figures repeated in copy ($110, $165, $190 on the home page; $55 groups; $75 late cancellation) are written in the page files.

The sliding scale works out income as a share of the federal poverty guideline (slidingFee() in app/utils/practice.ts): guideline = base + perPerson × (household − 1), then the first band whose upTo percentage is not exceeded sets the fee. Update base and perPerson with each year's guidelines.

Household Income Share of guideline Fee
1 $15,000 96% $40
1 $30,000 192% $60
3 $45,000 169% $60
4 $120,000 373% $125
2 $100,000 473% Standard fee

Insurance plans

PLANS entries have a type: "in" (in network with the clinicians who list the plan), "eap" (an employee assistance program with sessions free sessions) or "oon" (out of network: superbill). The plan names are fictional placeholders; use the plans your practice really works with. The out-of-network estimate is OON.allowed × share: with the defaults, 60% of $140 is about $84 back, so a $165 session costs about $81.

Groups

Each group in GROUPS starts on the nth weekday (0 = Sunday) of each month in months. The date shown is the first one at least GROUP_RULES.minLeadDays (7) days from today that isn't a holiday (observed, or the calendar date of a fixed-date holiday), so the list never shows a past date. weeks: 0 marks a one-day workshop. Opened on Nov 1, 2026 the demo shows Slack Tide from Thursday, Dec 10; Little Hours from Saturday, Jan 2, 2027; Steady Ground from Tuesday, Jan 5, 2027; Tide Pool from Wednesday, Jan 6, 2027; and Two Oars on Saturday, Mar 20, 2027.

To use fixed dates on a real site, replace the rule with your own logic in nextGroupStart() in app/utils/practice.ts, or simply edit months each term.

Images and artwork#

All artwork is SVG drawn for this template, in public/img/: the hero landscape, five service illustrations, page art, the map and six portraits. Each is a plain <img> with the base path in front (`${base}img/hero-sound.svg`, where base is useRuntimeConfig().app.baseURL), so you can replace any of them with a photo of the same aspect ratio:

Image Size Notes
hero-sound.svg 560 x 640 shown in an arch; keep the subject centered
svc-*.svg 360 x 220 cropped to 5:4 on /services
portrait-*.svg 320 x 400 shown in arches; keep faces in the upper middle
approach-stones.svg, fees-shells.svg, heron-404.svg about 400 x 440 page heroes
map.svg 640 x 440 replace with a map image or an embedded map

The social image is public/img/og-image.png (1200 x 630), generated from og-image.svg. Edit the SVG, then export it to PNG with any tool (or open it in a browser and take a 1200 x 630 screenshot). The favicon is public/img/favicon.svg.

The crisis bar (CrisisBar.vue), header (SiteHeader.vue) and footer (SiteFooter.vue) wrap every page in app/layouts/default.vue. To add a page, create app/pages/<page>.vue with its root <main id="main" tabindex="-1"> and a usePageSeo({ title, description }) call, add its route to routes in nuxt.config.ts (so it is prerendered), then add it to NAV in app/data/nav.ts; the desktop and mobile menus are built from it and mark the current page with aria-current="page".

Features#

Crisis panel#

Every page starts with a bar: "In crisis right now? Call or text 988, any time. In an emergency, call 911." The "More ways to get help now" button (data-crisis-toggle) opens a panel (#crisis-panel) with call buttons and the note that the practice can't respond to emergencies by form or email. It is a disclosure button with aria-expanded; Esc closes it while focus is inside. The home page, footer, match page and contact form repeat the same routes. Don't remove them, and check the numbers if you adapt the template for another country.

Office hours, open now and holidays#

OfficeHours.vue highlights today's row in every hours list, OpenStatus.vue shows the open-now pills, HolidayNote.vue the next holiday within 14 days and ReplyBy.vue the reply-by line. The clock behind them (useNow(), started by app/plugins/browser-state.client.ts) refreshes every minute and after every link.

Holidays are the 11 US federal holidays, worked out for any year from the rules in HOLIDAYS (app/utils/calendar.ts). Fixed-date holidays move to the Friday before when they fall on a Saturday, and to the Monday after on a Sunday, so New Year's Day 2028 (a Saturday) is observed on Friday, Dec 31, 2027. Add one-off closures to EXTRA_CLOSED.

All dates use PRACTICE.timeZone (America/Los_Angeles), whatever the visitor's own time zone. For review, add ?now=2027-01-04T10:00 to any page URL to see it at that Pacific wall-clock time.

Anything that depends on the clock, the URL or saved data is rendered in the browser after the page loads (useNow() is null until then, and the pages read the URL and storage in onMounted()), so the generated HTML shows the written text, never a date frozen at build time, and hydration never mismatches.

Next free consultation and reply-by dates#

nextConsult(id, now) in app/utils/practice.ts returns the first day at least consultLeadDays (2) days ahead when an accepting clinician works and it isn't a holiday; waitlisted clinicians show their wait instead. The home hero shows the earliest across the team (on Sunday, Nov 1, 2026: "Tue, Nov 3 with Eamon"). Reply-by dates are the next business day (Monday to Friday, not a holiday) after today.

Clinician directory and shortlist#

/team (app/pages/team.vue) filters the cards by concern, client group, in-network plan, format, "accepting new clients" and "sliding-scale spots". Filters are written to the URL (/team?concern=trauma&format=video&accepting=1) with history.replaceState, so filtered views can be shared; values are checked against the data before use. Example: concern trauma and format video shows Eamon only; plan Cascadia Mutual with accepting shows Eamon and Nadira.

The "Save" buttons (data-save-id) are toggle buttons with aria-pressed that add a clinician to the shortlist bar, which links to the consultation form with the first saved clinician. The profile page has the same button. Every button shares one list through useShortlist() in app/composables/useShortlist.ts.

Clinician profiles#

/clinician?id=<key> renders one clinician from CLINICIANS (app/pages/clinician.vue): name, role, pronouns, status, formats, bio, "in their own words", a Monday to Saturday hours grid, the next free consultation, an "at a glance" list and three related clinicians (most shared concerns and client groups). Without id it shows the first clinician; an unknown id shows a "We couldn't find that clinician" message. The tab title is set from the clinician in the browser. A link to another profile starts the page afresh at the top (app/plugins/same-page.client.ts).

Approach tabs#

/approach uses the ARIA tabs pattern (role="tablist", arrow keys, Home and End). The URL hash selects a tab (/approach#emdr), and profile pages link there. "Who offers it here" (ModalityTeam.vue) lists clinicians whose modalities include the tab's key. On phones the tabs are a scrolling row; the selected tab is scrolled into view inside the row without moving the page.

Match quiz#

/match (app/pages/match.vue) asks five questions: who it's for, up to three concerns (MATCH.maxConcerns), format, times and payment. matchClinicians(answers) in app/utils/practice.ts:

  1. Keeps clinicians who see the chosen client group (WHO[who].client).
  2. Drops those who don't offer the chosen format, unless that leaves nobody (then it says so).
  3. Scores 3 points per matching concern, 1 per matching time, 2 for payment (in-network plan, sliding-scale spots or the EAP) and 2 for accepting new clients (MATCH.points).
  4. Shows the top three, ties in data order, each with its reasons and a link to /contact?clinician=<key>&from=match.
Answers Suggestions (score)
Me · anxiety, life changes · video · evenings · sliding scale Eamon (11), Nadira (8), Iver (8)
Me and my partner · relationships · in person · Saturdays · Sound Shore Health Isaura (8)
My teen · intense emotions, school stress · video · evenings · Cascadia Mutual Nadira (11), Dr. Ostrander (3)
Me · trauma, grief · in person · daytime, evenings · EAP Eamon (12), Iver (9), Isaura (4)
My child · anxiety · video · daytime · paying myself Coralie (4), shown for in-person sessions

Sliding scale and insurance checker#

On /fees (app/pages/fees.vue), the calculator updates as you type and highlights the matching band; /fees?household=3&income=45000 prefills it. The plan checker (?plan= is kept in the URL) shows in-network clinicians, EAP details or the out-of-network explanation with a refund estimate by share (50–80%).

FAQ#

/faq uses <details> elements, so answers open with the keyboard and without JavaScript. The search box filters questions and answers as you type (/faq?q=insurance prefills it) and announces the count. "Open all answers" toggles every item, and linking to a topic (/faq#money) opens its first answer. To add a question, copy a <details class="faq" data-faq-item> block in app/pages/faq.vue and update the "17 questions" count written next to the search box.

Reveals, back to top and toast#

Sections fade in once as they scroll into view (app/plugins/reveal.client.ts, off with reduced motion; each element's delay is its --d style). A back-to-top button (BackToTop.vue) appears after scrolling and moves focus to the main content; toast() (app/utils/toast.ts, shown by ToastMessage.vue) announces shortlist changes through a polite live region. The mobile menu button has aria-expanded and aria-controls, and Esc, a click outside or a link closes it.

Forms#

There are two forms, and neither sends anything until you connect it:

Form File Validates Success state
Consultation request (data-intake-form) app/pages/contact.vue Who, format and payment (plan if insurance); times and contact method; first and last name, email, a 10-digit US phone when contact is by phone or text, and age confirmation "Thank you, Sam. Your request is in.", a reference like BBC-7K2QH, who will contact you and by when, and what happens next
Group interest list (data-group-form) app/pages/services.vue Group, first name, email "You're on the list for Steady Ground", the start date, the reply-by date and a reference

The consultation form reads ?clinician=, ?who=, ?format=, ?pay= and ?plan= from the URL and, with ?from=match, the saved quiz answers. Choose a clinician and it says whether they are accepting, their next free consultation, and whether they work in the chosen format or with the chosen client group.

To connect a form service such as Formspree, post the form data in the submit handler and show the success panel only when the service answers OK. In app/pages/contact.vue, find the comment "Connect your form service here" in onSubmit and add, before the code that fills in sent:

ts
const res = await fetch("https://formspree.io/f/your-id", {
  method: "POST",
  body: new FormData(form.value!),
  headers: { Accept: "application/json" },
});
if (!res.ok) return toast("Sorry, that didn’t send. Please call us at (360) 555-0142.");

Do the same in onSubmit in app/pages/services.vue. Every field has a name, so FormData picks them all up. If you deploy with pnpm build (a Node.js server) instead of pnpm generate, you can post to a server route of your own (server/api/request.post.ts) instead. Choose a form service that offers a signed business associate agreement (BAA) if you might receive health information, and keep the form copy asking people not to include health details.

Saved data (localStorage)#

Key Shape Validation on read Cleared
solace-shortlist array of clinician keys, e.g. ["delacorte"] unknown keys and duplicates dropped (loadShortlist()) "Clear" on the shortlist bar
solace-match { who, concerns[], format, times[], pay, plan, done } every value checked against the data (cleanMatch()) "Start over"
solace-intake { who, format, clinician, pay, plan, times[], contact, voicemail } every value checked (cleanDraft() in contact.vue) when the request is sent

Names, email addresses, phone numbers and the free-text note are never stored. The keys are listed in KEYS (app/composables/useShortlist.ts), and all access goes through safeStorage, readJSON() and writeJSON() in app/utils/safeStorage.ts: when localStorage is blocked (sandboxed iframes, strict privacy settings), it falls back to memory, so the shortlist, quiz and form keep working for the page view. To reset everything, clear the site's data in the browser or run localStorage.clear() in the console.

SEO and sharing#

Each page calls usePageSeo({ title, description }) (app/composables/usePageSeo.ts), which sets the title, description, canonical link, Open Graph and Twitter tags. Canonical URLs and the social image need your domain: set SITE_URL when you build (SITE_URL=https://yourpractice.example pnpm generate); the default is in runtimeConfig.public.siteUrl in nuxt.config.ts. The 404 page passes noindex: true. Add a sitemap.xml and robots.txt to public/ if you want them. If you add structured data, the MedicalBusiness or Psychologist schema.org types fit.

Accessibility#

  • Skip link, landmarks, one h1 per page and a logical heading order.
  • Visible focus outlines; WCAG AA color contrast for text.
  • Keyboard-accessible menu, crisis disclosure, tabs, <details> accordions, filters and toggle buttons with aria-pressed.
  • Radio and checkbox cards are real inputs inside labels, grouped in <fieldset> with <legend>.
  • Inline errors are tied to their fields with aria-describedby and aria-invalid; focus moves to the first problem. Step changes move focus to the new step's heading.
  • Results and counts are announced with role="status".
  • prefers-reduced-motion turns off reveals, the floating cards and smooth scrolling.

When editing, keep one h1, label every new input, and keep the crisis routes visible.

Building and deploying#

pnpm generate writes a static site to .output/public/: every page in routes (nuxt.config.ts) as <page>.html with its _payload.json, plus 404.html. Upload that folder to any static host (Netlify, Cloudflare Pages, GitHub Pages, Vercel or your own server), with extensionless URLs (/team) served from the .html files, which most static hosts do. Use HTTPS, and connect the forms before you go live.

To serve the site from a sub-folder, set DEMO_BASE_PATH when building (DEMO_BASE_PATH=/solace pnpm generate); nuxt.config.ts turns it into app.baseURL, which every link, script and image path uses. Leave it unset for a site at the root of its domain. pnpm build makes a Node.js server build instead, for hosts that run Nuxt (the pages are still prerendered).

Troubleshooting#

  • Times or "open now" look wrong. Everything uses PRACTICE.timeZone. If your practice isn't in Pacific time, change it and update the "Pacific time" wording in the pages.
  • Dates show the written text for a moment. That's by design: the generated HTML can't know today's date, so clock-dependent parts fill in right after the page loads.
  • A new Tailwind class has no effect. Write class names as whole strings (not built from pieces like `bg-clay-${n}`), so Tailwind can find them.
  • A new page shows the 404 page after pnpm generate. Add its route to routes in nuxt.config.ts.
  • A new clinician doesn't appear in the quiz. Check that their clients include the answer's client group (WHO), and that their formats include the chosen format.
  • The group date is not what you expected. It's the first date at least 7 days ahead in a listed month that isn't a holiday; preview other dates with ?now=YYYY-MM-DDTHH:MM.
  • The shortlist or quiz answers don't survive a reload. The browser is blocking storage (private mode, a sandboxed preview); they still work for the page view.
  • The social image doesn't show when shared. Build with SITE_URL set to your domain.

License#

Freeframe Commercial License: use it in unlimited websites and apps for yourself and your clients; it may not be resold or redistributed as a template. 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. The fonts (Gilda Display and Rosario) are from Google Fonts under the SIL Open Font License. Budd Bay Counseling, its clinicians, insurance plans, groups and address are fictional placeholders.

Search documentation

Search the guides and every template's documentation