Uncategorized

How to Design a Testimonials Section That Builds Trust (Layouts and CSS Examples)

Most testimonial sections fail for the same reason: they look like decoration instead of evidence. A row of anonymous five-star quotes with stock photos gets skimmed and forgotten. A single sentence from a named buyer describing a measurable outcome can carry an entire landing page. This guide is not another gallery of screenshots. It covers the testimonial section design decisions that actually move conversion rates: which layout to use for which page, what makes a quote credible, and the CSS you can paste into your project today. Every snippet below is dependency-free, responsive, and accessible. What is a testimonial section (and what it is really for) A testimonial section is a block on a web page that displays quotes, ratings or short stories from real customers, usually with the person’s name, photo, role and company. Structurally it is a set of <blockquote> elements with attribution. Its job is narrow and specific: to remove a doubt at the exact moment the visitor feels it. That is why placement matters as much as visual design. A testimonial about onboarding speed belongs next to the pricing table, not buried in a carousel at the bottom of the homepage. The credibility checklist: what makes a testimonial believable Before you touch CSS, audit your content. A beautiful section filled with weak quotes converts worse than an ugly section filled with strong ones. Score each testimonial out of 7: Signal Weak version Strong version Full name Sarah M. Sarah Mendel Real photo Stock portrait or initials LinkedIn-style headshot Job title + company Happy customer Head of Engineering, Northline Logistics Specific result Great service, highly recommend Cut our API response time from 800ms to 120ms in three weeks Objection handled Nice team to work with I was worried about migration downtime. We shipped with zero downtime. Verifiable source No link Link to G2 review, LinkedIn profile or case study Length 200+ words of praise 20 to 45 words, one idea Rule of thumb: if a quote could have been written about any company in your industry, it is not a testimonial, it is filler. Delete it. One practical trick: bold the result inside the quote. Readers skim testimonials, so highlighting the number or outcome makes the proof survive a two-second glance. Where to place testimonials: homepage, service page, pricing page Placement is the highest-leverage decision in testimonial section design. Use the doubt each page creates as your map. Page Placement Best layout What the quote must prove Homepage Logo strip directly under the hero, full grid after the second value block Logo strip + 3-card grid Serious companies trust us Service / feature page Inline after each feature explanation Single pull quote (no card) This specific feature delivered a specific result Pricing page Immediately below the plan table, above the FAQ 2 quote cards focused on ROI The price was worth it and there were no surprises Checkout / signup Sidebar next to the form One compact quote + rating Signing up is safe and quick Blog post Mid-article, near the CTA Inline pull quote We practice what we write about Two placement mistakes to avoid: putting the only testimonial section in the footer area where scroll depth drops below 30 percent, and repeating the exact same three quotes on every page so returning visitors stop seeing them. Shared base CSS (paste this first) All layouts below share the same card and attribution styles, so define them once. :root{ –t-bg:#ffffff; –t-border:#e5e7eb; –t-text:#1f2937; –t-muted:#6b7280; –t-accent:#0f766e; –t-radius:14px; } .t-section{padding:72px 20px;max-width:1160px;margin-inline:auto} .t-head{max-width:640px;margin:0 auto 40px;text-align:center} .t-head h2{font-size:clamp(1.5rem,3vw,2.125rem);line-height:1.2;margin:0 0 12px} .t-head p{color:var(–t-muted);margin:0} .t-card{ background:var(–t-bg); border:1px solid var(–t-border); border-radius:var(–t-radius); padding:24px; display:flex; flex-direction:column; gap:18px; box-shadow:0 1px 2px rgba(16,24,40,.06); } .t-card blockquote{margin:0;font-size:1.0625rem;line-height:1.65;color:var(–t-text)} .t-card blockquote strong{color:var(–t-accent)} .t-person{display:flex;align-items:center;gap:12px;margin-top:auto} .t-person img{width:44px;height:44px;border-radius:50%;object-fit:cover;flex:0 0 auto} .t-name{font-weight:600;font-size:.9375rem;line-height:1.3} .t-role{color:var(–t-muted);font-size:.8125rem;line-height:1.3} .t-stars{color:#f59e0b;font-size:.875rem;letter-spacing:2px} And the semantic markup for a single card, reused everywhere: <figure class=’t-card’> <div class=’t-stars’ aria-label=’Rated 5 out of 5′>★★★★★</div> <blockquote>We migrated 14 services in one sprint and <strong>cut hosting costs by 38%</strong>. The rollback plan alone was worth it.</blockquote> <figcaption class=’t-person’> <img src=’/img/sarah-mendel.jpg’ width=’44’ height=’44’ alt=’Sarah Mendel’ loading=’lazy’> <div> <div class=’t-name’>Sarah Mendel</div> <div class=’t-role’>Head of Engineering, Northline Logistics</div> </div> </figcaption> </figure> Use <figure> + <figcaption> rather than a bare <div>. It gives screen readers the relationship between the quote and its author, and it keeps the markup meaningful without extra ARIA. Layout 1: the responsive grid (the safe default) Best for homepages and dedicated testimonial pages. It shows volume of proof at a glance and needs zero JavaScript. .t-grid{ display:grid; gap:24px; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); align-items:stretch; } /* optional: make the first card the hero quote */ @media (min-width:900px){ .t-grid .t-card:first-child{grid-column:span 2} .t-grid .t-card:first-child blockquote{font-size:1.25rem} } Why auto-fit with minmax: the grid reflows from 3 columns to 2 to 1 without a single media query, and cards never get narrower than a readable 280px. Design decisions that matter here Three or six cards, not four. Four cards create an awkward 2+2 on tablet and a lonely orphan on desktop. Equal height, unequal content. margin-top:auto on the attribution row pins names to the bottom so the grid reads as a tidy baseline. Trim quotes to a similar length. Aim for 25 to 45 words each. Wildly different lengths make the section feel unedited. Layout 2: masonry columns for uneven quote lengths If you have great quotes of very different lengths and refuse to trim them, use CSS columns instead of grid. It looks editorial and avoids huge empty gaps. .t-masonry{columns:3 300px;column-gap:24px} .t-masonry .t-card{break-inside:avoid;margin:0 0 24px} @media (max-width:640px){.t-masonry{columns:1}} Trade-off to know: CSS columns flow content top-to-bottom per column, so the DOM order does not match visual reading order left-to-right. Put your strongest testimonial first anyway, since it will land at the top of column one. Layout 3: scroll-snap slider (no JavaScript) Sliders are right when you have 8+ testimonials and limited vertical space, such as a pricing page or a mobile-first landing page. Build it with native scroll snapping instead of a carousel library. Testimonial Graphic Templates tackles the same question from another angle. .t-slider{ display:flex; gap:20px; overflow-x:auto; scroll-snap-type:x mandatory; scroll-behavior:smooth; padding:4px 4px 20px; -webkit-overflow-scrolling:touch; scrollbar-width:thin;

How to Design a Testimonials Section That Builds Trust (Layouts and CSS Examples) Read More »

How to Add a Custom Loading Animation to a Website With CSS (No JavaScript Library)

Almost every tutorial about a CSS loading animation stops at the same place: here is a spinning circle, copy the keyframes, done. What they rarely tell you is that a badly implemented loading screen can make your site feel slower, push your Largest Contentful Paint later, and create layout shift the moment the content finally appears. This tutorial takes the opposite approach. You will build a lightweight preloader and a skeleton screen with pure CSS, hide them with roughly ten lines of vanilla JavaScript, and apply the rules that keep the animation off the browser’s main thread. No spinner library, no animation framework, no extra kilobytes. This discussion raises a few points we skipped. Quick answer: the minimum viable CSS loading animation If you only need the code, here it is. One element, one keyframe, no dependencies: <div class=”spinner” role=”status” aria-label=”Loading”></div> <style> .spinner { width: 48px; height: 48px; border-radius: 50%; border: 4px solid rgba(0, 0, 0, 0.12); border-top-color: #0b63f6; animation: spinner-rotate 0.8s linear infinite; } @keyframes spinner-rotate { to { transform: rotate(360deg); } } </style> That is the whole spinner. The rest of this article is about the part that actually matters: where you put it, when you remove it, and how to keep it from damaging your performance metrics. First decide: do you even need a loading screen? A full-screen preloader hides your content on purpose. On a fast connection, that means you are deliberately delaying the moment the user sees something useful. Use this table as a decision guide before you write a single line of CSS. Expected wait What to show Why Under 100 ms Nothing The action feels instant. An indicator that flashes for 80 ms only creates visual noise. 100 ms to 1 s Subtle inline state (button spinner, dimmed area) Confirms the click was registered without blocking the page. 1 s to 10 s Skeleton screen or spinner Users need proof that something is happening. Skeletons win for content, spinners for actions. Over 10 s Determinate progress bar with text An infinite spinner for 12 seconds reads as “broken”, not “loading”. Rule of thumb: content pages should almost never use a full-screen preloader. Dashboards, editors, canvas apps and anything that needs to boot before it is usable are the legitimate use cases. Part 1: a full-screen preloader in pure CSS Step 1: the markup Put the overlay as the first element inside the body, before your app markup. It must be able to paint before anything else. <body> <div id=”preloader” class=”preloader” role=”status” aria-live=”polite”> <div class=”preloader__spinner” aria-hidden=”true”></div> <span class=”visually-hidden”>Loading page content</span> </div> <main id=”app”> <!– your real content –> </main> </body> Step 2: inline the critical CSS This is the step most tutorials skip. If your preloader styles live in an external stylesheet that arrives late, the user sees an unstyled flash of your raw HTML first. Put the overlay CSS inline in the <head>, and keep it tiny. <head> <style> .preloader { position: fixed; inset: 0; z-index: 9999; display: grid; place-items: center; background: #ffffff; opacity: 1; transition: opacity 0.3s ease, visibility 0.3s ease; } .preloader.is-hidden { opacity: 0; visibility: hidden; pointer-events: none; } .preloader__spinner { width: 44px; height: 44px; border-radius: 50%; border: 4px solid #e3e8ef; border-top-color: #0b63f6; animation: preloader-rotate 0.8s linear infinite; } @keyframes preloader-rotate { to { transform: rotate(360deg); } } .visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; } @media (prefers-reduced-motion: reduce) { .preloader__spinner { animation: preloader-fade 1.4s ease-in-out infinite; } @keyframes preloader-fade { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } } } </style> </head> Notice three details: The animation only touches transform, which the browser can composite without repainting. The exit uses opacity plus visibility, not display: none, so the fade actually renders. Users who requested reduced motion get a gentle opacity pulse instead of a rotating element. Step 3: hide it with a few lines of vanilla JavaScript No library needed. Listen for the load event, add a class, then remove the node from the DOM so it never intercepts clicks. (function () { var preloader = document.getElementById(‘preloader’); if (!preloader) return; function hidePreloader() { if (preloader.classList.contains(‘is-hidden’)) return; preloader.classList.add(‘is-hidden’); document.body.classList.remove(‘is-loading’); setTimeout(function () { if (preloader.parentNode) preloader.parentNode.removeChild(preloader); }, 400); } window.addEventListener(‘load’, hidePreloader); // Safety net: never trap the user behind the overlay setTimeout(hidePreloader, 5000); })(); The safety net timeout is not optional. If a third party script hangs, a slow font never resolves, or an image 404s in a way that stalls the load event, your visitors would stare at a spinner forever. Five seconds is a reasonable ceiling. Step 4: handle the no-JavaScript case If JavaScript is blocked or fails to parse, the overlay stays on screen and your site looks dead. Add a pure CSS escape hatch that fades the preloader out on its own: .preloader { animation: preloader-selfdestruct 0s linear 8s forwards; } @keyframes preloader-selfdestruct { to { opacity: 0; visibility: hidden; } } Or, even simpler and stricter: render the overlay itself from a <noscript>-aware class that JavaScript adds to <html> at the very top of the page. If JS never runs, the overlay is never shown. There is more on it in The Progress CSS Loaders Collection. Part 2: the skeleton screen (usually the better choice) A skeleton screen shows the shape of the content before the content exists. Research on perceived performance consistently shows that people rate skeletons as faster than spinners for the same real duration, because the interface looks like it is already assembling itself. Skeleton markup <article class=”card” aria-busy=”true”> <div class=”skeleton skeleton–thumb”></div> <div class=”skeleton skeleton–line” style=”width: 80%”></div> <div class=”skeleton skeleton–line” style=”width: 95%”></div> <div class=”skeleton skeleton–line” style=”width: 60%”></div> </article> Skeleton CSS with a compositor-friendly shimmer .skeleton { position: relative; overflow: hidden; background: #e9edf2; border-radius: 6px; } .skeleton–thumb { aspect-ratio: 16 / 9; margin-bottom: 12px; } .skeleton–line { height: 0.9em; margin-bottom: 8px; } .skeleton::after { content: “”; position: absolute; inset: 0; transform: translateX(-100%); background: linear-gradient( 90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.65) 50%, rgba(255, 255, 255,

How to Add a Custom Loading Animation to a Website With CSS (No JavaScript Library) Read More »

How to Design a Careers Page That Attracts Better Job Applicants

Most careers pages fail for the same three reasons: they say nothing specific, they bury the open roles, and they hand the applicant a ten-minute form. Good careers page design is not about winning a design award. It is about turning a curious visitor into a qualified applicant in under two minutes, and making sure your roles show up where candidates actually search. This guide breaks down the sections a careers page needs, the copy that goes inside each one, the layout that works on mobile, and the structured data that lets your jobs appear in Google’s job search results. It is written for small business and agency sites, so nothing here needs a six-figure recruitment marketing budget. What a careers page is actually for Before layout, get clear on the job the page has to do. A careers page has four visitors, and each one needs something different: The active applicant who already wants a job with you and just needs the list and the form. The passive candidate who found you through a job listing and is checking whether you are a real, decent company. The referral sent by a friend who works there, looking for the role their friend mentioned. The search engine, which needs machine-readable job data to index and surface your roles. A page that only serves the first visitor becomes a bare list of links. A page that only serves the second becomes a brand film with no jobs on it. You need both, in the right order. The anatomy of a high-converting careers page Section What it does Recommended length 1. Hero States who you are and links straight to open roles Headline + 1 sentence + 1 button 2. Culture blurb Gives candidates a reason to self-select in or out 80 to 150 words 3. Benefits grid Answers “what do I get” at a glance 6 to 9 tiles 4. Open roles list The conversion engine of the page One row per role, no pagination under 25 roles 5. Hiring process Removes anxiety, reduces drop-off before applying 3 to 5 steps with timing 6. Proof Team quotes, photos, review scores 2 to 3 short quotes 7. Application form Captures the applicant with minimum friction 5 to 7 fields 8. Talent pool fallback Catches interest when nothing is open Email + role interest, 2 fields Section 1: The hero The hero is where most careers pages waste their best real estate on a stock photo of people high-fiving. Keep it functional. The point is broken down further in this analysis. Layout Left-aligned text block, image or short muted video on the right (or a full-width photo of your actual team behind a dark overlay). One primary button: View open roles, anchored to the jobs list further down the page. A small counter is useful and honest: “7 open roles across 3 teams”. On mobile, stack text above the image and keep the button above the fold. Copy Write the headline as a statement about the work, not about the company’s ambition. Compare: Weak: “Join our journey to change the world.” Better: “Build software for 400 independent pharmacies, in a team of nine.” Specific numbers do two things: they filter out mismatched applicants and they signal that a real human wrote the page. Section 2: The culture blurb This is the section that separates a careers page from a job board listing. It should be short, concrete, and slightly uncomfortable to write, because it should say something a competitor cannot copy and paste. What to include How you work: meeting load, remote or hybrid policy with actual days, async or synchronous, tooling. How decisions get made: who signs off on what, how much autonomy a new hire really gets. Who thrives here: one sentence describing the person who does well, and one describing who does not. That last point is the highest-value sentence on the whole page. “If you want a structured onboarding programme and a defined career ladder, we are probably too small for you right now” will lose you applicants you did not want and win you trust with the ones you did. Career page design: How to do it right covers this in more depth. Layout tip Break the blurb into two or three short paragraphs of 40 words or fewer, or use a two-column layout with a heading on the left and text on the right. Walls of text in this section get skipped on mobile. Section 3: The benefits grid Candidates scan this. Design for scanning. Layout rules Three columns on desktop, two on tablet, one on mobile. Equal-height cards. Icon or small illustration, a 3 to 5 word label in bold, then one line of detail underneath. Six to nine tiles. Fewer looks thin, more looks like padding. Never use an icon font that fails to load. Inline SVG is safer and faster. Copy rules Quantify everything you can. “Generous holiday” means nothing. “28 days plus public holidays, plus the week between Christmas and New Year” means something. Same for learning budgets, equipment allowances, pension contributions and parental leave. Vague Specific Flexible working Core hours 10:00 to 15:00, the rest is yours Great equipment Laptop of your choice plus a 400 EUR home desk budget Room to grow 1,500 EUR per year for courses and conferences, no approval needed under 300 EUR Competitive salary Published band on every job listing Section 4: The open roles list This is where the page earns its keep. Treat it as a product listing, not a footer link dump. Structure Group by department if you have more than six roles. Otherwise a single flat list is faster to scan. Each row shows: job title, department, location, employment type, and salary band if you publish one. The whole row is clickable, not just the title. Minimum touch target of 44 pixels in height. Filters only if you have 15+ roles. Below that, filters add friction rather than remove it. Every role gets

How to Design a Careers Page That Attracts Better Job Applicants Read More »

How to Create a Mega Menu for a Website With HTML and CSS (No Plugin Needed)

If your site has more than about 25 pages, a single-level dropdown starts to hurt. Links get buried, users scroll inside tiny menus, and your best content never gets clicked. A mega menu fixes that by showing an entire section of your site in one wide panel, organised into columns, with headings, icons and featured links. This tutorial shows how to design a mega menu from the information architecture stage right through to production-ready HTML, CSS and a small JavaScript layer for keyboard and touch users. No page builder, no jQuery, no 200 KB plugin. Everything here works in every modern browser and degrades gracefully. What a Mega Menu Is (and When You Actually Need One) A mega menu is a large, rectangular dropdown panel that opens from a top-level navigation item and displays many links at once, grouped into visible columns and categories. Nielsen Norman Group research has consistently shown that these panels outperform traditional cascading dropdowns because everything is visible in one glance instead of hidden behind hover-and-pray sub-levels. Use a mega menu when: You have 4 or more top-level sections, each with 5 to 20 sub-pages. Your categories benefit from visual grouping (product families, documentation topics, industries, resource types). You want to promote specific pages such as pricing, a free trial, a flagship guide or a new release. Analytics show users going straight to search because the nav does not surface what they need. Skip it when you have fewer than 15 destination pages. A mega menu on a small site is just noise. Mega Menu vs Standard Dropdown Criteria Standard dropdown Mega menu Links visible at once 5 to 10 15 to 40 Grouping and labels Flat list Column headings, icons, descriptions Sub-levels needed Often 2 or 3 deep One level, no cascading Promotion space None Featured card, CTA, image Risk Hidden content Overload if unstructured Step 1: Design the Structure Before You Write Any Code The biggest mistake in mega menu projects is opening a code editor first. Spend 30 minutes on paper or in a spreadsheet instead. Export your page list from your sitemap or CMS. Group pages into 3 to 5 columns per top-level item. Each column gets a short, noun-based heading such as “Hosting”, “Developer tools”, “By industry”. If a column needs more than 8 links, split it or move something to a landing page. Order links by demand, not alphabetically. Put your top traffic and top revenue pages first in each column. Reserve one slot for a featured block per panel: latest release, popular guide, pricing, demo booking. Write link labels as users would search them. “API documentation” beats “Resources for builders”. Cap the panel at roughly 30 links. Beyond that, add a “View all” link to a full section index page. Visual rules that make a mega menu readable Keep panel height under about 480 px so nothing gets cut off on a 13-inch laptop. Use one accent colour for hover states, not five. Icons should be 20 to 24 px, monochrome, and only used when they help identification. Decorative icons on every link slow scanning. Give each link 8 to 12 px vertical padding so touch targets stay usable. Left-align everything. Centered columns are much harder to scan. Add a soft drop shadow and a light top border so the panel reads as a layer above the page. Step 2: The HTML Skeleton Semantic markup does most of the accessibility work for you. Use a <nav>, a real list, and a <button> for anything that opens a panel. Never use a bare <div> or an empty <a href=”#”> as a trigger. <nav class=”mm” aria-label=”Main navigation” data-mega> <a class=”mm-logo” href=”/”>Acme</a> <button class=”mm-burger” aria-expanded=”false” aria-controls=”mm-list”> <span class=”sr-only”>Open menu</span> <span class=”mm-burger-bars” aria-hidden=”true”></span> </button> <ul class=”mm-list” id=”mm-list”> <li class=”mm-item mm-has-panel”> <button class=”mm-trigger” aria-expanded=”false” aria-controls=”panel-products”> Products <span class=”mm-caret” aria-hidden=”true”></span> </button> <div class=”mm-panel” id=”panel-products”> <div class=”mm-grid”> <div class=”mm-col”> <h2 class=”mm-heading”>Hosting</h2> <ul class=”mm-links”> <li><a href=”/shared”> <svg class=”mm-ico” aria-hidden=”true” viewBox=”0 0 24 24″><use href=”#i-server”></use></svg> <span>Shared hosting<span class=”mm-desc”>Best for small sites</span></span> </a></li> <li><a href=”/vps”><span>VPS</span></a></li> <li><a href=”/dedicated”><span>Dedicated servers</span></a></li> </ul> </div> <div class=”mm-col”> <h2 class=”mm-heading”>Developer tools</h2> <ul class=”mm-links”> <li><a href=”/cli”><span>CLI</span></a></li> <li><a href=”/api”><span>REST API</span></a></li> <li><a href=”/webhooks”><span>Webhooks</span></a></li> </ul> </div> <div class=”mm-col”> <h2 class=”mm-heading”>By use case</h2> <ul class=”mm-links”> <li><a href=”/ecommerce”><span>Ecommerce</span></a></li> <li><a href=”/agencies”><span>Agencies</span></a></li> <li><a href=”/saas”><span>SaaS teams</span></a></li> </ul> </div> <div class=”mm-col mm-featured”> <p class=”mm-tag”>New</p> <h2 class=”mm-feat-title”>Edge deploys in 12 seconds</h2> <p class=”mm-desc”>See the benchmark and migration guide.</p> <a class=”mm-btn” href=”/edge”>Read the guide</a> </div> </div> </div> </li> <li class=”mm-item”><a class=”mm-link” href=”/pricing”>Pricing</a></li> <li class=”mm-item”><a class=”mm-link” href=”/contact”>Contact</a></li> </ul> </nav> Notes on the markup: aria-expanded tells screen readers whether the panel is open. JavaScript flips it. aria-controls links the trigger to the panel ID. Column headings use real heading tags so the panel has an outline. If your page heading levels conflict, use <p class=”mm-heading”> plus role=”group” and aria-label on the column instead. Icons carry aria-hidden=”true” because the text already labels the link. Step 3: The CSS That Does the Heavy Lifting The trick for a full-width panel is simple: the parent <nav> is position: relative, the list items stay position: static, and the panel is absolutely positioned with left: 0; right: 0. That stretches the panel edge to edge no matter where the trigger sits. :root{ –mm-ink:#16181d; –mm-muted:#6b7280; –mm-accent:#0b5fff; –mm-line:#e5e7eb; –mm-radius:10px; } .mm{ position:relative; display:flex; align-items:center; gap:24px; padding:0 24px; background:#fff; border-bottom:1px solid var(–mm-line); font-family:system-ui,-apple-system,”Segoe UI”,sans-serif; } .mm-list{display:flex;gap:2px;margin:0;padding:0;list-style:none} .mm-item{position:static} .mm-trigger,.mm-link{ display:flex;align-items:center;gap:7px; padding:20px 14px; background:none;border:0; font:inherit;font-weight:600;font-size:.95rem; color:var(–mm-ink);text-decoration:none;cursor:pointer; } .mm-trigger:hover,.mm-link:hover, .mm-trigger[aria-expanded=”true”]{color:var(–mm-accent)} .mm-caret{ width:7px;height:7px; border-right:2px solid currentColor; border-bottom:2px solid currentColor; transform:rotate(45deg) translateY(-2px); transition:transform .2s ease; } .mm-trigger[aria-expanded=”true”] .mm-caret{transform:rotate(225deg) translateY(2px)} /* The panel */ .mm-panel{ position:absolute;left:0;right:0;top:100%; z-index:60; padding:34px 24px 38px; background:#fff; border-top:1px solid var(–mm-line); box-shadow:0 26px 50px rgba(15,23,42,.12); opacity:0;visibility:hidden; transform:translateY(-10px); transition:opacity .18s ease,transform .18s ease,visibility .18s; } /* Three ways to open: mouse, keyboard, JS class */ .mm-has-panel:hover > .mm-panel, .mm-has-panel:focus-within > .mm-panel, .mm-has-panel.is-open > .mm-panel{ opacity:1;visibility:visible;transform:translateY(0); } /* Columns */ .mm-grid{ display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:34px; max-width:1200px;margin:0 auto; } .mm-heading{ margin:0 0 14px; font-size:.72rem;font-weight:700; letter-spacing:.09em;text-transform:uppercase; color:var(–mm-muted); } .mm-links{margin:0;padding:0;list-style:none} .mm-links li+li{margin-top:4px} .mm-links a{ display:flex;gap:11px;align-items:flex-start; padding:8px 9px;border-radius:var(–mm-radius); color:var(–mm-ink);text-decoration:none;font-size:.93rem; } .mm-links a:hover,.mm-links a:focus-visible{background:#f2f6ff;color:var(–mm-accent)} .mm-ico{flex:0 0 20px;width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.6} .mm-desc{display:block;margin-top:2px;font-size:.8rem;font-weight:400;color:var(–mm-muted)} /* Featured card */ .mm-featured{background:#f7f9fc;border:1px solid var(–mm-line);border-radius:14px;padding:22px} .mm-tag{display:inline-block;margin:0 0

How to Create a Mega Menu for a Website With HTML and CSS (No Plugin Needed) Read More »

How to Add a Video Background to a Website Without Killing Page Speed

A video background website looks great in a design mockup and terrible in a Lighthouse report, at least when it is built the lazy way. Drop a 40 MB MP4 into a hero section, hit publish, and you have just traded 1.5 seconds of Largest Contentful Paint for a bit of motion that most mobile users will never see. It does not have to work that way. This is a practical, performance-first tutorial: the exact HTML5 markup, the encoding commands, the poster image strategy, the mobile fallback, the Express server headers, and the measurements that prove the whole thing did not hurt your Core Web Vitals. We will also cover the part most tutorials skip: when a looping video is the wrong tool and a CSS animation or an animated image is smarter. Dynamic Websites: 5 Excellent Examples of Video Backgrounds covers this in more depth. What a video background website really costs Before writing a line of code, put a number on the decision. A hero video is a bandwidth purchase, and you should know the price. Approach Typical weight (10s loop) Risk to LCP Unoptimised 1080p MP4 straight from the camera or stock site 20 MB to 60 MB Severe Properly encoded H.264 MP4 + WebM, 1280px wide, no audio 700 KB to 2 MB Low if deferred Animated GIF 5 MB to 20 MB Severe (blocking image decode) CSS or SVG animation 2 KB to 30 KB None Rule of thumb: the video file should never be the first thing the browser downloads, and it should never exceed roughly 2 MB for a desktop hero. If your creative concept cannot survive that budget, change the concept, not the budget. Step 1: Decide whether you actually need video Most of the sites you find in a “best video background websites” gallery use footage that carries real meaning: a product in motion, a place, a process, people. If your loop is an abstract gradient, drifting particles, or slow-moving smoke, you are paying megabytes for something a few lines of CSS can fake. If the content is… Use Why Real footage: product, location, people, manufacturing HTML5 <video> loop Only video reproduces real-world detail credibly Abstract motion: gradients, blobs, grain, waves CSS animation, canvas or animated SVG Kilobytes instead of megabytes, resolution independent Short 2 to 3 second UI demo Muted MP4 or animated AVIF/WebP Far smaller than GIF at the same quality Anything on a page whose only job is conversion speed Static image Fastest paint, zero autoplay edge cases And one hard rule: never ship a GIF for a background. A silent MP4 or WebM is roughly ten to twenty times smaller for the same visual result, and the browser can decode it on the GPU. Step 2: Cut and prepare the source clip Editing decisions matter more than encoder settings. Before you touch ffmpeg: Keep it between 6 and 12 seconds. Longer loops rarely get watched and multiply file size linearly. Choose low-motion footage. Slow pans, shallow depth of field and static camera positions compress dramatically better than handheld shots with fast movement or falling confetti. Strip the audio track entirely. A background video must be muted to autoplay anyway, so an audio track is pure waste. Make the loop seamless. Cut on a matching frame, or add a short cross-dissolve between the end and the start so the jump is invisible. Design for the overlay. If text sits over the video, pick footage with a calm area, then darken it with a CSS gradient so contrast ratios stay accessible. Check the licence. Free libraries such as Pexels, Pixabay, Coverr and Mixkit are fine for commercial use, but read the terms for logos, faces and trademarks visible in the frame. Step 3: Encode the video properly Two files cover the whole modern browser landscape: an MP4 (H.264) for universal support and a WebM (VP9 or AV1) that is typically 25 to 50 percent smaller where supported. Recommended output targets Setting Desktop hero Tablet / small viewport Width 1600 to 1920 px 1280 px Frame rate 24 to 25 fps 24 fps Audio None None File size ceiling 2 MB 1 MB Pixel format yuv420p yuv420p ffmpeg commands you can copy H.264 MP4, with the moov atom moved to the front so playback can start before the file finishes downloading: ffmpeg -i source.mov -an \ -vf “scale=1600:-2,fps=25” \ -c:v libx264 -profile:v high -pix_fmt yuv420p \ -crf 27 -preset slow -g 50 \ -movflags +faststart \ hero-1600.mp4 VP9 WebM (broad support, good ratio): ffmpeg -i source.mov -an \ -vf “scale=1600:-2,fps=25” \ -c:v libvpx-vp9 -crf 36 -b:v 0 -row-mt 1 \ hero-1600.webm AV1 WebM if your toolchain has SVT-AV1 (smallest files, slower encode): ffmpeg -i source.mov -an \ -vf “scale=1600:-2,fps=25″ \ -c:v libsvtav1 -crf 40 -preset 6 -pix_fmt yuv420p \ hero-1600-av1.webm Raise the CRF value until you see artefacts, then step back one. Because the video sits behind a dark overlay and moving text, you can usually push CRF several points higher than you would for a video the user is meant to study. Generate the poster image from the first frame ffmpeg -i hero-1600.mp4 -ss 00:00:00.5 -frames:v 1 poster.png cwebp -q 72 poster.png -o hero-poster.webp avifenc –min 24 –max 34 poster.png hero-poster.avif Extracting the poster from the encoded video, not from the raw source, guarantees the still frame and the first video frame match. No flash, no colour shift when playback starts. Step 4: The HTML5 markup Every attribute below is there for a reason. <section class=”hero”> <video class=”hero__video” poster=”/media/hero-poster.avif” muted loop playsinline preload=”none” disablepictureinpicture aria-hidden=”true” tabindex=”-1″> <source data-src=”/media/hero-1600.webm” type=”video/webm”> <source data-src=”/media/hero-1600.mp4″ type=”video/mp4″> </video> <div class=”hero__overlay”></div> <div class=”hero__content”> <h1>Ship your Express app faster</h1> <p>Minimal, unopinionated, production ready.</p> <a class=”btn” href=”/get-started”>Get started</a> </div> </section> What each attribute does muted: mandatory. Chrome, Safari and Firefox all block autoplay with sound. playsinline: prevents iOS from opening the video full screen. Without it, your background becomes a takeover on iPhone. loop: continuous playback, no manual restart logic. preload=”none”: the browser fetches nothing until

How to Add a Video Background to a Website Without Killing Page Speed Read More »

How to Design an About Us Page for a Small Business (Structure and Examples)

Most small business owners treat the About page as an afterthought. It gets written last, filled with three vague paragraphs about “passion for quality”, and never touched again. Meanwhile, analytics keep showing the same thing: the About page is often the second or third most visited page on the site, and visitors who read it convert at a far higher rate than those who do not. That gap is the opportunity. Good about us page design is not decoration. It is a structured trust argument that answers three silent questions in the visitor’s head: Who am I dealing with? Can they actually do this? What happens if I contact them? This guide breaks down the seven blocks that make an About page convert, with layout notes for each one, plus templates you can copy, common mistakes, and a checklist before you publish. The point is broken down further in this analysis. Why the About Page Is a Sales Page, Not a Filler Page Think about when people actually click “About”. It is almost never at the start of a visit. It happens after someone has seen your pricing, read a service page, or landed on a blog post and thought “this might be the one”. They click About to reduce risk before contacting you. That means your About page has a job in the funnel: Confirm legitimacy. Real people, real address, real history. Differentiate. Why you and not the three other quotes they are collecting. Reduce friction. Show the next step so they do not go back to Google. A page designed with those three jobs in mind looks very different from a page that opens with “Founded in 2014, we are a company committed to excellence.” The one rule that changes everything Write about your customer, prove it with yourself. Every claim about your team should end with an implied benefit for the reader. “12 years in commercial plumbing” is a fact. “12 years in commercial plumbing, which is why we can quote a retrofit without three site visits” is a sales argument. The 7-Block About Page Structure Here is the skeleton. You can drop a block if it does not apply, but keep the order, because it mirrors how trust is built in a conversation. # Block Job on the page Suggested length 1 Positioning hero Say what you do and for whom in one sentence 1 headline + 2 lines 2 Founder story Create human connection and explain the “why” 150 to 300 words 3 Credibility bar Numbers, logos, certifications, press 3 to 6 items 4 Team photos Put faces on the promise 3 to 9 people 5 Values or method Show how you work, not adjectives 3 or 4 items 6 Timeline or milestones Prove longevity and momentum 4 to 8 entries 7 Closing CTA Convert the trust you just built 1 primary + 1 secondary Block 1: The Positioning Hero Do not open with a stock photo of a handshake and the word “About Us”. Open with a sentence that a stranger could repeat to a friend. What to write Use this formula: We help [specific audience] [achieve specific outcome] through [what you actually do], since [year] in [location]. Examples: “We install and maintain heat pumps for homeowners across Bristol. Family run since 2011, 2,400 systems fitted.” “A three-person studio building Shopify stores for independent food brands. Based in Lyon, shipping worldwide.” Layout notes Left-aligned headline at 40 to 56px on desktop, 28 to 32px on mobile. Support it with one real photograph: your storefront, your workshop, your team in the room. Never a generic stock image. Keep the hero under 60% of viewport height so the next block is visible without scrolling on laptops. If you have a strong number (years, clients, projects), place it here as a small caption under the headline. Block 2: The Founder Story This is the block most small businesses get wrong, either by skipping it or by writing an autobiography. The founder story is not about you. It is about the problem you kept running into, which is the same problem your reader has right now. A four-beat structure that works The trigger. What situation made you start? “After nine years managing a warehouse, I kept seeing the same inventory errors cost small retailers thousands.” The insight. What did you realise that others missed? The decision. What did you build or change in response? The bridge to the reader. “If that sounds like your situation, that is exactly who we work with.” Layout notes Two-column layout: portrait photo on one side, text on the other. Portrait should be at least 600px wide, natural light, no heavy filters. Keep the text column to 60 to 75 characters per line. Long lines kill readability on this kind of narrative block. Add a short pull quote in larger type to break the wall of text. Sign it. A handwritten-style signature and a first name make the block feel like a letter rather than copy. Optional: a 45 to 90 second video introduction. Keep it short and unpolished rather than long and corporate. Block 3: The Credibility Bar Immediately after the emotional block, hit the rational one. A reader who just felt something needs a reason to believe it. What counts as a credibility signal Quantified results: projects completed, clients served, tons recycled, average response time. Third-party proof: Google review rating with count, industry association memberships, certifications, insurance details. Client logos: six to eight, grayscale, evenly spaced. If you cannot show logos, write “trusted by teams in [industries]”. Press or awards: even a local newspaper feature works. Guarantees: warranty length, refund policy, service level. Layout notes Use a full-width band with a contrasting background so it reads as a separate “proof zone”. Three or four stat cards in a row on desktop, stacked two-by-two on mobile. Number large, label small. Never invent round numbers. “1,200+ jobs completed” is believable. “Millions of happy customers” is not. Link certifications to the issuing body

How to Design an About Us Page for a Small Business (Structure and Examples) Read More »

How to Add a Print Stylesheet to a Website So Pages Print Correctly

Most websites look great on screen and terrible on paper. Menus eat half a page, a sidebar pushes the article into a narrow column, dark backgrounds burn through ink, and links become useless because the URL is invisible. A CSS print stylesheet fixes all of that in about 60 lines of code. This tutorial gives you a complete, copy-paste print stylesheet, explains every block so you can adapt it, shows how to preview the result in Chrome DevTools without wasting paper, and ends with a WordPress-specific section on exactly where to enqueue the file. What a CSS print stylesheet actually does A print stylesheet is a set of CSS rules that the browser applies only when the page is sent to a printer or saved as a PDF. It uses the print media type, which browsers activate during printing and during print preview. CSS-Tricks Finally Gets A Print Stylesheet covers this in more depth. Two things matter here: Print styles are additive. Your screen CSS still applies unless you override it. That is why every practical print stylesheet starts by resetting colors, widths and floats. Saving as PDF from the browser uses the same print stylesheet. Fixing printing also fixes “Save as PDF”, which is how most people actually “print” today. Two ways to add print CSS (and which one to pick) Method Code Best for @media print block inside your main CSS @media print { … } Small to medium sites, one HTTP request, easiest to maintain Separate file loaded with a media attribute <link rel=”stylesheet” href=”/css/print.css” media=”print”> Large sites, CMS themes, keeping print rules isolated from screen rules Both Separate file that contains one @media print wrapper Safest option: the rules stay scoped even if the file gets concatenated by a build tool or a cache plugin Recommendation: create print.css, wrap its content in @media print { }, and load it with media=”print”. Browsers download it at low priority, so it does not block your screen rendering, and the wrapper protects you from build pipelines that merge stylesheets. The copy-paste CSS print stylesheet Drop this into print.css. It is intentionally generic: adjust the selectors in the “hide” block to match your markup and you are done. @media print { /* ———- 1. Page geometry ———- */ @page { size: A4 portrait; margin: 15mm 12mm; } /* ———- 2. Readable black on white ———- */ html, body { background: #fff !important; color: #000 !important; font-size: 12pt; line-height: 1.45; width: auto !important; margin: 0 !important; padding: 0 !important; float: none !important; } body { font-family: Georgia, “Times New Roman”, serif; } * { background: transparent !important; color: #000 !important; box-shadow: none !important; text-shadow: none !important; } /* ———- 3. Hide screen-only interface ———- */ nav, aside, header nav, .site-header, .site-navigation, .main-navigation, .menu-toggle, .sidebar, .widget-area, .breadcrumbs, .site-footer, .comments-area, .comment-respond, .share-buttons, .social-links, .related-posts, .newsletter, .cookie-banner, .back-to-top, .ad, .ads, .advertisement, [id*=”google_ads”], [class*=”adsbygoogle”], iframe, video, audio, form, button, .no-print { display: none !important; } /* Utility: force something to appear only on paper */ .print-only { display: block !important; } /* ———- 4. Give the content the full page ———- */ .content, .site-content, .entry-content, main, article { width: 100% !important; max-width: 100% !important; margin: 0 !important; padding: 0 !important; float: none !important; display: block !important; } /* Kill sticky and fixed elements, they repeat on every page */ * { position: static !important; } /* ———- 5. Links: show the destination ———- */ a, a:visited { text-decoration: underline; } a[href^=”http”]:after { content: ” (” attr(href) “)”; font-size: 90%; word-wrap: break-word; } /* Do not print URLs that are useless on paper */ a[href^=”#”]:after, a[href^=”javascript:”]:after, a[href^=”mailto:”]:after, a[href^=”tel:”]:after, .no-url:after { content: “”; } abbr[title]:after { content: ” (” attr(title) “)”; } /* ———- 6. Page breaks ———- */ h1, h2, h3, h4, h5, h6 { break-after: avoid; break-inside: avoid; page-break-after: avoid; /* legacy fallback */ } p, blockquote, li { orphans: 3; widows: 3; } img, figure, table, pre, blockquote { break-inside: avoid; page-break-inside: avoid; /* legacy fallback */ } .page-break { break-before: page; page-break-before: always; } /* ———- 7. Media and tables ———- */ img { max-width: 100% !important; height: auto !important; } figure { margin: 0 0 1em; } figcaption { font-size: 10pt; font-style: italic; } table { width: 100% !important; border-collapse: collapse; } th, td { border: 1px solid #000; padding: 4pt 6pt; } thead { display: table-header-group; /* repeats headers on each page */ } tr { break-inside: avoid; } pre, code { font-family: “Courier New”, monospace; font-size: 10pt; white-space: pre-wrap; word-wrap: break-word; border: 1px solid #999; } blockquote { border-left: 3pt solid #000; padding-left: 8pt; } } Rule by rule: what each block is doing 1. @page controls the paper, not the HTML The @page at-rule sets the physical sheet: size and margins. Use A4 for most of the world, letter for North America, or omit size entirely and let the user choose in the print dialog. Margins in mm or in are more predictable than pixels because paper is a physical medium. You can also target specific pages: @page :first { margin-top: 25mm; } @page :left { margin-right: 20mm; } Browser support for headers, footers and page counters inside @page is still limited in Chrome, Firefox and Safari, so do not rely on @bottom-center { content: counter(page) } for anything critical. The browser adds its own header and footer, and the user can toggle those in the print dialog. 2. Black text on white paper Dark themes, colored cards and gradient hero sections cost ink and reduce contrast on paper. Resetting background: transparent and color: #000 with !important is blunt but effective. Also switch to point units for print. 12pt body text with 10pt captions is a safe, readable baseline. Pixels work, but points map directly to what printers understand. For the wider picture, see CSS print page styling. If you have a chart, a logo or a color-coded badge that must keep its color, opt back in: .chart, .brand-logo { -webkit-print-color-adjust: exact; print-color-adjust: exact; background: initial !important; }

How to Add a Print Stylesheet to a Website So Pages Print Correctly Read More »

How to Add a Notification Bar to Your Website With HTML, CSS, and JavaScript

A website notification bar is one of the highest-return components you can ship in an afternoon: a single strip at the top of the page that announces a sale, a shipping delay, a new release, or a maintenance window. Most articles about notification bars point you to a paid widget or a plugin that loads 80 KB of JavaScript. This guide does the opposite: you get roughly 40 lines of your own code, no dependencies, and full control over styling, dismissal logic and performance. By the end of this tutorial you will have a bar that is sticky, dismissible, remembers the dismissal with localStorage, does not cause layout shift, and works with keyboards and screen readers. What a website notification bar is (and when to use one) A notification bar (also called an announcement bar, hello bar, sticky bar or top banner) is a full-width horizontal strip, usually pinned to the top or the bottom of the viewport, containing one short message and often one link or button. Good use cases: Promotions: “Free shipping over $50”, “20% off until Sunday” Shipping and delivery notices: carrier delays, cut-off dates, holiday closures Status and maintenance: scheduled downtime, incident updates Product and content announcements: new version, new feature, webinar Policy notices: pricing changes, terms updates Bad use cases: anything that needs more than one sentence, anything that requires a decision (use a modal or a dedicated page), or three messages competing in the same bar. What we are building Feature How it is handled Sticks to the top on scroll position: sticky (no JS) Dismissible Close button + Escape key Stays dismissed localStorage with an ID and an expiry date No flash, no layout shift Tiny blocking script in <head> that sets a class on <html> Accessible Real <button>, labels, focus styles, 44px target Weight Under 2 KB of HTML, CSS and JS combined Step 1: The HTML markup Place the bar as the first element inside <body>, before your header. Keeping it in the normal document flow is what lets us avoid layout shift later. 10 Website Notification Bar Examples that Get Results covers this in more depth. <body> <div class=”notice-bar” id=”siteNotice” role=”region” aria-label=”Site announcement”> <div class=”notice-bar__inner”> <p class=”notice-bar__text”> Free shipping on every order over $50. <a href=”/shipping”>See the details</a> </p> <button type=”button” class=”notice-bar__close” aria-label=”Dismiss announcement”>&times;</button> </div> </div> <header>…</header> <main id=”main”>…</main> </body> Three details that matter: The close control is a real <button>, not a <div> or an <a href=”#”>. It is focusable and operable with Enter and Space for free. aria-label=”Dismiss announcement” gives screen reader users something better than “times”. The wrapper uses role=”region” with a label so the bar is reachable in a landmark list. Do not use role=”alert” for a static promotional message; that is for urgent, dynamic content. Step 2: The CSS (sticky, responsive, no jump) :root { –notice-bg: #0f172a; –notice-fg: #ffffff; –notice-link: #7dd3fc; } .notice-bar { position: sticky; top: 0; z-index: 1000; background: var(–notice-bg); color: var(–notice-fg); font-size: 0.95rem; line-height: 1.4; } .notice-bar__inner { position: relative; max-width: 1200px; margin: 0 auto; padding: 0.75rem 3.25rem 0.75rem 1rem; } .notice-bar__text { margin: 0; text-align: center; } .notice-bar__text a { color: var(–notice-link); text-decoration: underline; } .notice-bar__close { position: absolute; top: 50%; right: 0.25rem; transform: translateY(-50%); width: 44px; height: 44px; border: 0; border-radius: 6px; background: transparent; color: inherit; font-size: 1.5rem; line-height: 1; cursor: pointer; } .notice-bar__close:hover { background: rgba(255,255,255,0.12); } .notice-bar__close:focus-visible { outline: 2px solid #fff; outline-offset: 2px; } /* The dismissed state, driven by a class on <html> */ .notice-hidden .notice-bar { display: none; } @media (max-width: 600px) { .notice-bar { font-size: 0.85rem; } .notice-bar__text { text-align: left; } } Why position: sticky instead of position: fixed A sticky element still occupies space in the document flow. The rest of the page is laid out below it from the very first paint, so nothing is ever pushed down and your Cumulative Layout Shift stays at zero. A fixed element is removed from the flow and overlaps your header until you manually compensate with padding. Positioning Stays visible on scroll Needs body padding Best for static No No Low-priority notices sticky Yes No Almost every case (recommended) fixed Yes Yes Bars injected by a third-party script If you really need a fixed bar Measure it and expose the height as a CSS variable so the body can compensate without hard-coded numbers: .notice-bar–fixed { position: fixed; top: 0; left: 0; right: 0; } body { padding-top: var(–notice-height, 0px); } html { scroll-padding-top: var(–notice-height, 0px); } /* anchors stay visible */ var bar = document.querySelector(‘.notice-bar–fixed’); function syncHeight() { document.documentElement.style.setProperty(‘–notice-height’, bar.offsetHeight + ‘px’); } syncHeight(); if (‘ResizeObserver’ in window) { new ResizeObserver(syncHeight).observe(bar); } window.addEventListener(‘resize’, syncHeight); Step 3: The JavaScript (dismiss + localStorage) Put this at the end of the body or in a deferred script file. (function () { ‘use strict’; var NOTICE_ID = ‘free-shipping-2026-08’; // change this to bring the bar back var STORAGE_KEY = ‘siteNotice’; var REMEMBER_DAYS = 30; var bar = document.getElementById(‘siteNotice’); if (!bar) { return; } function isDismissed() { try { var data = JSON.parse(localStorage.getItem(STORAGE_KEY) || ‘null’); return !!data && data.id === NOTICE_ID && data.expires > Date.now(); } catch (e) { return false; // private mode, storage disabled, corrupted value } } function dismiss() { document.documentElement.classList.add(‘notice-hidden’); try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ id: NOTICE_ID, expires: Date.now() + REMEMBER_DAYS * 86400000 })); } catch (e) {} // keep keyboard users from losing their place var target = document.querySelector(‘main’) || document.body; target.setAttribute(‘tabindex’, ‘-1’); target.focus({ preventScroll: true }); } if (isDismissed()) { document.documentElement.classList.add(‘notice-hidden’); } bar.querySelector(‘.notice-bar__close’).addEventListener(‘click’, dismiss); document.addEventListener(‘keydown’, function (e) { if (e.key === ‘Escape’ && !document.documentElement.classList.contains(‘notice-hidden’)) { dismiss(); } }); })(); Why store an object instead of a simple boolean The ID lets you publish a new message next month without every returning visitor staying blind to it. Change NOTICE_ID, the bar reappears for everyone. The expiry stops a dismissal from lasting forever. Thirty days is a sensible default for promotions; use one or two days for shipping and status notices. The try/catch matters more than people expect. localStorage throws in some privacy modes and when the

How to Add a Notification Bar to Your Website With HTML, CSS, and JavaScript Read More »

How to Create an Anchor Link Menu That Scrolls Smoothly (HTML, CSS, and JS)

Anchor links in HTML, explained in 30 seconds An anchor link (also called a jump link or in-page link) is a hyperlink that sends the visitor to a specific spot on a page instead of loading a new document. It needs two pieces: A target: any element carrying a unique id attribute, for example <h2 id=”pricing”>. A link: an <a> element whose href is the id preceded by a hash, for example <a href=”#pricing”>Pricing</a>. <!– The link –> <a href=”#pricing”>Jump to pricing</a> <!– The target, anywhere on the same page –> <h2 id=”pricing”>Pricing</h2> That is the whole mechanism. The old <a name=”pricing”> syntax still works in browsers, but it was dropped from the HTML spec, so use id on the real element instead. The problem is that raw anchor links behave badly on modern sites: the browser jumps instantly, the target heading ends up hidden behind a fixed header, and nothing in the menu tells the reader where they are. This tutorial fixes all three issues with about 20 lines of CSS and a small script. Anchor link syntax: every variation you will need Goal HTML Result Jump to a section on the same page <a href=”#setup”>Setup</a> Scrolls to the element with id=”setup” Jump to a section of another page <a href=”/docs/routing#params”>Params</a> Loads the page, then scrolls to #params Back to the top <a href=”#top”>Top</a> Scrolls to the top even without an element named top Link on an external domain <a href=”https://example.com/guide#faq”>FAQ</a> Opens the URL and scrolls to #faq Placeholder link (no jump) <a href=”#” role=”button”>Open</a> Avoid it, use <button> for actions Rules for the id value It must be unique on the page. Duplicated ids make the browser pick the first match and break your highlight logic. Use lowercase letters, digits and hyphens: getting-started, not Getting Started. No spaces. If you must use one in a URL, it becomes %20, which is fragile. Ids are case sensitive in the fragment: #Setup will not match id=”setup”. One detail that trips up backend developers: the fragment after the # is never sent to the server. If you serve pages with Express, your route handler sees /docs/routing and nothing else. Anchor scrolling is 100% a browser job, which is why every fix below lives in CSS and JavaScript. Step 1: mark up the sections Put the id on the element you actually want at the top of the viewport. In most layouts that is the <section> wrapper rather than the heading, because the section has padding you want to keep visible. <main> <section id=”install”> <h2>Install</h2> <p>…</p> </section> <section id=”routing”> <h2>Routing</h2> <p>…</p> </section> <section id=”middleware”> <h2>Middleware</h2> <p>…</p> </section> </main> Step 2: build the menu An in-page menu is a navigation landmark, so wrap it in <nav> and give it a label. Screen reader users then get a real table of contents instead of a pile of links. See echoecho.com for their take. <nav class=”toc” aria-label=”On this page”> <ul> <li><a href=”#install”>Install</a></li> <li><a href=”#routing”>Routing</a></li> <li><a href=”#middleware”>Middleware</a></li> </ul> </nav> Step 3: turn the jump into a smooth scroll (pure CSS) You do not need a library for this. One CSS declaration on the scrolling container handles it, and it works in every current browser. html { scroll-behavior: smooth; } /* Respect users who ask for less motion */ @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } } Two things worth knowing: Apply it to html, not body. If a wrapper element owns the scrollbar (overflow: auto), put the rule on that element instead. Always add the prefers-reduced-motion guard. Long animated scrolls trigger nausea for some users and it is a documented accessibility requirement. Step 4: stop the sticky header from covering your headings This is the number one complaint with anchor links html tutorials never mention. With a fixed or sticky header of 72px, the browser scrolls the target to y=0, which is behind the header. The fix is scroll-margin-top on the targets. :root { –header-height: 72px; } /* Any element that can be an anchor target */ [id] { scroll-margin-top: calc(var(–header-height) + 16px); } scroll-margin-top tells the browser to leave that much space above the element when it scrolls it into view. It works for clicks, for keyboard navigation and for a page loaded directly with a hash in the URL, which is exactly what the old JavaScript offset hacks failed to do. Do not use these old workarounds Old trick Why it fails Empty <span id=”x”> pushed up with negative margins Breaks layout, invisible targets confuse assistive tech padding-top + negative margin-top on every section Creates dead click zones over the previous section window.scrollTo(y – offset) on click Does nothing for direct hits on a URL that already contains a hash Header height that changes on mobile If your header shrinks on small screens, redeclare the variable in a media query. The scroll offset follows automatically. @media (max-width: 768px) { :root { –header-height: 56px; } } Step 5: highlight the link for the section being read An anchor menu without an active state is half a feature. Use IntersectionObserver rather than a scroll listener: it is cheaper, it does not fire hundreds of times per second and it does not jank on mobile. const links = Array.from(document.querySelectorAll(‘.toc a[href^=”#”]’)); const map = new Map(); links.forEach(link => { const target = document.querySelector(link.getAttribute(‘href’)); if (target) map.set(target, link); }); const setActive = (link) => { links.forEach(l => { l.classList.toggle(‘is-active’, l === link); if (l === link) { l.setAttribute(‘aria-current’, ‘true’); } else { l.removeAttribute(‘aria-current’); } }); }; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) setActive(map.get(entry.target)); }); }, { /* Activate a section when it crosses the upper third of the screen */ rootMargin: ‘-25% 0px -65% 0px’, threshold: 0 }); map.forEach((link, section) => observer.observe(section)); The rootMargin value is the part you tune. It shrinks the detection box to a horizontal band near the top of the viewport, so a section becomes active when its content reaches reading position, not when one pixel of it appears at the bottom. The matching CSS: .toc

How to Create an Anchor Link Menu That Scrolls Smoothly (HTML, CSS, and JS) Read More »

How to Design an FAQ Page That Answers Questions and Ranks on Google

Most FAQ pages fail for the same reason: they were written by the marketing team to sound reassuring, not by the support team to solve problems. The result is a long wall of questions nobody asked, buried in a collapsed accordion that Google barely rewards and users abandon in seconds. This guide shows you how to design an FAQ page that does two jobs at once: it deflects support tickets and it earns visibility in search results and AI answers. We will cover question sourcing, grouping, accordion patterns, layout decisions, internal linking, FAQ schema in its current (much narrower) role, when to split FAQs across multiple pages, and the mistakes that make FAQ pages useless. What a good FAQ page is actually for Before touching layout, be clear about the job. A well-designed FAQ page serves four purposes: Self-service: a user finds the answer without contacting you. Objection handling: pricing, refunds, security, contracts, delivery times. The doubts that block a purchase. Discovery: long-tail queries that bring in people who did not know your brand yet. Routing: when the answer is complex, the FAQ sends people to the right documentation, pricing page or support form. If a question does not serve at least one of those goals, it does not belong on the page. Step 1: Source real questions, not imagined ones The single biggest quality difference between a great FAQ page and a filler one is where the questions came from. Pull them from data you already own. Source What to extract Why it matters Support tickets and email The 20 most repeated requests of the last 90 days Highest ticket-deflection value Live chat and phone logs Exact customer wording, including slang and typos Gives you natural-language phrasing Internal site search Queries with zero results or high exit rate Reveals content gaps Search Console Question-shaped queries with impressions but low CTR Demand you already almost rank for SERP features People Also Ask, related searches, autocomplete Confirms how the market phrases the problem Sales calls Objections raised before signing Directly affects conversion Rule: keep the customer’s words in the question, and put your terminology in the answer. A visitor searches “why is my invoice different from the price I saw”, not “proration policy overview”. Score each question before you publish it Give every candidate question a quick score from 1 to 5 on two axes: frequency (how often it is asked) and impact (does it block a purchase or generate a ticket). Publish anything scoring 7 or more. Park the rest in a backlog and revisit quarterly. The team at zendesk.com reached a similar conclusion. Step 2: Choose your FAQ architecture This is the layout decision that most teams get wrong. There are three viable models. Model Best when Structure Single FAQ page Fewer than about 25 questions, short answers One page, 3 to 6 categories, accordions with anchor links FAQ hub plus category pages 25 to 100 questions across distinct topics /faq/ hub linking to /faq/billing/, /faq/shipping/, etc. Knowledge base with dedicated answer pages 100+ questions, or answers needing screenshots and steps Searchable base, one URL per question, FAQ page as entry point When to split FAQs across pages Split when any of these is true: The answer needs more than 150 words or requires steps, screenshots or a video. It becomes a guide, and the FAQ entry becomes a two-line summary plus a link. Two audiences are mixed. Pre-sales questions and existing-customer troubleshooting have different intents. Separate them, or at minimum separate them into clearly labelled sections. The page passes roughly 3,000 words or 40 accordion items. Scannability collapses and the page stops being about anything specific. A single question has real search volume. If “how long does shipping take to Canada” gets searched hundreds of times a month, it deserves its own URL that can rank, not a hidden accordion row. Different teams own different sections. Splitting reduces edit conflicts and keeps content fresh. Keep questions on one page when they are short, closely related and mostly consumed as a group (returns, exchanges and refunds, for example). Step 3: Group questions the way users think Never publish a flat, ungrouped list. Group by the user’s mental model, not your internal org chart. By stage of journey: Before you buy, Setup, Daily use, Billing, Cancelling. By topic: Pricing, Security, Integrations, Shipping, Returns. By audience: Developers, Agencies, Enterprise buyers. By product: only if your products are genuinely different, otherwise it fragments the page. Practical rules that hold up in usability testing: Aim for 3 to 7 categories. More than that and the category nav becomes its own puzzle. Aim for 4 to 10 questions per category. A category with one question is not a category. Order categories by demand, not alphabetically. Put the most-asked group first. Inside a category, put the highest-volume question first. Do not save the good stuff for the bottom. Label categories with plain nouns (“Billing and invoices”), not clever headings (“The money bit”). Step 4: Layout and the accordion question Accordions are the default pattern for a reason: they let a user scan 30 questions in one screen. But they have real costs, and how you implement them decides whether your page works. When accordions are the right choice Answers are short and independent of each other. Users arrive knowing roughly what they want (“I need the refund policy”). You have more than about 8 questions in a section. When to skip accordions You have fewer than 6 questions. Just show the answers. Users need to compare answers side by side (use a table instead). The page is a landing page where reassurance needs to be visible without a click. Accordion rules that prevent damage Question text must be the full question, not a truncated label. “Refunds” is not a question. “Can I get a refund after 30 days?” is. Make the whole row clickable, not just the tiny chevron, and give it a minimum touch height of 44px. Allow multiple panels open at once.

How to Design an FAQ Page That Answers Questions and Ranks on Google Read More »

About Us

Express Jam Studio was founded in 2004 by John Smith. John had previously worked for a courier company, but he saw an opportunity to start his own business in the web design and development industry.

Contact Info

Copyright © 2022 Express Jam Studio. All Rights Reserved.