I’m a giant fan of Astro’s deal with developer expertise (DX) and the onboarding of recent builders. Whereas the essential DX is powerful, I can simply make a convoluted system that’s exhausting to onboard my very own builders to. I don’t need that to occur.
If I’ve a number of builders engaged on a venture, I need them to know precisely what to anticipate from each part that they’ve at their disposal. This goes double for myself sooner or later after I’ve forgotten how one can work with my very own system!
To try this, a developer might go learn every part and get a robust grasp of it earlier than utilizing one, however that feels just like the onboarding could be extremely gradual. A greater means could be to arrange the interface in order that because the developer is utilizing the part, they’ve the fitting information instantly accessible. Past that, it could bake in some defaults that don’t permit builders to make pricey errors and alerts them to what these errors are earlier than pushing code!
Enter, in fact, TypeScript. Astro comes with TypeScript arrange out of the field. You don’t have to make use of it, however because it’s there, let’s discuss how one can use it to craft a stronger DX for our improvement groups.
Watch
I’ve additionally recorded a video model of this text which you can watch if that’s your jam. Test it out on YouTube for chapters and closed captioning.
Setup
On this demo, we’re going to make use of a fundamental Astro venture. To get this began, run the next command in your terminal and select the “Minimal” template.
npm create astro@newest
This may create a venture with an index route and a quite simple “Welcome” part. For readability, I like to recommend eradicating the <Welcome />
part from the path to have a clear start line in your venture.
So as to add a little bit of design, I’d suggest organising Tailwind for Astro (although, you’re welcome to model your part nonetheless you desire to together with a mode block within the part).
npx astro add tailwind
As soon as that is full, you’re prepared to jot down your first part.
Creating the essential Heading part
Let’s begin by defining precisely what choices we wish to present in our developer expertise.
For this part, we wish to let builders select from any HTML heading degree (H1-H6). We additionally need them to have the ability to select a selected font dimension and font weight — it could appear apparent now, however we don’t need individuals selecting a selected heading degree for the burden and font dimension, so we separate these issues.
Lastly, we wish to guarantee that any extra HTML attributes could be handed by means of to our part. There are few issues worse than having a part after which not with the ability to do fundamental performance later.
Utilizing Dynamic tags to create the HTML ingredient
Let’s begin by making a easy part that enables the consumer to dynamically select the HTML ingredient they wish to use. Create a brand new part at ./src/elements/Heading.astro
.
---
// ./src/part/Heading.astro
const { as } = Astro.props;
const As = as;
---
<As>
<slot />
</As>
To make use of a prop as a dynamic ingredient title, we’d like the variable to start out with a capital letter. We are able to outline this as a part of our naming conference and make the developer at all times capitalize this prop of their use, however that feels inconsistent with how most naming works inside props. As a substitute, let’s maintain our deal with the DX, and take that burden on for ourselves.
With a view to dynamically register an HTML ingredient in our part, the variable should begin with a capital letter. We are able to convert that within the frontmatter of our part. We then wrap all the youngsters of our part within the <As>
part by utilizing Astro’s built-in <slot />
part.
Now, we will use this part in our index route and render any HTML ingredient we would like. Import the part on the prime of the file, after which add <h1>
and <h2>
components to the route.
---
// ./src/pages/index.astro
import Format from '../layouts/Format.astro';
import Heading from '../elements/Heading.astro';
---
<Format>
<Heading as="h1">Hi there!</Heading>
<Heading as="h2">Hi there world</Heading>
</Format>

