Freeframe

Documentation

Tessel

v1.0.0Updated Oct 27, 2026A neo-brutalist SaaS website for an online form and survey builder, with a live form builder in the hero.

Overview#

Tessel is a multi-page marketing website for a SaaS form and survey builder, styled in a neo-brutalist way: warm cream paper with visible grid lines, thick 2.5px black borders, hard offset shadows with no blur, flat tomato, butter, mint, periwinkle and pink blocks, sticker-like badges and chunky buttons that press down when clicked. The fictional company, Tessel Forms, Inc., is based at 1110 Harney Street in Omaha, Nebraska, so the copy uses US spelling, prices are in US dollars and everything time-based runs in Central Time (America/Chicago). It is built with plain HTML, Tailwind CSS v4 and vanilla JavaScript, has a light and a dark theme, and is version 1.0.0. See it on the template page, which links to the live demo.

Quick start#

  1. Unzip the download and open index.html in a browser. Everything works from the file system.

  2. Or serve the folder so URLs look like a real site:

    sh
    npx serve .
  3. To change styles you need Node.js 24 or newer:

    sh
    npm install
    npm run watch

assets/css/style.css is the compiled Tailwind output and is already included, so you only need Node when you add or change classes.

Pages#

File Purpose Notable sections and features
index.html Home Live mini form builder with a validating preview, customer wordmarks, four feature blocks, how it works, stats, testimonial, three featured templates, call to action
features.html Features Sticky section nav, conditional-logic demo with a running total, payments receipt, integration strip, drop-off analytics card with a 7/30 day switch, smaller features
templates.html Templates gallery Search, category chips, Saved filter, result count, empty state, preview dialog, save to shortlist
pricing.html Pricing Response-volume slider, monthly / annual switch, four plans with a "Best fit" sticker, comparison table, FAQ accordion
integrations.html Integrations Search and category filter over 20 integrations, "What syncs" details, empty state, webhook sample
changelog.html Changelog Releases with type filter, dates that stay current, "Coming next" roadmap, monthly roundup form
customers.html Customer stories Featured Bramblecourt story, six stories with before/after charts and an industry filter
contact.html Talk to sales Sales form with routing and a reference, live sales hours, next weekly walkthrough, team, map
signup.html Sign up and sign in Tabs, password strength, plan and template hand-off, sign-in and password reset
404.html Not found Links back to home and templates

Project structure#

tessel/
  index.html ... 404.html   the ten pages (header and footer repeated in each)
  assets/
    css/style.css           compiled Tailwind CSS (generated by npm run build)
    js/data.js              content: field types, templates, plans, integrations, changelog, stories
    js/main.js              theme, menu, reveals and every interactive feature
    img/favicon.svg         the four-tile logo
    img/og-image.svg        source of the social image
    img/og-image.png        1200×630 social image used by og:image
  src/input.css             Tailwind source: @theme tokens, dark values, a few components
  package.json              build and watch scripts for the CSS
  README.md                 short overview
  DOCS.md                   this guide
  CHANGELOG.md              the template's version history
  LICENSE.txt               MIT license (in the download)

Customizing#

Brand and colors#

All tokens live in the @theme block of src/input.css. Theme tokens change between light and dark; brand colors stay the same in both.

Token Light Used for
--color-bg #fbf3e4 Page background (bg-bg)
--color-bg-2 #f3e5c9 Alternate bands (bg-bg-2)
--color-card #fffaf1 Cards, inputs
--color-ink #16130f Text (text-ink)
--color-ink-soft #4a4238 Secondary text
--color-line #16130f Borders and hard shadows (border-line)
--color-grid 9% ink The grid paper behind heroes
--color-tomato, --color-butter, --color-mint, --color-peri, --color-bubble fixed Accent blocks, stickers, chart bars
--color-coal, --color-cream fixed Text on accent blocks (always dark) and on the dark stats band

The hard shadows are tokens too: --shadow-brut-sm, --shadow-brut and --shadow-brut-lg (utilities shadow-brut-sm, shadow-brut, shadow-brut-lg). To change the main accent, edit one value and rebuild:

css
@theme {
  --color-tomato: #ff4f8b; /* hot pink instead of tomato */
}

