The 1 file this component needs and nothing else, each fenced with the path it belongs at, followed by what the library's own verification covered and what it did not. No instructions attached — for placing it yourself, or handing it to something that has its own.
Read it first
Install kit: "Signal ticker bar"
Library page: https://astro.baysixmedia.com/components/signal-ticker-bar/
Files: 1 required, 1 supporting
Write each block below to the path named in its header, relative to the project root. A block marked (required) is one the component does not work without; (supporting) is documentation. Nothing here needs downloading except a block that says so in as many words.
===== FILE (required): src/components/SignalTickerBar.astro =====
---
/**
* SignalTickerBar
*
* The thin lime strip that runs across the very top of a page: uppercase,
* tracked, heavy items separated by "+". Static by default, as measured.
* `marquee` opts into a CSS-only scrolling loop that pauses on hover and
* focus and never runs under reduced motion. No script.
*
* Reads --nv-* tokens from any ancestor, with fallbacks.
*/
export type SignalTickerItem = string | { label: string; href?: string };
export interface SignalTickerBarProps {
items?: SignalTickerItem[];
/** Opt-in scrolling loop. Off by default: the reference is static. */
marquee?: boolean;
/** Seconds for one full loop when `marquee` is on. */
duration?: number;
/** Accessible name of the strip's region landmark. */
label?: string;
/** Strip colour. Defaults to the measured ticker lime. */
background?: string;
/** Text colour. */
color?: string;
fontStylesheet?: string;
id?: string;
}
interface Props extends SignalTickerBarProps {}
const {
items = [],
marquee = false,
duration = 38,
label = "Service highlights",
background,
color,
fontStylesheet = "https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700;800&display=swap",
id,
} = Astro.props;
/* The items are the caller's own claims; none are defaulted. An item with no
label is dropped, and no items means no strip at all. */
const list = (items ?? [])
.map((item) => (typeof item === "string" ? { label: item } : item))
.filter((item) => item && typeof item.label === "string" && item.label.trim() !== "");
/* A marquee loop needs each group to be at least as wide as the viewport,
or the seam shows as a gap. Estimate the group width from the copy (12px
800 Manrope, tracked, runs about 8.6px per character) and repeat it
inside the group until it clears a wide 1920px screen. Only the first
copy of the first group is read out; every repeat is aria-hidden. */
const estWidth = list.reduce((w, i) => w + i.label.length * 8.6 + 34, 0) + 64;
const repeats = marquee ? Math.max(1, Math.ceil(1920 / Math.max(estWidth, 1))) : 1;
const style = [
background ? `--stb-bg:${background}` : "",
color ? `--stb-ink:${color}` : "",
marquee ? `--stb-dur:${duration}s` : "",
]
.filter(Boolean)
.join(";");
---
{list.length > 0 && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{list.length > 0 && (
<section
class:list={["stb", marquee && "stb--marquee"]}
id={id}
aria-label={label}
style={style || undefined}
data-stb-root
>
<div class="stb-track">
{
Array.from({ length: marquee ? 2 : 1 }).map((_, g) => (
<div class="stb-group" aria-hidden={g > 0 ? "true" : undefined}>
{Array.from({ length: repeats }).map((_, r) => {
const hidden = g > 0 || r > 0;
return (
<ul class="stb-list" role="list" aria-hidden={hidden && g === 0 ? "true" : undefined}>
{list.map((item) => (
<li class="stb-item">
{item.href ? (
<a class="stb-link" href={item.href} tabindex={hidden ? -1 : undefined}>
{item.label}
</a>
) : (
<span>{item.label}</span>
)}
<span class="stb-sep" aria-hidden="true">+</span>
</li>
))}
</ul>
);
})}
</div>
))
}
</div>
</section>
)}
<style>
.stb {
--stb-bg-v: var(--stb-bg, var(--nv-lime, #b7ff39));
--stb-ink-v: var(--stb-ink, var(--nv-ink, #08110f));
box-sizing: border-box;
position: relative;
display: flex;
align-items: center;
width: 100%;
height: 38px;
overflow: hidden;
background: var(--stb-bg-v);
color: var(--stb-ink-v);
font-family: var(--nv-font, Manrope, ui-sans-serif, system-ui, sans-serif);
}
.stb *,
.stb *::before,
.stb *::after {
box-sizing: border-box;
}
.stb-track {
display: flex;
flex: none;
align-items: center;
width: max-content;
padding-left: 20px;
}
.stb-group {
display: flex;
flex: none;
align-items: center;
}
/* `.stb .stb-list`: beats any host `ul` reset or margin. */
.stb .stb-list {
display: flex;
flex: none;
align-items: center;
margin: 0;
/* The measured 64px between repeats of the message. */
padding: 0 64px 0 0;
list-style: none;
}
.stb-item {
display: flex;
align-items: center;
white-space: nowrap;
font-size: 12px;
font-weight: 800;
line-height: 14.4px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
/* Two spaces either side of the "+" in the reference: about 0.7em at
this size and tracking. */
.stb-sep {
margin: 0 0.72em 0 0.62em;
}
.stb-link {
color: inherit;
text-decoration: none;
border-radius: 3px;
}
.stb-link:hover {
text-decoration: underline;
text-underline-offset: 2px;
}
.stb-link:focus-visible {
outline: 2px solid var(--stb-ink-v);
outline-offset: 2px;
}
/* ---- marquee (opt-in) ---- */
.stb--marquee .stb-track {
padding-left: 0;
}
@media (prefers-reduced-motion: no-preference) {
.stb--marquee .stb-track {
animation: stb-loop var(--stb-dur, 38s) linear infinite;
}
.stb--marquee:hover .stb-track,
.stb--marquee:focus-within .stb-track {
animation-play-state: paused;
}
}
/* Reduced motion: the marquee version sits still, starting where the
static strip does. */
@media (prefers-reduced-motion: reduce) {
.stb--marquee .stb-track {
padding-left: 20px;
}
}
@keyframes stb-loop {
from {
transform: translate3d(0, 0, 0);
}
to {
transform: translate3d(-50%, 0, 0);
}
}
/* ---- phone ---- */
@media (max-width: 809.98px) {
.stb {
height: 36px;
}
.stb-item {
font-size: 11px;
line-height: 13.2px;
}
}
@media (forced-colors: active) {
.stb {
border-bottom: 1px solid CanvasText;
}
}
</style>
===== END src/components/SignalTickerBar.astro =====
===== FILE (supporting): src/components/SignalTickerBar.md =====
# Signal ticker bar
The thin lime strip across the very top of a page: short uppercase, tracked,
heavy items separated by "+". Static by default, as measured on the
reference. `marquee` turns it into a CSS-only scrolling loop.
Catalog page: `/components/signal-ticker-bar`
## Purpose
A one-line proof strip above the header — capacity, speed, a headline stat.
Use it for four or five short claims. It is not an announcement bar with a
close button and not a news feed.
## Files required
**`SignalTickerBar.astro`** — one file, the whole component. No imports, no
script, no npm packages, no images.
`SignalTickerBar.md` — this file. Documentation, not required to run.
## Basic usage
```astro
---
import SignalTickerBar from "../components/SignalTickerBar.astro";
---
<SignalTickerBar
items={["Live carrier capacity", "Customs cleared in 4 hours", { label: "Track a load", href: "/track" }]}
/>
```
Put it first in `<body>`, above the header. It is 38px tall (36px below
810px) and sits in the flow, so an overlay header placed after it needs a
`top` offset that clears it — `GlassNavHeader` defaults to 52px, which is
38px plus the measured 14px gap.
## Props
| Prop | Type | Default | What it does |
| :--- | :--- | :--- | :--- |
| `items` | `(string \| { label, href? })[]` | `[]` | The items — your own claims, never defaulted. An item with `href` renders as a link; one with no label is dropped. No items → the component renders nothing. |
| `marquee` | `boolean` | `false` | Opt-in scrolling loop. The reference is static. |
| `duration` | `number` | `38` | Seconds per loop when `marquee` is on. |
| `label` | `string` | `"Service highlights"` | Accessible name of the strip's region. |
| `background` | `string` | `var(--nv-lime, #b7ff39)` | Strip colour. |
| `color` | `string` | `var(--nv-ink, #08110f)` | Text colour. |
| `fontStylesheet` | `string` | Manrope + Inter on Google Fonts | `""` skips the `<link>`. |
| `id` | `string` | — | Root id. |
## Behaviour
- **Static (default).** One run of the items, left-aligned with a 20px inset,
clipped at the right edge on narrow screens. No transform, no animation.
- **Marquee (`marquee`).** The run is repeated inside a group until the group
is wider than a 1920px screen (estimated from the copy length at build
time), the group is duplicated, and the track translates `0 → -50%` on a
linear infinite CSS animation, so the loop is seamless. It pauses on hover
and while anything inside it has focus (`:focus-within`), and it does not
run at all under `prefers-reduced-motion: reduce` — it then shows the same
still strip as the static version.
- No hover state was measured on the strip. Linked items underline on hover
and show a 2px ink focus ring.
## Accessibility
- A `<section>` with an `aria-label`, holding a real `<ul>`; the "+"
separators are `aria-hidden`.
- In marquee mode only the first copy is exposed: every repeat and the whole
duplicate group are `aria-hidden`, and links inside them get
`tabindex="-1"`, so a screen reader hears the list once and Tab visits each
link once.
- Text is ink on lime, well above 4.5:1.
## Integration notes
- Full width; paints its own background. No wrapper needed.
- If you theme with `--nv-lime`, the strip follows it. The measured ticker
lime (`#b7ff39`) is a shade greener than the page accent (`#c8ff38`); the
fallback keeps the measured value.
- The marquee repeat estimate assumes the default Manrope 800 at 12px. With a
much narrower font the group can end up shorter than a very wide screen;
add items or raise the estimate constant in the frontmatter.
## Images and credits
None. The component uses no images.
## Approximations
- The reference's static strip is offset by a leftover Framer ticker
transform, so its first visible item is cut at the left edge. This strip
starts cleanly at a 20px inset instead.
- The spacing around each "+" is two literal spaces in the reference; here it
is a margin (~0.62em / 0.72em) chosen to match the screenshot's gap, not a
measured value.
- The reference has no marquee. Its duration, pause rules and repeat logic
are this library's.
- Tablet (810–1199px) was not captured; it uses the desktop size.
===== END src/components/SignalTickerBar.md =====
--- VERIFIED IN THE COMPONENT LIBRARY ---
Enrolled for verification but never run.
No verification run has been recorded.
No checks have passed yet.
No usable source fingerprint, so these results cannot be tied to the files below.
--- NOT PROVEN ---
- ClientRouter: not applicable — this component ships no script.
--- WHAT THIS DOES NOT CERTIFY ---
Verification was performed on the unmodified component, in an isolated fixture, in the
library. It says nothing about this project's CSS, routes, layout or data, and nothing
about the component once you adapt it. A verified component is a good starting point,
not a guarantee about the installation you are about to build.
--- TEST IN THIS PROJECT AFTER INSTALLING OR CUSTOMISING ---
- Horizontal overflow at 1440, 1280, 1024, 834, 390 and 320px.
- Keyboard operation end to end, including visible focus.
- The no-JavaScript render.
- prefers-reduced-motion: reduce.
- Two instances on one page, if this project renders more than one.
- If this project uses <ClientRouter />: teardown and reinitialisation across a real navigation.