This may render them appropriately on the web page and is a superb begin.
Including extra customized props as a developer interface
Let’s clear up the ingredient selecting by bringing it inline to our props destructuring, after which add in extra props for weight, dimension, and any extra HTML attributes.
To start out, let’s convey the customized ingredient selector into the destructuring of the Astro.props
object. On the identical time, let’s set a wise default in order that if a developer forgets to move this prop, they nonetheless will get a heading.
---
// ./src/part/Heading.astro
const { as: As="h2" } = Astro.props;
---
<As>
<slot />
</As>
Subsequent, we’ll get weight and dimension. Right here’s our subsequent design alternative for our part system: can we make our builders know the category names they should use or do we offer a generic set of sizes and do the mapping ourselves? Since we’re constructing a system, I believe it’s vital to maneuver away from class names and right into a extra declarative setup. This can even future-proof our system by permitting us to alter out the underlying styling and sophistication system with out affecting the DX.
Not solely can we future proof it, however we are also in a position to get round a limitation of Tailwind by doing this. Tailwind, because it seems can’t deal with dynamically-created class strings, so by mapping them, we resolve an instantaneous concern as nicely.
On this case, our sizes will go from small (sm
) to 6 instances the scale (6xl
) and our weights will go from “mild” to “daring”.
Let’s begin by adjusting our frontmatter. We have to get these props off the Astro.props
object and create a pair objects that we will use to map our interface to the right class construction.
---
// ./src/part/Heading.astro
const weights = {
"daring": "font-bold",
"semibold": "font-semibold",
"medium": "font-medium",
"mild": "font-light"
}
const sizes= {
"6xl": "text-6xl",
"5xl": "text-5xl",
"4xl": "text-4xl",
"3xl": "text-3xl",
"2xl": "text-2xl",
"xl": "text-xl",
"lg": "text-lg",
"md": "text-md",
"sm": "text-sm"
}
const { as: As="h2", weight="medium", dimension="2xl" } = Astro.props;
---
Relying in your use case, this quantity of sizes and weights is perhaps overkill. The wonderful thing about crafting your personal part system is that you simply get to decide on and the one limitations are those you set for your self.
From right here, we will then set the courses on our part. Whereas we might add them in a typical class
attribute, I discover utilizing Astro’s built-in class:record
directive to be the cleaner solution to programmatically set courses in a part like this. The directive takes an array of courses that may be strings, arrays themselves, objects, or variables. On this case, we’ll choose the proper dimension and weight from our map objects within the frontmatter.
---
// ./src/part/Heading.astro
const weights = {
daring: "font-bold",
semibold: "font-semibold",
medium: "font-medium",
mild: "font-light",
};
const sizes = {
"6xl": "text-6xl",
"5xl": "text-5xl",
"4xl": "text-4xl",
"3xl": "text-3xl",
"2xl": "text-2xl",
xl: "text-xl",
lg: "text-lg",
md: "text-md",
sm: "text-sm",
};
const { as: As = "h2", weight = "medium", dimension = "2xl" } = Astro.props;
---
<As class:record={[
sizes[size],
weights[weight]
]}
>
<slot />
</As>
Your front-end ought to routinely shift a little bit on this replace. Now your font weight will likely be barely thicker and the courses must be utilized in your developer instruments.

From right here, add the props to your index route, and discover the fitting configuration in your app.
---
// ./src/pages/index.astro
import Format from '../layouts/Format.astro';
import Heading from '../elements/Heading.astro';
---
<Format>
<Heading as="h1" dimension="6xl" weight="mild">Hi there!</Heading>
<Heading as="h3" dimension="xl" weight="daring">Hi there world</Heading>
</Format>

Our customized props are completed, however presently, we will’t use any default HTML attributes, so let’s repair that.
Including HTML attributes to the part
We don’t know what kinds of attributes our builders will wish to add, so let’s make certain they’ll add any extra ones they want.
To try this, we will unfold every other prop being handed to our part, after which add them to the rendered part.
---
// ./src/part/Heading.astro
const weights = {
// and so on.
};
const sizes = {
// and so on.
};
const { as: As = "h2", weight = "medium", dimension = "md", ...attrs } = Astro.props;
---
<As class:record={[
sizes[size],
weights[weight]
]}
{...attrs}
>
<slot />
</As>
From right here, we will add any arbitrary attributes to our ingredient.
---
// ./src/pages/index.astro
import Format from '../layouts/Format.astro';
import Heading from '../elements/Heading.astro';
---
<Format>
<Heading id="my-id" as="h1" dimension="6xl" weight="mild">Hi there!</Heading>
<Heading class="text-blue-500" as="h3" dimension="xl" weight="daring">Hi there world</Heading>
</Format>
I’d wish to take a second to really respect one facet of this code. Our <h1>
, we add an id
attribute. No large deal. Our <h3>
, although, we’re including an extra class. My authentic assumption when creating this was that this is able to battle with the class:record
set in our part. Astro takes that fear away. When the category is handed and added to the part, Astro is aware of to merge the category prop with the class:record
directive and routinely makes it work. One much less line of code!

