@sonordev/site-kit/cta-bar
The Liquid Glass mobile CTA bar (6.1.0). A floating frosted capsule that keeps a site's one or two highest-intent actions a thumb away on phones.
It replaces a hand-rolled sticky mobile bar, and handles what each of those ends up solving on its own:
| Behaviour | How |
|---|---|
| Hide while the form it points at is on screen | hideOver |
| Stay off the hero until the hero CTA scrolls away | showAfter |
| Get out from under the Echo launcher | automatic |
| Ride out the iOS toolbar collapsing | --sk-vv-layout-gap |
| Get out of the way of the keyboard | hideWhileTyping |
| Tell Sonor which action converts | cta_click event |
Use it
// app/layout.tsx (a server component)
import Link from 'next/link'
import { Phone, ClipboardCheck } from 'lucide-react'
import { CtaBar, CtaBarAction } from '@sonordev/site-kit/cta-bar'
<SiteKitLayout>
<Header />
<main>{children}</main>
<Footer />
<CtaBar label="Call or request a quote" hideOver="#quote">
<CtaBarAction href="tel:+15555550100" variant="secondary" icon={<Phone />}>
Call now
</CtaBarAction>
<CtaBarAction as={Link} href="/quote" icon={<ClipboardCheck />}>
Free quote
</CtaBarAction>
</CtaBar>
</SiteKitLayout>Rules:
-
Render it once per page: at the layout root for a site-wide bar, or inside the page for a page-specific one. It's a labelled
<aside>, valid at either depth; the kit's axe gate covers both placements. -
Never inside a blurred header.
backdrop-filteron an ancestor becomes the containing block for this fixed bar and clips it. -
Delete the site's own bar and its compensating
padding-bottom. The kit renders a spacer that reserves the bar's height at the end of the page. -
Better: pad the footer instead of adding a strip after it. A spacer after a dark footer is a blank band in the page colour. Pass
spacer={false}and let the footer's own background run under the bar:footer { padding-bottom: calc(2rem + var(--sk-cta-bar-space, 0px)); }--sk-cta-bar-spaceis set on<html>only while a bar is present and below its breakpoint, so desktop and bar-less pages get0px. -
Delete any
--sk-echo-offset-bottomrule written for the old bar. The kit sets it while the bar is on screen.
CtaBar and CtaBarAction are plain components with no hooks, so a server
layout can pass as={Link} and Link children without crossing a client
boundary. The only client code is a childless behaviour island the bar
mounts itself (about 4.4 KB gzipped for the whole module, glass and
analytics included).
<CtaBar>
| Prop | Default | |
|---|---|---|
label | "Quick actions" | Accessible name of the landmark. |
breakpoint | "lg" | Hidden at this width and up: sm 640, md 768, lg 1024, xl 1280, none = every width. |
layout | "fill" | fill stretches actions across the capsule. fit hugs a single action in a centred capsule (the old floating "Request a quote" button). |
showAfter | Selector. Hidden until that element scrolls off the top (the hero CTA). Server-rendered hidden, so it never slides in over the hero during hydration. No match = shown. | |
hideOver | Selector or selectors. Steps aside while any match is on screen: the form it points at, the footer. | |
hideWhileTyping | true | Hidden while a text field has focus, so it never sits on the keyboard. |
compactOnScroll | true | Tightens while scrolling down; an icon-bearing secondary action folds to its icon. Scrolling up restores it. |
echoClearance | true | Lifts the Echo launcher above the bar while the bar is on screen, below the breakpoint only. |
spacer | true | Reserves the bar's height at the end of the page. |
track | true | Sends cta_click (category: engagement, label, properties.href, properties.variant, properties.location = "cta_bar") through the standalone analytics dispatch. |
Client navigation re-finds showAfter and hideOver targets on each route.
<CtaBarAction>
| Prop | Default | |
|---|---|---|
as | a with an href, else button | Any element or component, e.g. Next's Link. Polymorphic since 6.1.3: the action then takes that component's own props (required ones included), so <CtaBarAction as={ScheduleTourButton} values={unit}> type-checks and a missing required prop doesn't. |
variant | "primary" | primary: solid brand. secondary: tinted glass. plain: layout only, bring your own classes. |
icon | Leading icon, hidden from assistive tech. Lucide icons are sized to 18px. | |
collapse | secondary + icon | Fold to icon-only while compact. The label stays in the accessible name. |
Everything else (href, onClick, target, aria-*, data-*) passes
through. A button without an explicit type gets type="button".
Theme it
Colours and geometry are custom properties. Set them on :root so the bar,
its spacer and the Echo clearance all read the same values.
:root {
--sk-cta-primary-bg: var(--brand-primary); /* default: --sk-primary, then #2563eb */
--sk-cta-primary-text: #fff;
--sk-cta-bar-text: var(--ink-900); /* secondary label colour */
}A dark bar: scope the tint to the bar, not :root. The Echo window
reads the same --sk-glass-tint, and its text follows --sk-text-primary
(dark by default). A dark tint on :root puts that dark text on dark glass.
Either scope the bar's colours to the bar:
.sk-cta-bar {
--sk-glass-tint: #0b0a09;
--sk-cta-bar-text: #fff;
--sk-cta-primary-bg: #fff;
--sk-cta-primary-text: #0b0a09;
}or theme the whole kit dark with --sk-bg and --sk-text-primary on
:root, which both surfaces (and site-kit forms) read.
| Token | Default |
|---|---|
--sk-cta-bar-inset | 12px from the screen edges |
--sk-cta-bar-padding | 6px between glass and buttons |
--sk-cta-bar-action-height | 48px (44px tap target minimum; don't go lower) |
--sk-cta-bar-max-width | 520px |
--sk-cta-bar-z | 40 |
--sk-cta-bar-text | --sk-text-primary, then #111827 |
--sk-cta-primary-bg / --sk-cta-primary-text | --sk-primary / #fff |
--sk-cta-secondary-bg / --sk-cta-secondary-text | 8% of the text colour / the text colour |
--sk-glass-* | the shared glass recipe (--sk-glass-tint, --sk-glass-opacity, --sk-glass-blur, --sk-glass-saturate) |
The glass
The surface is the kit's one Liquid Glass recipe (src/shared/glass.tsx),
the same material as the Echo launcher and chat window, so the bar and the
chat always match. The tint is mostly opaque (72%): the tint carries the
contrast, and the blur only softens what shows through. Browsers without
backdrop-filter, and visitors who ask their OS for reduced transparency
or more contrast, get the same surface solid. Reduced motion turns the
transitions off. JS-off visitors get the bar, visible, through
@media (scripting: none).
On phones where the layout viewport runs taller than the visible one
(in-app browsers, toolbar animations), mount VisualViewportGap from
@sonordev/site-kit/client; the bar already adds --sk-vv-layout-gap to
its bottom.