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 an embeddable chart of tide predictions. It ships in two free embed shapes:
- 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.
Free vs paid
Two tiers, same chart engine, different data path under the hood:
- Free tier — widget renders tide predictions from NOAA CO-OPS (the U.S. National Oceanic and Atmospheric Administration). Coverage is U.S. waters and territories, ~3,000 stations. Resolution is hourly. Attribution chrome shows "Powered by SeaLegs". No signup, no API key, no usage caps.
- Paid tier — widget renders from the SeaLegs backend, which aggregates NOAA + UKHO + IHO + EOT20 + regional providers for worldwide coverage. Resolution is 30-minute. White-label branding, full theme control, and an edge SLA. Contact support@sealegs.ai about pricing and onboarding.
The widget chooses its data path automatically based on the embedder's domain — SeaLegs's own pages and paid customers (with a data-widget-id) route to the SeaLegs backend; everyone else routes directly to NOAA. Same script, same embed shapes, no configuration required.
An interactive map widget (Leaflet, click-to-pick discovery) is available on the 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.
Light mode
Both shapes support a daylight blue/white palette suited for embeds on light-themed pages. Use the theme option:
<!-- Iframe — append ?theme=light -->
<iframe src="https://cdn.sealegs.ai/tides/chart/index.html?lat=41.5&lon=-71.3&theme=light" ... ></iframe>
<!-- JS embed — set data-theme="light" on the host -->
<div data-sealegs-tides
data-lat="39.355" data-lon="-74.418"
data-theme="light"
style="width:100%;height:540px"></div>
Anything other than light (including the default empty value) falls back to dark.
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. |
theme |
?theme= |
data-theme= |
dark |
dark (default) or light. Light mode applies a daylight blue/white palette suited for embeds on light-themed pages. Anything other than light falls back to dark. |
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 CSS custom properties above tune the dark palette only. To switch the whole chart to a daylight blue/white skin in one step, use the theme option (?theme=light on the iframe shape, data-theme="light" on the JS embed) instead of overriding individual variables. The two approaches are mutually exclusive: theme=light swaps the entire palette set; piecewise overrides assume the dark base.
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
Where the tide data comes from depends on which tier the embed is running:
- Free tier — widget calls NOAA CO-OPS (the U.S. National Oceanic and Atmospheric Administration's Center for Operational Oceanographic Products and Services) directly from the embedder's browser. Two endpoints:
mdapi/prod/webapi/stations.jsonfor the station catalog andapi/prod/datagetter?product=predictionsfor the predictions themselves. Both have open CORS so they work from any embedder origin. Coverage: U.S. waters and territories. Resolution: hourly samples for the curve, plus exact high/low extrema. SeaLegs caches the station catalog at the page level and de-duplicates fetches across multiple charts on the same page. - Paid tier — widget calls the SeaLegs backend at
api.sealegs.ai/v2/tides. The backend aggregates NOAA + UKHO (UK Hydrographic Office) + IHO member services + EOT20 (a global empirical ocean tide model) + regional hydrographic offices for worldwide coverage. Resolution: 30-minute samples. Edge-cached at CloudFront so per-chart latency is sub-100ms.
SeaLegs LLC is not affiliated with, endorsed by, or sponsored by NOAA or any other government or international body. 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.
"No NOAA tide station within 100 nm of this location"
The free tier covers U.S. waters via NOAA CO-OPS. If your lat/lon is more than 100 nautical miles from the nearest NOAA station — for example, a non-U.S. coastline — the widget can't render. For international coverage, contact support@sealegs.ai about a paid plan; the paid backend aggregates UKHO, IHO, EOT20, and regional providers worldwide.
"Tide data temporarily unavailable"
NOAA's API had a transient blip. The widget retries on every chart re-mount; tell the visitor to refresh in a few minutes. If the problem persists for more than 30 minutes, check NOAA's status at api.tidesandcurrents.noaa.gov. The paid tier routes through the SeaLegs backend's edge cache, which absorbs brief NOAA outages transparently.
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.