In some ways, I like to contemplate these extra attributes as “escape hatches” in our part library. Positive, we would like our builders to make use of our instruments precisely as meant, however typically, it’s vital so as to add new attributes or push our design system’s boundaries. For this, we permit them so as to add their very own attributes, and it may well create a robust combine.
It appears finished, however are we?
At this level, for those who’re following alongside, it’d really feel like we’re finished, however we have now two points with our code proper now: (1) our part has “purple squiggles” in our code editor and (2) our builders could make a BIG mistake in the event that they select.
The purple squiggles come from kind errors in our part. Astro provides us TypeScript and linting by default, and sizes and weights can’t be of kind: any
. Not a giant deal, however regarding relying in your deployment settings.
The opposite concern is that our builders don’t have to decide on a heading ingredient for his or her heading. I’m all for escape hatches, however provided that they don’t break the accessibility and search engine optimisation of my website.
Think about, if a developer used this with a div
as an alternative of an h1
on the web page. What would occur?We don’t must think about, make the change and see.

It appears similar, however now there’s no <h1>
ingredient on the web page. Our semantic construction is damaged, and that’s unhealthy information for a lot of causes. Let’s use typing to assist our builders make the perfect choices and know what choices can be found for every prop.
Including sorts to the part
To arrange our sorts, first we wish to make certain we deal with any HTML attributes that come by means of. Astro, once more, has our backs and has the typing we have to make this work. We are able to import the fitting HTML attribute sorts from Astro’s typing package deal. Import the kind after which we will lengthen that kind for our personal props. In our instance, we’ll choose the h1
sorts, since that ought to cowl most something we’d like for our headings.
Contained in the Props
interface, we’ll additionally add our first customized kind. We’ll specify that the as
prop have to be one in every of a set of strings, as an alternative of only a fundamental string kind. On this case, we would like it to be h1
–h6
and nothing else.
---
// ./src/part/Heading.astro
import kind { HTMLAttributes } from 'astro/sorts';
interface Props extends HTMLAttributes<'h1'> "h5"
//... The remainder of the file
---
After including this, you’ll word that in your index route, the <h1>
part ought to now have a purple underline for the as="div"
property. Whenever you hover over it, it can let you already know that the as
kind doesn’t permit for div
and it’ll present you a listing of acceptable strings.
When you delete the div
, you also needs to now have the flexibility to see a listing of what’s accessible as you attempt to add the string.

Whereas it’s not a giant deal for the ingredient choice, understanding what’s accessible is a a lot greater deal to the remainder of the props, since these are far more customized.
Let’s lengthen the customized typing to indicate all of the accessible choices. We additionally denote these things as elective by utilizing the ?:
earlier than defining the kind.
Whereas we might outline every of those with the identical kind performance as our as
kind, that doesn’t maintain this future proofed. If we add a brand new dimension or weight, we’d have to ensure to replace our kind. To resolve this, we will use a enjoyable trick in TypeScript: keyof typeof
.
There are two helper features in TypeScript that can assist us convert our weights and sizes object maps into string literal sorts:
typeof
: This helper takes an object and converts it to a sort. As an illustrationtypeof weights
would returnkind { daring: string, semibold: string, ...and so on}
keyof
: This helper operate takes a sort and returns a listing of string literals from that kind’s keys. As an illustrationkeyof kind { daring: string, semibold: string, ...and so on}
would return"daring" | "semibold" | ...and so on
which is strictly what we would like for each weights and sizes.
---
// ./src/part/Heading.astro
import kind { HTMLAttributes } from 'astro/sorts';
interface Props extends HTMLAttributes<'h1'> "h2"
// ... The remainder of the file
Now, after we wish to add a dimension or weight, we get a dropdown record in our code editor displaying precisely what’s accessible on the kind. If one thing is chosen, exterior the record, it can present an error within the code editor serving to the developer know what they missed.

Whereas none of that is vital within the creation of Astro elements, the truth that it’s in-built and there’s no extra tooling to arrange implies that utilizing it is extremely straightforward to choose into.
I’m under no circumstances a TypeScript skilled, however getting this arrange for every part takes only some extra minutes and might save quite a lot of time for builders down the road (to not point out, it makes onboarding builders to your system a lot simpler).