Three files, three jobs
HTML defines the heading, button and message. CSS styles their appearance. JavaScript responds to the button click and changes the message. Keeping these jobs in separate files makes the small project resemble real websites and lets you change one concern without rewriting the others.
Create one folder containing index.html, styles.css and script.js. Their filenames and letter case must match the references in index.html. The example below is a single-file preview so you can run it here; the three snippets after it show the separate files for your local folder.
Write the three files
The link element in head loads styles.css. The script element loads script.js; defer waits until HTML parsing is complete before running the script, so the button and message exist when querySelector looks for them. The type="button" attribute prevents the button from accidentally submitting a form if it is later placed inside one.
In script.js, addEventListener attaches a click handler to the button. When clicked, the handler selects the paragraph and replaces its textContent. aria-live="polite" asks assistive technology to announce the updated message without interrupting the current reading task. The code changes text rather than injecting markup.
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Study reminder</title>
<link rel="stylesheet" href="styles.css">
<script src="script.js" defer></script>
</head>
<body>
<main>
<h1>Study reminder</h1>
<button id="reveal" type="button">Show reminder</button>
<p id="message" aria-live="polite">Press the button to reveal a message.</p>
</main>
</body>
</html>styles.css
body { font: 1rem/1.5 system-ui, sans-serif; margin: 2rem; }
button { padding: .6rem 1rem; }script.js
document.querySelector('#reveal').addEventListener('click', () => {
document.querySelector('#message').textContent = 'Take one small step today.';
});Run and troubleshoot
Open index.html in a browser. The heading and button should appear with spacing; clicking the button should change the paragraph. If you remove the CSS link, the content and click behaviour still work but the spacing changes. If you remove the script, the button remains visible but no longer changes the text.
If nothing happens when you click, check that the id values match the selectors, that script.js is saved in the same folder and that the browser Console shows no loading or syntax error. If styles are missing, inspect the href spelling and path. A server is unnecessary for this small local demonstration, although advanced browser APIs may require one.
Syntax
<link rel="stylesheet" href="styles.css">
<script src="script.js" defer></script>Examples
Try the complete page
This self-contained preview performs the same action as the three-file project.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Study reminder</title>
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 2rem; }
button { padding: .6rem 1rem; }
</style>
</head>
<body>
<main>
<h1>Study reminder</h1>
<button id="reveal" type="button">Show reminder</button>
<p id="message" aria-live="polite">Press the button to reveal a message.</p>
</main>
<script>
document.querySelector('#reveal').addEventListener('click', () => {
document.querySelector('#message').textContent = 'Take one small step today.';
});
</script>
</body>
</html>- HTML creates the button and message, so both appear even before the click.
- The style block changes spacing and type; it does not decide what happens when the button is pressed.
- The inline script runs after the button has been parsed, listens for a click and replaces the paragraph text. The separate-file version uses defer because its script is in head.
- The local three-file version moves the same CSS and JavaScript into their own files and loads them with link and script.
Best practices
- Keep linked file paths relative to the HTML file and check their spelling.
- Use defer when a script in head needs parsed page elements.
- Start with meaningful HTML before adding styling or behaviour.
Browser compatibility
Support applies to the named feature, not every attribute or behaviour in this article. Versions refer to browsers, not operating systems.
These records cover HTML elements and script defer, not CSS or JavaScript API support.
<link>
Desktop
- Chrome
- Supported
From 1 - Firefox
- Supported
From 1 - Safari
- Supported
By 4 (possibly earlier) - Edge
- Supported
From 12 - Opera
- Supported
By 12.1 (possibly earlier)
Mobile
- Safari on iOS
- Supported
By 3.2 (possibly earlier) - Chrome for Android
- Supported
From 18 - Firefox for Android
- Supported
From 4 - Samsung Internet
- Supported
From 1.0
MDN BCD record for this feature · MDN reference
Support history and limitations
Chrome
- From 1
Firefox
- From 1
Safari
- By 4 (possibly earlier)
Edge
- From 12
Opera
- By 12.1 (possibly earlier)
Safari on iOS
- By 3.2 (possibly earlier)
Chrome for Android
- From 18
Firefox for Android
- From 4
Samsung Internet
- From 1.0
<script>
Desktop
- Chrome
- Supported
From 1 - Firefox
- Supported
From 1 - Safari
- Supported
From 3 - Edge
- Supported
From 12 - Opera
- Supported
By 12.1 (possibly earlier)
Mobile
- Safari on iOS
- Supported
From 2 - Chrome for Android
- Supported
From 18 - Firefox for Android
- Supported
From 4 - Samsung Internet
- Supported
From 1.0
MDN BCD record for this feature · MDN reference
Support history and limitations
Chrome
- From 1
Firefox
- From 1 · Starting in Firefox 4, inserting <script> elements that have been created by calling document.createElement("script") no longer enforces execution in insertion order. This change lets Firefox properly abide by the specification. To make script-inserted external scripts execute in their insertion order, set .async=false on them.
Safari
- From 3
Edge
- From 12
Opera
- By 12.1 (possibly earlier)
Safari on iOS
- From 2
Chrome for Android
- From 18
Firefox for Android
- From 4
Samsung Internet
- From 1.0
<script> defer
Desktop
- Chrome
- Supported
From 1 - Firefox
- Supported
From 3.5 - Safari
- Supported
From 3 - Edge
- Supported
From 12 - Opera
- Supported
From 15
Mobile
- Safari on iOS
- Supported
From 2 - Chrome for Android
- Supported
From 18 - Firefox for Android
- Supported
From 4 - Samsung Internet
- Supported
From 1.0
MDN BCD record for this feature
Support history and limitations
Chrome
- From 1 · Chrome does not defer scripts with the defer attribute when the page is served as XHTML (application/xhtml+xml), see bug 41253514 and bug 41408348
Firefox
- From 3.5 · Since Firefox 3.6, the defer attribute is ignored on scripts that don't have the src attribute. However, in Firefox 3.5 even inline scripts are deferred if the defer attribute is set.
Safari
- From 3
Edge
- From 12
Opera
- From 15 · Opera does not defer scripts with the defer attribute when the page is served as XHTML (application/xhtml+xml), see bug 41253514 and bug 41408348
Safari on iOS
- From 2
Chrome for Android
- From 18 · Chrome Android does not defer scripts with the defer attribute when the page is served as XHTML (application/xhtml+xml), see bug 41253514 and bug 41408348
Firefox for Android
- From 4
Samsung Internet
- From 1.0 · Samsung Browser does not defer scripts with the defer attribute when the page is served as XHTML (application/xhtml+xml), see bug 41253514 and bug 41408348
<button>
Desktop
- Chrome
- Supported
From 1 - Firefox
- Supported
From 1 - Safari
- Supported
From 1 - Edge
- Supported
From 12 - Opera
- Supported
By 15 (possibly earlier)
Mobile
- Safari on iOS
- Supported
From 1 - Chrome for Android
- Supported
From 18 - Firefox for Android
- Supported
From 4 - Samsung Internet
- Supported
From 1.0
MDN BCD record for this feature · MDN reference
Support history and limitations
Chrome
- From 1
Firefox
- From 1
Safari
- From 1
Edge
- From 12
Opera
- By 15 (possibly earlier)
Safari on iOS
- From 1
Chrome for Android
- From 18
Firefox for Android
- From 4
Samsung Internet
- From 1.0
MDN BCD 8.1.5 · Dataset built 2026-10-08 · Mapping reviewed 2026-10-21. These dates are not individual browser-test or claim-verification dates. Unknown means the data does not establish support.