`document.currentScript` is more useful than I thought.
After discovering and being unsure of its value, I just realized how useful `document.currentScript` can be in exposing configuration properties to `<script>` elements (and other things too).Every so often, I stumble across a well-established JavaScript API in the browser that I probably should've known about years ago. Examples include the window.screen
property and the CSS.supports()
method. To my relief, I've realized I'm not always alone in my ignorance. I remember posting about window.screen
and getting a surprising amount of feedback from people who also didn't know of it. That made me feel a little less dumb.
I think the awareness of an API has less to do with how long it's been around, and more to do with its applicability to the problems we're trying to solve. If there aren't a ton of cases in which window.screen
is useful, people will tend to forget it exists.
But occasionally, there's the surprise opportunity to use one of these not-so-well-known features. I think I found (at least) one for document.currentScript
, and I intend to savor it for a while.
What's it do?
One glance at the API reveals what provides: a reference to the <script>
element currently executing the code in which it's invoked.
<script>
console.log("tag name:", document.currentScript.tagName);
console.log(
"script element?",
document.currentScript instanceof HTMLScriptElement
);
// tag name: SCRIPT
// script element? true
</script>
Since you get back the element itself, you're able to access properties like you would any other DOM node.
<script data-external-key="123urmom" defer>
console.log("external key:", document.currentScript.dataset.externalKey);
if (document.currentScript.defer) {
console.log("script is deferred!");
}
</script>
// external key: 123urmom
// script is deferred!
Pretty straightforward. And what should be obvious by now, browser support is of zero concern. It's been in all major browsers for over a decade. That’s enough web years to form natural diamonds.
No-Go for Modules
A noteworthy tidbit about document.currentScript
is that it's not available within modules. But oddly enough, you won't get undefined
for attempting to access it. Instead, it'll be set to null
:
<script type="module">
console.log(document.currentScript);
console.log(document.doesNotExist);
// null
// undefined
</script>
This is a deliberate decision noted in the specification. As soon as the document
is constructed, currentScript
is initialized with null
:
ThecurrentScript
attribute, on getting, must return the value to which it was most recently set. When theDocument
is created, thecurrentScript
must be initialized to null.
And since it's switched back to that initial value after the script synchronously executes, you'll also get null
when executing asynchronous code:
<script>
console.log(document.currentScript);
// <script> tag
setTimeout(() => {
console.log(document.currentScript);
// null
}, 1000);
</script>
In light of that, there doesn't seem to be any way of accessing the current script tag from within <script type="module" />
. The best you can do is know whether the script is running in a module, and that's best accomplished by checking for null
(again, only if isn't performed inside any asynchronous activity).
function isInModule() {
return document.currentScript === null;
}
Don't bother checking for import.meta
, by the way, even if it's within a try/catch
. By simply existing in a classic <script>
tag, the browser will throw a SyntaxError
. It doesn't even need to run. It's thrown as the browser first parses the script's contents.
<script>
// Will throw a `SyntaxError` without eveing being invoked!
function isInModule() {
try {
return !!import.meta;
} catch (e) {
return false;
}
};
// Also throws error:
console.log(typeof import?.meta);
</script>
Since modules still lack this sort of feature, it'll be interesting to see how resolving it shakes out. A note in the specification suggests it's an ongoing discussion:
This API has fallen out of favor in the implementer and standards community, as it globally exposesscript
or SVGscript
elements. As such, it is not available in newer contexts, such as when running module scripts or when running scripts in a shadow tree. We are looking into creating a new solution for identifying the running script in such contexts, which does not make it globally available: see issue #1013.
That issue, by the way, is a long-lived one, stretching back to 2016 with a whole bunch of people contributing. Until it lands, I suppose querying for the element is your best bet:
<script type="module" id="moduleScript">
const scriptTag = document.getElementById("moduleScript");
// Do stuff.
</script>
The Need: Passing Configuration Properties
I'm using a Stripe pricing table on PicPerf's website, embeddable by means of a native web component. Load a script, drop the element into your HTML, and set a couple of attributes:
<script
async
src="https://js.stripe.com/v3/pricing-table.js">
</script>
<stripe-pricing-table
pricing-table-id='prctbl_blahblahblah'
publishable-key="pk_test_blahblahblah"
>
</stripe-pricing-table>
That's all well & good if you can pull a couple environment variables when your HTML's being rendered, but I wanted to embed the table in a Markdown file. Markdown supports raw HTML just fine, but accessing those property values isn't as simple as reaching for import.meta.env
or process.env
. Instead, I'd need to dynamically inject the values independently from the markup rendered to page.
Unfortunately, it's not possible to separate rendering of the table from accessing & setting its configuration properties. The values must be available when the element is initialized.
That left me with injecting the entire element (property values & all) from within a client-side script. I'd target a placeholder element in my Markdown, and plop in the finished table markup from there.
## My Pricing Table
<div data-pricing-table></div>
<script>
document.querySelectorAll('[data-pricing-table]').forEach(table => {
table.innerHTML = ``;
})
</script>
At this point, the only thing I'm missing is the attribute values themselves. Server-rendering them onto the window
object was an option, but wouldn't leave a nice feeling in my tummy. It's not keen on shoveling things into the global scope like that.
An Admission
To be very honest, I could've had this solved in about 14 seconds. PicPerf.io is built with Astro, which offers a define:vars
directive. It's stupid-easy to make server-side variables available to client-side scripts:
---
const truth = "Taxation is theft.";
---
<style define:vars={{ truth }}>
console.log(truth);
// Taxation is theft.
</style>
But there is neither fun nor a blog post in a problem solved so quickly.
More than that, define:vars
is a highly proprietary way to address a challenge shared by many other platforms and content management systems (I've worked with them).
A Challenge More Common Than You Might Think
It's very usual for a CMS's constraints to be very narrow by design. An editor may be able to configure bits and pieces of the markup, but very rarely the contents of <script>
tags. That's for good reason. A lot of potential security vulnerabilities there.
On top of that, those scripts often point to external packages shared by other teams, but still require some configuration. In those cases, server-rendering the values within the script isn’t an option at all.
<!-- Shared library, but still requires configuration! -->
<script src="path/to/shared/signup-form.js"></script>
In similar situations, I've seen configuration values made available via server-rendered data attributes. You define the attribute values, and the script takes it from there. Modular single-page applications whose configuration is set on the root node are where I've most often seen it:
<div
id="app"
data-recaptcha-site-key="{{ siteKey }}"
></div>
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const appNode = document.getElementById('app');
const root = ReactDOM.createRoot(appNode);
root.render(
// Use value plucked from root element.
<App recaptchaSiteKey={appNode.dataset.recaptchaSiteKey} />
);
It should be glaringly clear where I'm going with this. Data attributes are a tidy way to pass specific values from the server to the client. In the SPA example above, the only extraordinarily nit-picky downside is the need to first query for the element before being able to access its attributes.
But since my scenario used a <script />
tag instead of any other element, I could eliminate that complaint too. The document.currentScript
property provides it for free:
<script
data-stripe-pricing-table="{{pricingTableId}}"
data-stripe-publishable-key="{{publishableKey}}"
>
const scriptData = document.currentScript.dataset;
document.querySelectorAll('[data-pricing-table]').forEach(table => {
table.innerHTML = ``;
})
</script>
That's feeling pretty nice. There's nothing magically proprietary going on, no values are being jammed into the global scope, and it enables me to gloat on X that I'm "using the platform." Wins all around.
Other Applications
Exploring this feature drew a couple other possible use cases to mind, one of which pitched to me after this was originally published.
Installation Guidance
Say you maintain a JavaScript library that's required to be loaded asynchronously. It's simple to give quick, clear feedback using document.currentScript
:
<script defer src="./script.js"></script>
// script.js
if (!document.currentScript.async) {
throw new Error("This script must be loaded asynchronously!!!");
}
// The rest of the library...
You could even enforce a certain <script>
position on the page. Maybe it has to be loaded just before the opening body tag:
const isFirstBodyChild =
document.body.firstElementChild === document.currentScript;
if (!isFirstBodyChild) {
throw new Error(
"This MUST be loaded immediately after the opening <body> tag."
);
}
There's not much ambiguity in an error like that:

In all, it provides some nice, guided reinforcement. A great counterpart to solid documentation.
Locality of Behaviour
This one was brought up by ShotgunPayDay on Reddit. The Locality of Behavior principle asserts that you should be able to understand a chunk of code by looking at only that chunk of code (Carson Gross has a helpful post on it). Any sort of framework with "single-file components" (SFC) might come to mind. Everything's just there, in one spot.
As it relates to document.currentScript
, it means you can build portable experiences just by virtue of elements being placed next to each other. Here's an example submits any form asynchronously just by placing the script tag right after it. Your central script would know to target the element right before <script>
:
// form-submitter.js
const form = document.currentScript.previousElementSibling;
form.addEventListener("submit", async (e) => {
e.preventDefault();
const formData = new FormData(form);
const method = form.method || "POST";
const submitGet = () => fetch(`${form.action}?${params}`, {
method: "GET",
});
const submitPost = () => fetch(form.action, {
method: method,
body: formData,
});
const submit = method === "GET" ? submitGet : submitPost;
const response = await submit();
form.reset();
alert(response.ok ? "Success!" : "Error occurred!");
});
And, it's just a matter of placing it where you'd like to take effect:
<form action="/endpoint-one" method="POST">
<input type="text" name="firstName"/>
<input type="text" name="lastName"/>
<input type="submit" value="Submit" />
</form>
<script src="form-submitter.js"></script>
<form action="/endpoint-two" method="POST">
<input type="email" name="emailAddress" />
<input type="submit" value="Submit" />
</form>
<script src="form-submitter.js" ></script>
I doubt this pattern is something I'll reach for anytime soon, but it's interesting to know it's on the table.
Feel's Good
There's something very rewarding about finally understanding the utility of some of these web-old, lesser known features. It gives me an appreciation for the API designers of the early and adolescent web, especially knowing how often they need to deal with the arrogant complaints of modern programmers. I'm eager to see what else I'll find. Maybe AGI's already in the HTML spec and we just haven't found it yet.
Get blog posts like this in your inbox.
May be irregular. Unsubscribe whenever.