Text on an accent block always uses text-coal, so it stays readable in dark mode. A few SVGs use the hex values directly (for example #ff6b42); search for them if you change a brand color.

The small component layer in src/input.css holds the pieces used on every page: .btn (with .btn-tomato, .btn-mint, .btn-plain, .btn-sm, .btn-lg), .icon-btn, .card, .sticker, .eyebrow, .chip, .field, .label, .error, .switch, .range, .grid-paper, .marker and .reveal. Everything else is Tailwind utilities in the markup.

Typography#

Three Google Fonts, loaded with one <link> in each page's <head>:

  • Bowlby One for display headings (font-display)
  • Ubuntu Sans for body text (font-sans, weights 400–800)
  • Ubuntu Sans Mono for labels, stickers and numbers (font-mono)

To swap a font, change the Google Fonts URL in every page and the matching --font-* token in src/input.css, then rebuild the CSS.

Content#

Most page copy is plain HTML. The lists that drive interactive parts live in assets/js/data.js as window.TESSEL:

Constant What it drives
FIELD_TYPES, FIELD_ORDER, MAX_FIELDS, BUILDER_DEFAULT The home page form builder
TICKETS, ADDON_TSHIRT Prices in the logic demo
TEMPLATE_CATEGORIES, TEMPLATES Templates gallery, featured templates, sign-up hand-off
VOLUME_STEPS, DEFAULT_VOLUME, ANNUAL_DISCOUNT, PLANS Pricing calculator
INTEGRATION_CATEGORIES, INTEGRATIONS Integrations directory and the strip on the features page
CHANGES, CHANGE_TAGS Changelog
INDUSTRIES, STORIES Customer stories

Add a template by appending an object to TEMPLATES. A bracketed hint in a field label sets its kind in the thumbnail and the preview (choice, rating, payment, upload, checkbox, date, logic):

js
{ id: "potluck-signup", name: "Potluck sign-up", cat: "events", uses: 1280, accent: "butter",
  desc: "Who's bringing what, with allergy notes.",
  fields: ["Name", "Dish (choice)", "Contains nuts? (checkbox)", "Arrival time (date)"] },

The category chips on templates.html are written in the HTML; add a chip there if you add a category. Integrations take a glyph name from GLYPHS in main.js (wave, leaf, coin, bird, grid, bolt and so on) and an accent (tomato, butter, mint, peri, bubble).

Settings that aren't content are at the top of main.js: TIME_ZONE, KEEP_DEMO_CURRENT, DEMO_ANCHOR, STORAGE_KEYS, SALES_HOURS, WALKTHROUGH, TRIAL_DAYS and ANALYTICS (the numbers in the analytics card).

Images and artwork#

All artwork is inline SVG drawn for the template: the feature block illustrations, the hero doodles, portraits, the school bus, the receipt, the map and the logo. Template thumbnails, integration logos and story charts are generated by main.js (templateArt(), glyph(), storyChart(), portrait()), so they follow the data. If you replace an illustration with a photo, keep a similar aspect ratio (the feature block art is about 2.7:1, the testimonial portrait is square) and add a 2.5px border to match the style.

The social image is assets/img/og-image.png (1200×630), made from og-image.svg. Edit the SVG, then export it to PNG with any SVG tool. The favicon is assets/img/favicon.svg.

The header and footer are repeated in every HTML file. To add a page, copy an existing page, change its <title>, description, Open Graph tags and data-page, and add a link to the <nav aria-label="Main"> list and the mobile menu list (#mobile-menu) in every file. Mark the current page with aria-current="page". The desktop nav shows from 1280px (xl:); below that the menu button opens the mobile menu.

Dark mode#

The moon button in the header ([data-theme-toggle]) switches themes. The choice is saved as tessel:theme ("light" or "dark"). With no saved choice the site follows the visitor's system setting. A small script in each page's <head> adds the dark class to <html> before the page paints; src/input.css redefines the theme tokens under html.dark, keeping the thick borders and shadows (cream on near-black). To ship light only, delete the toggle button and the prefers-color-scheme check in the head script.

Features#

Live form builder#

initBuilder() in main.js, markup in index.html ([data-builder]). Visitors rename the form, add up to MAX_FIELDS (8) fields from FIELD_ORDER, reword each question, toggle Required, move fields up and down and remove them. The right-hand preview renders a real form: required questions show an asterisk, errors appear as you type once a question has been touched (empty required answers, an email without @ and a domain, a phone number with fewer than 10 digits), a progress bar counts required answers, and Submit shows a thank-you screen.

  • On a first visit there are 3 fields (BUILDER_DEFAULT), so the badge reads "3 fields" and the progress line "0 of 2 required answered". Adding Rating makes it "4 fields" with "How did we do?" in the preview.
  • The editor state is saved as tessel:builder:v1; answers are not saved. Reset restores the default.
  • Accessibility: every control is a real button or input with a label; changes are announced in a polite live region ("Rating added. 4 fields."); focus stays on the moved or toggled control.

initTemplates(), templates.html. Search matches name, description, category and field names; chips filter by category or by Saved. The URL keeps ?cat= and ?q= so a filtered view can be shared, and ?preview=<id> opens a template's preview (the home page cards link there). The preview is a native <dialog> that closes with Esc, the close button or a click on the backdrop, and returns focus. The bookmark button saves a template to the shortlist (tessel:saved-templates:v1), and "Use this template" links to signup.html?template=<id>.

Logic demo#

initLogicDemo(), features.html. Four rules show and hide questions: "In person" shows Ticket type and Dietary needs, "Student" shows School email, "Online stream" shows the recording checkbox. Rules that fire turn mint, and the badge reads "2 of 4 rules on" by default. The total is the ticket price from TICKETS when attending in person plus ADDON_TSHIRT ($18): $40.00 by default, $15.00 for Student, $93.00 for Supporter with a T-shirt, $18.00 online with a T-shirt.

Analytics card#

initAnalytics(), features.html. The 7 and 30 day buttons (aria-pressed) swap the numbers in ANALYTICS. Completion is finishes ÷ starts (30 days: 2,288 ÷ 2,966 = 77%). The question with the biggest fall from the one before it is drawn in tomato with a note (30 days: Dietary needs, 14 points).

Pricing calculator#

initPricing(), pricing.html. The slider steps through VOLUME_STEPS (100 to 100,000 responses a month). For each plan, monthly price = base + extra × the number of blocks needed above included; a plan whose max is below the volume shows "—". Annual billing takes ANNUAL_DISCOUNT (20%) off, rounded to whole dollars. The cheapest plan that fits gets the "Best fit" sticker; from 50,000 responses Business is recommended.

Responses Billing Free Maker Team
1,000 (default) annual — $19 $47
1,000 monthly — $24 $59
2,500 monthly — $40 $59
25,000 annual — — $83

The choice is saved as tessel:pricing:v1; ?billing=monthly|annual and ?responses=<a value from VOLUME_STEPS> override it.

Integrations directory#

initIntegrations(), integrations.html. Search covers names, descriptions, categories and the "What syncs" lists; chips filter by category. ?cat= and ?q= are kept in the URL (the features page strip links to integrations.html?q=<name>).

Changelog#

initChangelog(), changelog.html. Releases from CHANGES, newest first, filterable by New, Improved and Fixed. The "Coming next" months are filled in from today (data-months-ahead="1" is next month).

Customer stories#

initCustomers(), customers.html. Six stories from STORIES with an industry filter; each has a before and after bar chart (with a text alternative) and an illustrated portrait. Links like customers.html#hobnail scroll to a story.

Sales hours and walkthrough#

initContact(), contact.html. The badge says "Open now · until 5:30 p.m. CT" during SALES_HOURS (Monday–Friday, 8:00–17:30 Central) and otherwise when sales opens next. The walkthrough card shows the first Thursday at least one day after today at noon Central (on Tuesday, October 27, 2026: "Thursday, October 29 · 12:00 p.m. CT"). Public holidays are not taken into account; add a check in salesStatus() if you need them.

Keeping the demo current#

Demos stay online for years, so nothing dated is allowed to go stale:

  • Changelog dates: while KEEP_DEMO_CURRENT is true, each date in CHANGES moves forward by the whole weeks elapsed since DEMO_ANCHOR (2026-10-27), so weekdays stay right and the newest release always reads as recent. On the anchor date the page shows exactly the dates in data.js. For a real site, set KEEP_DEMO_CURRENT = false and write your real release dates.
  • Roadmap months, walkthrough date, trial end date and the footer year are worked out in the browser from today in Central Time.
  • ?now=YYYY-MM-DDTHH:MM previews any page at another Central time, for example contact.html?now=2027-03-06T10:00.

The mobile menu button toggles aria-expanded, and Esc closes it. Sections fade in as they scroll into view (.reveal, an IntersectionObserver); with reduced motion everything shows at once and button presses don't animate.

Forms#

None of the forms send anything until you connect them. Each one validates on the client, shows inline errors (linked with aria-describedby), and then shows its success state.

Form Where Validates Success state
Sales form contact.html, [data-sales-form] Name, work email, organization, optional 10-digit phone, team size, volume Thanks, the rep who has it (Ysolde Ebbesen for 50,000+ responses or 251+ people, otherwise Leopold Rasmussen), a reference like TS-7K2Q
Create account signup.html, [data-signup-form] Name, email, password of 8+ characters with a number, terms Welcome message, the plan or trial end date, the chosen template, workspace address
Sign in signup.html, [data-signin-form] Email, password "Signed in."
Password reset signup.html, [data-reset-form] Email Reset link note
Monthly roundup changelog.html, [data-subscribe] Email Confirmation note
Builder preview index.html Per field, as described above "Response #… recorded" (demo of the product)

signup.html reads ?plan=free|maker|team (from the pricing buttons) and ?template=<id>; #signin opens the Sign in tab.

To connect the sales form to a service such as Formspree, replace the comment in the sales form's submit handler in main.js with a request, and show the success state only when it answers OK:

js
const response = await fetch("https://formspree.io/f/your-id", {
  method: "POST",
  body: new FormData(form),
  headers: { Accept: "application/json" },
});
if (!response.ok) {
  setError(form, "email", "Something went wrong. Please try again or call (402) 555-0134.");
  return;
}

Make the handler async when you add await. Sign-up and sign-in need a real authentication provider; the places to call it are marked with // To go live: comments.

Saved data (localStorage)#

Key Shape Validated on read
tessel:theme "light" or "dark" Any other value is ignored
tessel:builder:v1 { title, fields: [{ type, label, required }] } Unknown types dropped, strings trimmed to length, at most 8 fields; broken JSON is removed
tessel:saved-templates:v1 ["event-rsvp", ...] Only ids that exist in TEMPLATES, no duplicates
tessel:pricing:v1 { billing, responses } billing must be monthly or annual, responses a value in VOLUME_STEPS

All access goes through safeStorage in main.js, which falls back to memory when the browser blocks storage (private modes, sandboxed iframes), so features keep working for the page view. To reset, clear the site's data in the browser or run localStorage.removeItem("tessel:builder:v1") in the console. URL values are checked with own-key lookups, so values like ?cat=constructor are ignored.

SEO and sharing#

Each page has its own <title>, meta description, Open Graph tags, theme-color and og:locale in its <head>. og:image points at assets/img/og-image.png; most social sites need an absolute URL, so change it to https://your-domain.example/assets/img/og-image.png when you deploy. Add a sitemap.xml listing the pages and a robots.txt that points to it. 404.html is picked up automatically by most static hosts.

Accessibility#

  • Skip link, landmarks, one <h1> per page and headings in order (the sign-up page has a visually hidden <h1> above the tabs).
  • Visible 3px focus outlines; chips show focus around the whole chip.
  • Tabs on signup.html follow the ARIA tabs pattern (arrow keys move between tabs).
  • The preview dialog is a native modal <dialog>; FAQ items are <details>.
  • Toggle buttons use aria-pressed; result counts and the builder use polite live regions.
  • Charts have text alternatives; decorative SVGs are aria-hidden.
  • Text meets WCAG AA in both themes; on accent blocks keep text-coal.
  • Motion is reduced when the visitor asks for it.

When you edit, keep one <h1> per page, give every new input a <label>, and check that new color pairs still pass contrast in dark mode.

Building and deploying#

sh
npm install
npm run build    # one-off, minified
npm run watch    # rebuild on save

Tailwind scans the HTML and JavaScript files for class names, so classes used only in main.js strings are included as long as they are written out in full. Upload the folder (without node_modules, src and package.json if you like) to any static host: Netlify Drop, GitHub Pages, Cloudflare Pages or your own server.

Troubleshooting#

  • A new class has no effect. Rebuild the CSS with npm run build; only classes found in the files are generated. Don't build class names from pieces like "bg-" + color; write the full name.
  • The builder shows old fields. It restores tessel:builder:v1. Click Reset or clear the key.
  • Dark mode turns on by itself. With no saved choice the site follows the system setting; click the toggle once to save a choice.
  • Changelog dates differ from data.js. KEEP_DEMO_CURRENT is moving them forward; set it to false.
  • The sales badge says closed during business hours. Hours are Central Time; change TIME_ZONE and SALES_HOURS in main.js for another office.
  • Social previews show no image. Use an absolute URL in og:image.
  • Fonts look like Arial. The Google Fonts link was removed or blocked; restore it in each page's <head>.

License#

MIT. The code and all included artwork are free for personal and commercial use under LICENSE.txt. Bowlby One (SIL Open Font License) and Ubuntu Sans and Ubuntu Sans Mono (Ubuntu Font Licence) are served by Google Fonts. Tessel, its people, customers, integrations, contact details and figures are fictional.

Search documentation

Search the guides and every template's documentation