
Why Data Attributes Matter in Modern Web Development
Data attributes let you store extra information in HTML elements. Theyโre perfect for passing data between HTML and JavaScript without breaking standards. In Astro, theyโre very useful to pass data from props into the custom component constructor.
The standard format uses data-
prefixes and kebab-case in the html element. For example, data-theme-type="dark"
becomes camelCase dataset.themeType
in JavaScript. Unforunately thereโs no typesafety for the data attributes, until now!
Use Cases For Global Data Attributes
Global data attributes are useful when there is a script that can be used on any or most elements.
-
Copy To Clipboard
-
Animation Presets
-
Tooltips
Use Cases For Component-Specific Data Attributes
Component-specific data attributes are useful when there is a script that can be used on a specific component.
-
Passing data to a component script
-
State Management
-
Unique Animation Presets
Before getting started
Letโs define the goals for our typesafety system:
- Intellicense and type validation on the html element
- HTMLElement dataset Intellicense and type validation in Typescript
- Intellicense for custom components with unique data attributes in the both the html definition and the component constructor script.
- Easily extend the attributes for custom components and html elements.
Known Limitations
Data attribute values are always compiled to strings, so we need to parse them to the correct type when interacting with them in the script. Objects must be JSON.stringify()
when setting in both the html element and TypeScript, and JSON.parse()
when getting in TypeScript.
We can however set the data attribute to a number, boolean, string, or string union, and it will be compiled correctly to a string. Keep in mind when getting the data it will always be a string.
Autocomplete for global data attribute names will not appear in the html element, but it will show type validation.
There is no intellicense or type checking for custom data attributes directly in the <custom-component>
html element, but there is for the global data attributes.
Benefits of Type-Safe Data Attributes
-
Editor autocomplete
-
Runtime type safety
-
Component-specific attributes
-
Improved developer experience
Creating Your First Type-Safe Attribute System
Start by creating a TypeScript declaration file src/custom-attributes.d.ts
. Letโs start with the global data attributes that are accessible on all html elements. Letโs start with varying types including ones we donโt want, like an object, to test the typesafety works.
type GlobalDataAttributes = CustomDataset<{ 'data-animate-speed': number; 'data-animate-direction': 'left' | 'right' | 'top' | 'bottom'; 'data-close-side-menu': boolean; 'data-clipboard-data': string; 'data-obj': { some: string; other: number; };}>;
We will need two different versions of the data attributes, one for the html element in kebab-case and one for Typescript in camelCase. The type for html elements allows for string, string unions, number, and boolean. All other types are converted to a string, and when accessing the data from Typescript it will always be a string.
HTMLDataAttributes
Starting with the html element type transformers, letโs turn the camelCase to kebab-case.
type KebabData<T extends Record<string, unknown>> = { [K in keyof T as `data-${CamelToKebab<string & K>}`]: T[K];};
type CamelToKebab<S extends string> = S extends `${infer T}-${infer U}` ? `${CamelToKebab<T>}-${Lowercase<U>}` : S extends `${infer T}${infer U}` ? (T extends Uppercase<T> ? (U extends `${Uppercase<string>}${string}` ? `${Lowercase<T>}${CamelToKebab<U>}` : `-${Lowercase<T>}${CamelToKebab<U>}`) : `${T}${CamelToKebab<U>}`) : S;
Now we can use the KebabData
type to set global data attributes on html elements. But first lets make the types safer, so that only the allowed types are passed through, anything else will be a string.
type SimpleTypes = string | number | boolean | null | undefined;
type SafeTypes<T extends Record<string, unknown>> = { [K in keyof T]: T[K] extends SimpleTypes ? T[K] : string;};
Now letโs put them together.
type HTMLDataAttributes<T extends Record<string, unknown>> = Partial<KebabData<SafeTypes<T>>>;
The result from this transformer is kebab-case with data-
prefix and the allowed types for the html element.
To get the intellicense working we need to extend Astroโs type definition for the standardhtml elements and custom component elements.
declare namespace astroHTML.JSX { //Needed for all html tags // eslint-disable-next-line @typescript-eslint/no-empty-object-type interface HTMLAttributes extends HTMLDataAttributes<GlobalDataAttributes> {} //Needed for custom components (global data attributes directly in <custom-component> tags) interface IntrinsicElements extends astroHTML.JSX.DefinedIntrinsicElements { [name: string]: { [name: string]: any; } & HTMLDataAttributes<GlobalDataAttributes>; }}
Now we have intellicense for the global data attributes in the html elements, custom html elements, and the custom component props.
ComponentDataAttributes
On to the custom component data attributes. Itโs just one more transformer since the global data attributes are already camelCase.
type ComponentDataAttributes<T extends object> = Partial< SafeTypes< { [K in keyof T]: T[K] extends string ? T[K] : string; } & { //Set value below to either: //never: strict typing on custom components. // string: loose (can set and look for any dataset that is declared) [K: string]: string; } >>;
Now let extend the DOMStringMap to include the component data attributes. After this our dataset will have intellicense in TypeScript.
interface DOMStringMap extends ComponentDataAttributes<GlobalDataAttributes> {}
Create a new file src/types.d.ts
to export the types for the custom component.
//Needed for custom component props to extend the global data attributesexport type HTMLGlobalDataAttributes = HTMLDataAttributes<GlobalDataAttributes>;// Needed for custom components (global data attributes in imported <CustomComponent> tags)export type HTMLAttributesWithData<T> = import('astro/types').HTMLAttributes<T> & HTMLGlobalDataAttributes;//Export here to use in the components since Astro will not import `HTMLDataAttributes` from `src/custom-attributes.d.ts`export type HTMLData<T> = HTMLDataAttributes<T>;
Custom Component Data Attributes
Letโs cover the custom component data attributes. For this we will create a folder for our custom component with three files:
src/ โโโ components โโโ my-component โโโ MyComponent.astro โโโ index.ts โโโ data.ts
src/components/my-component/data.ts
Define the component data attributes and an exported type for the html data attributes.
export type MyComponentDataAttributes = { hello?: number; world?: 'works' | 'with' | 'unions';};
src/components/my-component/index.ts
Define the constructor class for the custom component and import the data attributes. Declare the dataset in the class to get dataset intellicense in the class.
import type { MyComponentDataAttributes } from './data';
class MyComponent extends HTMLElement { declare dataset: DOMStringMap & ComponentDataAttributes<MyComponentDataAttributes>; constructor() { super(); if (Number(this.dataset.hello) === 10) { console.log(this.dataset.hello); } if (this.dataset.world === 'works') { console.log(this.dataset.world); } }}
customElements.define('my-component', MyComponent);
Lastly create the file src/components/my-component/MyComponent.astro
and use the HTMLAttributesWithData
type for the props.
---import type { HTMLAttributesWithData, HTMLData } from '@/types';import type { HTMLMyComponentDataAttributes } from './data';
export interface Props extends HTMLAttributesWithData<'div'>, HTMLData<HTMLMyComponentDataAttributes> { text: string;}
const { 'data-animate-direction': animateDirection, ...rest } = Astro.props;
console.log(animateDirection);---<div data-animate-direction={animateDirection} data-animate-speed={100} {...rest}>{text}</div>
I prefer to define a prop that sets the value of the attribute. This way you can set the attribute in the html element via the prop instead of the dataset, and make it required if needed.
---import type { HTMLAttributesWithData, HTMLData } from '@/types';import type { HTMLMyComponentDataAttributes } from './data';
export interface Props extends HTMLAttributesWithData<'div'>, HTMLData<HTMLMyComponentDataAttributes> { animateDirection: HTMLMyComponentDataAttributes['animateDirection'];}
const { animateDirection, ...rest } = Astro.props;---
<div data-animate-direction={animateDirection} {...rest}>{text}</div>
Note: Because data-animate-direction
is also potentialy defined in ...rest
we the prop value could be overwritten. To prevent this you can destructure the prop
Common Data Attribute Challenges
Remember these important details:
- Numbers and booleans convert to strings in datasets
- Complex objects need
JSON.stringify()
this includes arrays - Date objects should be converted to ISOstring before setting in the html element
- Always validate values from datasets:
// For booleansconst isTrue = element.dataset.active === 'true';
// For numbersconst count = Number(element.dataset.count);
// convert ISOstring to Date, can return invalid date if the string is not a valid ISOstringconst date = new Date(element.dataset.date);
// check if the date is validconst validDate = isFinite(+date);
// For objectstype Data = { hello: string; world: number;};try { //Consider using zod to validate the data attribute const data: Data = JSON.parse(element.dataset.data || '{}') as Data | {};} catch (error) { console.error('Error parsing data attribute:', error);}
Whatโs Next?
Now that you have a typesafe system for data attributes, but thereโs still one issue. When queryselecting an element, the intellicense for the data attributes will not work. Thereโs a solution for this, you can read about it in the next article.

Custom Component Types in Astro
Learn how to create custom component types in Astro.
Limited Time Launch Sale
Our AstroJS starterkit is an all-in-one solution for your Astro projects. Get your project started with a professional design system, components, and documentation.
GET 60% OFF!TLDR Full Code
type GlobalDataAttributes = { //Define your custom data attributes here};
type ComponentDataAttributes<T extends object> = Partial< SafeTypes< { [K in keyof T]: T[K] extends string ? T[K] : string; } & { //Set value below to either: //never: strict typing on custom components. // string: loose (can set and look for any dataset that is declared) [K: string]: string; } >>;
//Needed for all html tagsdeclare namespace astroHTML.JSX { // eslint-disable-next-line @typescript-eslint/no-empty-object-type interface HTMLAttributes extends HTMLDataAttributes<GlobalDataAttributes> {}}
//Needed for custom components .astro filesdeclare module 'astro/types' { type HTMLTag = keyof astroHTML.JSX.DefinedIntrinsicElements; // Extend the HTMLAttributes type to include your custom data attributes type HTMLAttributes<Tag extends HTMLTag> = Omit<astroHTML.JSX.DefinedIntrinsicElements[Tag], keyof Omit<import('astro').AstroBuiltinAttributes, 'class:list'>> & HTMLDataAttributes<GlobalDataAttributes>;}
// Needed for any for interacting with elements// eslint-disable-next-line @typescript-eslint/no-empty-object-typeinterface DOMStringMap extends ComponentDataAttributes<GlobalDataAttributes> {}
type SimpleTypes = string | number | boolean | null | undefined;
type HTMLDataAttributes<T extends Record<string, any>> = Partial<KebabData<SafeTypes<T>>>;
type KebabData<T extends Record<string, unknown>> = { [K in keyof T as `data-${CamelToKebab<string & K>}`]: T[K];};
type SafeTypes<T extends Record<string, any>> = { [K in keyof T]: T[K] extends SimpleTypes ? T[K] : string;};
type CamelToKebab<S extends string> = S extends `${infer T}-${infer U}` ? `${CamelToKebab<T>}-${Lowercase<U>}` : S extends `${infer T}${infer U}` ? (T extends Uppercase<T> ? (U extends `${Uppercase<string>}${string}` ? `${Lowercase<T>}${CamelToKebab<U>}` : `-${Lowercase<T>}${CamelToKebab<U>}`) : `${T}${CamelToKebab<U>}`) : S;
//Needed for custom component props to extend the global data attributesexport type HTMLGlobalDataAttributes = HTMLDataAttributes<GlobalDataAttributes>;// Needed for custom components (global data attributes in imported <CustomComponent> tags)export type HTMLAttributesWithData<T> = import('astro/types').HTMLAttributes<T> & HTMLGlobalDataAttributes;//Export here to use in the components since Astro will not import `HTMLDataAttributes` from `src/custom-attributes.d.ts`export type HTMLData<T> = HTMLDataAttributes<T>;
Related Articles

How To Create A Secure Staging Environment For Cloudflare Pages
Testing website changes before they go live is essential for maintaining a professional online presence. Learn how to create a secure, password-protected staging environment for your Cloudflare Pages site.

How To Redirect .pages.dev to Your Domain
Learn how to redirect .pages.dev to your domain with this step-by-step guide

Schedule Cloudflare Pages Builds
Schedule your Cloudflare Pages builds to run at specific times. No external services needed.

Fix Bad Indentation Of A Mapping Entry Error In Astro
Troubleshoot and resolve the common "bad indentation of a mapping entry" error in AstroJS with this practical guide.