Astro’s define:vars directive passes values from a component’s frontmatter into its <style> or <script> tag. On a style tag, each key becomes a CSS custom property you read with var(--key), scoped to that component instance. Use it when a style depends on a prop, such as a brand colour, a column count or a background image, without falling back to inline style attributes.
Key Takeaways
<style define:vars={{ accent }}>makesvar(--accent)available in that component’s CSS.- Each instance gets its own values, so the same component can render in different colours.
- On a
<script>,define:varsmakes the scriptis:inline: no bundling, no TypeScript, repeated per instance.- For scripts, pass data through
data-*attributes instead.
Basic use
---
// src/components/Badge.astro
interface Props { color?: string; text?: string }
const { color = '#2563eb', text = 'white' } = Astro.props;
---
<span class="badge"><slot /></span>
<style define:vars={{ color, text }}>
.badge {
background: var(--color);
color: var(--text);
padding: 0.25rem 0.6rem;
border-radius: 999px;
}
</style>
<Badge>Default</Badge>
<Badge color="#16a34a">Live</Badge>
<Badge color="#dc2626">Deprecated</Badge>
Astro sets the variables in a style attribute on the component’s root elements, so each instance resolves var(--color) to its own value. The stylesheet itself is shared and cached.
Useful patterns
Layouts driven by props:
---
const { columns = 3, gap = '1.5rem' } = Astro.props;
---
<div class="grid"><slot /></div>
<style define:vars={{ columns, gap }}>
.grid {
display: grid;
grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
gap: var(--gap);
}
</style>
Background images from content:
---
import { getImage } from 'astro:assets';
import heroSrc from '../assets/hero.jpg';
const hero = await getImage({ src: heroSrc, width: 1600, format: 'webp' });
const bg = `url(${hero.src})`;
---
<section class="hero"><slot /></section>
<style define:vars={{ bg }}>
.hero { background-image: var(--bg); background-size: cover; }
</style>
Keys must be valid as custom property names; camelCase works, and Astro uses the name as written.
On scripts: the catch
define:vars also works on <script>, but it changes how the script is handled:
<script define:vars={{ apiUrl }}>
console.log(apiUrl); // works, but this script is now inline
</script>
A script with define:vars is implicitly is:inline. That means it isn’t bundled, imports and TypeScript don’t work, and it’s repeated in the HTML for every instance of the component. For anything beyond a line or two, pass data through attributes and keep a normal processed script:
---
const { apiUrl } = Astro.props;
---
<div class="widget" data-api-url={apiUrl}></div>
<script>
document.querySelectorAll<HTMLElement>('.widget').forEach((el) => {
const apiUrl = el.dataset.apiUrl;
// ...
});
</script>
When not to use it
- Values that never change per instance. Put them in a global token file as plain CSS custom properties. See the token section in the Astro styling guide.
- Tailwind projects. Arbitrary values (
bg-(--accent)in Tailwind 4,bg-[var(--accent)]in Tailwind 3) plus astyleattribute on the element often read more clearly. - Client-side changes.
define:varsis set at render time. To change a value in the browser, set the custom property from a script withel.style.setProperty('--color', value).
For a gentler alternative in Sass projects, see using Sass in Astro. The styling docs for Komal themes show where theme tokens live, which covers most cases where you might otherwise reach for define:vars. The Astro directives reference has the formal definition.
FAQ
Does define:vars work with scoped styles?
Yes. The CSS stays scoped to the component, and the variables are set per instance.
Why can’t I import modules in a script with define:vars?
Because define:vars makes the script inline and unprocessed. Use data-* attributes and a regular <script> instead.