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 requiredmin-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-radiusOuter panel corner rounding24px
--tides-bg-deepDeepest sky band color (top of panel)#050d18
--tides-bg-midMid sky band#0a1929
--tides-bg-shallowHorizon / shallow water band#1e3a5f
--tides-text-primaryHeader titles, hero numbers#e6f4ff
--tides-text-secondaryY-axis labels, day strip textrgba(230,244,255,0.62)
--tides-text-mutedHour ticks, secondary metadatargba(230,244,255,0.38)
--tides-curveTide curve line color#7dd3fc
--tides-curve-glowCurve glow / fill stroke aurargba(125,211,252,0.55)
--tides-fill-topWater fill top color (under curve)rgba(125,211,252,0.35)
--tides-fill-bottomWater fill fade-out colorrgba(125,211,252,0.02)
--tides-now"Now" indicator dot + ring#fb923c
--tides-risingRising-tide indicators (arrow, hero pill)#34d399
--tides-fallingFalling-tide indicators#f87171
--tides-highHigh-tide markers and high values in day strip#34d399
--tides-lowLow-tide markers and low values in day strip#f87171
--tides-wave-backBack wave layer colorrgba(125,211,252,0.07)
--tides-wave-midMid wave layer colorrgba(125,211,252,0.12)
--tides-wave-frontFront wave layer colorrgba(125,211,252,0.18)
--tides-glass-fillDay-strip card background fillrgba(8,18,32,0.72)
--tides-glass-borderDay-strip card border colorrgba(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
WordPressAdd a Custom HTML block and paste the embed code. In the classic editor, use the "Text" tab.
SquarespaceAdd a Code Block (Business plan and above) and paste the embed code.
WixUse 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.
WebflowAdd an Embed element. Both shapes work; the JS embed is preferred for marina-list pages where you want multiple charts.
React / Next.jsFor 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 HTMLPaste 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.json for the station catalog and api/prod/datagetter?product=predictions for 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.