Tide Widget
Embed a NOAA-accurate tide chart on any website. Free, no API key required.
Live preview — Atlantic City, NJ. Same widget script and embed shape documented on this page.
Overview
The SeaLegs tide widget is a free, embeddable chart of tide predictions for any coastal location. It pulls from the same harmonic data as official NOAA tide tables (1,300+ US stations) plus regional providers worldwide.
The widget ships in two free embed shapes — pick whichever fits your page. Both share the same chart engine and the same data; they're interchangeable.
- Chart iframe — one fixed location, no JavaScript on your page.
- JS embed — one or many charts inline via Shadow DOM, with full control over sizing and DOM placement.
An interactive map widget (Leaflet, click-to-pick discovery) is available on a paid plan. Contact support if you need it.
By embedding the widget you agree to the Widget Terms of Use, which include a maritime safety disclaimer — the widget is for informational use only, not for navigation.
Quick start
The simplest embed is one iframe. Replace lat and lon with your spot:
<!-- SeaLegs LLC Tide Widget — by embedding, you agree to https://www.sealegs.ai/widget-terms -->
<iframe
src="https://cdn.sealegs.ai/tides/chart/index.html?lat=39.355&lon=-74.418&title=Atlantic%20City"
style="width:100%;height:560px;border:0"
title="Tide forecast"
loading="lazy"
referrerpolicy="strict-origin-when-cross-origin"
sandbox="allow-scripts allow-same-origin"></iframe>
<p style="text-align:center;font-size:0.875rem">
Tide forecasts by <a href="https://www.sealegs.ai/tide-widget">SeaLegs</a>
</p>
The widget snaps to the nearest NOAA tide station automatically. The visible <a> link below the iframe is the SEO backlink — iframes don't pass link equity, so keep that line if you want backlink credit.
Embed shapes
Chart iframe
Drop-in chart for one fixed location. Pure iframe — no JavaScript on your page. Sandboxed for safety.
<!-- SeaLegs LLC Tide Widget — by embedding, you agree to https://www.sealegs.ai/widget-terms -->
<iframe
src="https://cdn.sealegs.ai/tides/chart/index.html?lat=41.5&lon=-71.3&station_id=8447930&title=Newport%20Harbor"
style="width:100%;height:560px;border:0"
title="Newport Harbor tide forecast"
loading="lazy"
referrerpolicy="strict-origin-when-cross-origin"
sandbox="allow-scripts allow-same-origin"></iframe>
JS embed
Mount one or many charts inline. The script auto-mounts every [data-sealegs-tides] element on the page; each chart renders inside its own Shadow DOM so styles don't leak in either direction.
<!-- SeaLegs LLC Tide Widget — by embedding, you agree to https://www.sealegs.ai/widget-terms -->
<!-- Once at the top of the page -->
<script src="https://cdn.sealegs.ai/tides/tides_chart_widget.js" type="module" defer></script>
<!-- Anywhere a chart should appear -->
<div data-sealegs-tides
data-lat="39.355" data-lon="-74.418"
style="width:100%;height:540px"></div>
Multiple charts on the same page share one parsed copy of the chart CSS and one font load — the script de-duplicates network requests internally.
Options
Configure the widget via URL parameters (chart iframe) or data-* attributes (JS embed). Same names, same meanings.
| Option | Iframe (URL param) | JS embed (data attr) | Default | Description |
|---|---|---|---|---|
lat |
?lat= |
data-lat= |
39.355 |
Latitude in decimal degrees, −90 to 90. |
lon |
?lon= |
data-lon= |
-74.418 |
Longitude in decimal degrees, −180 to 180. |
station_id |
?station_id= |
data-station-id= |
nearest | Pin the chart to a specific NOAA station ID instead of auto-snapping to nearest. Useful when matching a chart on another reference. |
days |
?days= |
data-days= |
8 |
Forecast window in days. Range 1–14. |
title |
?title= |
— | TIDECAST |
Iframe-only. Header text rendered above the chart. Plain text only (no HTML). |
footer |
?footer= |
— | Powered by SeaLegsAI |
Iframe-only. Footer text. Plain text only. Custom links are a v2 feature. |
data-hide-title-bar |
— | boolean | (shown) | JS-embed-only. When present, hides the station picker title bar. Useful for compact list layouts where you provide your own row header. |
URL params and data attributes are forgiving — missing values fall back to defaults. Out-of-range values (e.g., days=99) are clamped to the supported range.
Sizing
The widget doesn't auto-fit to its content height. The host page controls width and height. Three sensible options:
<!-- Full viewport — best for dedicated tide pages -->
<iframe src="..." style="width:100%;height:100dvh;border:0"></iframe>
<!-- Fixed pixel height -->
<iframe src="..." style="width:100%;height:600px;border:0"></iframe>
<!-- Aspect-ratio — height scales with width -->
<iframe src="..." style="width:100%;aspect-ratio:4/3;border:0"></iframe>
100dvh collapses correctly with mobile browser address bars; 100vh doesn't. Use 100dvh with a 100vh fallback for the broadest support.
For the JS embed, set width and height directly on the <div data-sealegs-tides> host element. The chart engine uses clientHeight to size itself, so an explicit pixel height is required — min-height alone is not enough. Below ~258 px of available chart-body height the layout switches to a "mini" mode with no daystrip cards; below that, ~150 px, the chart simply has too little room and is best avoided.
Iframe security attributes
The recommended embed includes four iframe attributes worth understanding:
| Attribute | Purpose |
|---|---|
sandbox="allow-scripts allow-same-origin" |
Defense-in-depth. Blocks top-navigation, popups, and form submission even if the widget is compromised. Both flags are required — allow-scripts lets the chart engine run; allow-same-origin lets it fetch tide data from the API. |
referrerpolicy="strict-origin-when-cross-origin" |
Sends Referer: https://your-site.com/ (origin only, no path) so we can identify the embedder for usage analytics and abuse mitigation. |
loading="lazy" |
Defers loading until the iframe scrolls near the viewport. No perf hit on long pages. |
title="..." |
Accessibility — required for screen readers to announce the iframe purpose. |
Custom styling (advanced)
The chart engine reads CSS custom properties through the Shadow DOM boundary. Set any of these on the <div data-sealegs-tides> host element (or on a parent) to override the defaults. JS embed only — the iframe shape is sandboxed and can't pick up host-page CSS.
<div data-sealegs-tides
data-lat="39.355" data-lon="-74.418"
style="width:100%; height:540px;
--tides-curve: #06b6d4;
--tides-rising: #16a34a;
--tides-falling: #dc2626;
--tides-now: #f97316;
--tides-bg-mid: #0c1f33;
--tides-text-primary: #e2f3ff;"></div>
CSS custom properties
| Property | What it controls | Default |
|---|---|---|
--tides-radius | Outer panel corner rounding | 24px |
--tides-bg-deep | Deepest sky band color (top of panel) | #050d18 |
--tides-bg-mid | Mid sky band | #0a1929 |
--tides-bg-shallow | Horizon / shallow water band | #1e3a5f |
--tides-text-primary | Header titles, hero numbers | #e6f4ff |
--tides-text-secondary | Y-axis labels, day strip text | rgba(230,244,255,0.62) |
--tides-text-muted | Hour ticks, secondary metadata | rgba(230,244,255,0.38) |
--tides-curve | Tide curve line color | #7dd3fc |
--tides-curve-glow | Curve glow / fill stroke aura | rgba(125,211,252,0.55) |
--tides-fill-top | Water fill top color (under curve) | rgba(125,211,252,0.35) |
--tides-fill-bottom | Water fill fade-out color | rgba(125,211,252,0.02) |
--tides-now | "Now" indicator dot + ring | #fb923c |
--tides-rising | Rising-tide indicators (arrow, hero pill) | #34d399 |
--tides-falling | Falling-tide indicators | #f87171 |
--tides-high | High-tide markers and high values in day strip | #34d399 |
--tides-low | Low-tide markers and low values in day strip | #f87171 |
--tides-wave-back | Back wave layer color | rgba(125,211,252,0.07) |
--tides-wave-mid | Mid wave layer color | rgba(125,211,252,0.12) |
--tides-wave-front | Front wave layer color | rgba(125,211,252,0.18) |
--tides-glass-fill | Day-strip card background fill | rgba(8,18,32,0.72) |
--tides-glass-border | Day-strip card border color | rgba(255,255,255,0.10) |
The chart is intentionally dark-themed — sky-and-water aesthetic is part of the brand. Light-mode variants are not currently supported via CSS variables (you'd need most of the palette to maintain the gradient relationships).
Multiple charts on one page
Both shapes support multiple charts on the same page. With the JS embed, drop as many <div data-sealegs-tides> elements as you need:
<!-- SeaLegs LLC Tide Widget — by embedding, you agree to https://www.sealegs.ai/widget-terms -->
<script src="https://cdn.sealegs.ai/tides/tides_chart_widget.js" type="module" defer></script>
<!-- Marina A -->
<div data-sealegs-tides
data-lat="41.50" data-lon="-71.33"
style="width:100%;height:540px"></div>
<!-- Marina B -->
<div data-sealegs-tides
data-lat="39.355" data-lon="-74.418"
data-station-id="8534720"
style="width:100%;height:540px"></div>
<!-- Marina C -->
<div data-sealegs-tides
data-lat="41.17" data-lon="-71.55"
style="width:100%;height:540px"></div>
The script de-duplicates the chart CSS and font load across all instances. Tide predictions are cached at the CDN edge, so the second chart's data fetch is typically a sub-100ms round-trip.
For iframe embeds, the same approach works — just stack <iframe> elements. Each iframe is its own document, so there's no cross-iframe deduplication, but each one is small (~50 KB total fetch) and edge-cached.
JavaScript API
The JS embed exposes a programmatic mounting API on window.SeaLegs for cases where auto-mount isn't enough — for example, charts created from JSON state, lazy-mounted on tab change, or destroyed and re-mounted with new coordinates.
const el = document.querySelector('#my-chart-host');
const chart = window.SeaLegs.mountTidesChart(el, {
lat: 41.50,
lon: -71.33,
stationId: '8447930',
days: 8,
});
// Update an already-mounted chart
chart.update({ lat: 39.355, lon: -74.418, stationId: '8534720' });
// Tear down
chart.destroy();
mountTidesChart(el, opts) returns a chart instance with update(), destroy(), on(event, fn), and off(event, fn) methods.
The script also exposes its build version as window.SeaLegs.tidesVersion — useful for support reports.
Events
The chart dispatches custom events on its host element so you can react to user interactions. Listen via the host element directly, or via chart.on(name, fn) on the programmatic API:
| Event name | Payload | Fires when |
|---|---|---|
sealegs:tides:ready |
{ stationId } |
The chart has finished its initial mount and the first data fetch is complete. |
sealegs:tides:station-change |
{ stationId, lat, lon } |
The user picks a different station from the dropdown (or your code calls chart.update()). |
sealegs:tides:scrub |
{ time, height } or { time: null } |
The user drags the chart cursor. Payload is the height at the cursor's time. { time: null } fires when the user releases. |
sealegs:tides:favorites-change |
{ favorites: Set<string> } |
The user stars or unstars a station. Favorites are stored in localStorage on the embedder's origin. |
Example: log the current tide height as the user scrubs the cursor.
const el = document.querySelector('[data-sealegs-tides]');
el.addEventListener('sealegs:tides:scrub', (e) => {
if (e.detail.time === null) return; // released
console.log('At', e.detail.time, 'tide is', e.detail.height, 'ft');
});
Platform notes
| Platform | How to embed |
|---|---|
| WordPress | Add a Custom HTML block and paste the embed code. In the classic editor, use the "Text" tab. |
| Squarespace | Add a Code Block (Business plan and above) and paste the embed code. |
| Wix | Use the Embed HTML element from the "Add" menu. Wix renders custom embeds inside a sandboxed iframe at a fixed size, so the iframe shape is the right pick — the JS embed needs control over its host element's height that Wix's iframe doesn't expose cleanly. |
| Webflow | Add an Embed element. Both shapes work; the JS embed is preferred for marina-list pages where you want multiple charts. |
| React / Next.js | For the iframe shape, render a regular <iframe>. For the JS embed, place the script in a useEffect hook or use next/script with strategy="lazyOnload"; render the host divs in JSX. |
| Static HTML | Paste the embed code directly into your HTML file. |
Performance
- The widget script is ~30 KB gzipped. The chart engine and CSS lazy-load on first chart mount.
- Tide predictions are cached at the CloudFront edge, so the API round-trip is typically <100ms.
loading="lazy"on iframes defers loading until they scroll near the viewport — charts below the fold pay zero cost on initial page load.- Multiple JS-embed charts share one parsed copy of the chart CSS and one font load.
Data sources
Tide predictions are sourced from:
- U.S. National Oceanic and Atmospheric Administration (NOAA) Center for Operational Oceanographic Products and Services (CO-OPS) for U.S. coastal stations — same harmonic data as the official NOAA tide tables.
- Regional providers (UKHO, IHO members, EOT20, and others) for non-U.S. regions.
SeaLegs is not affiliated with, endorsed by, or sponsored by NOAA. The widget retains attribution to the underlying data source where required by upstream terms.
Troubleshooting
The chart area is blank
Most common cause: the host element doesn't have an explicit pixel height. The chart engine reads clientHeight at mount time; min-height alone won't size the chart. Set style="height:540px" (or any pixel value) directly on the iframe or the <div data-sealegs-tides>.
The chart loads but the curve is misaligned with the y-axis
Update to the latest widget build — this was a layout-regime bug fixed May 2026. If you're seeing it on a CDN-cached version, hard-refresh (Cmd+Shift+R / Ctrl+Shift+R) to bust the cache.
API returns "missing_widget_id"
The chart iframe handles this transparently — if you're seeing it from your own JS embed, the host page is on a non-SeaLegs origin and the tide API is gating the request. If you need cross-origin JS embed support today, use the chart iframe shape (its origin is cdn.sealegs.ai, which the API allows). For broader gating control on a paid plan, contact support@sealegs.ai.
The widget shows a different location than I configured
The widget snaps your lat/lon to the nearest NOAA station. If a closer station exists than the one you intended, the chart picks the closer one. Pin a specific station with station_id to override.
Support
Questions, bug reports, or commercial / white-label inquiries: support@sealegs.ai
By embedding the widget you agree to the Widget Terms of Use.