Component Structure Guidelines
Component Structure Guidelines
Component Shape
Astro
- Imports
- Types and interfaces
- Props destructuring
- Constants and helpers
- Markup
- Scoped styles, if used
- Scoped script, if used
---
import Container from "../components/layout/Container.astro";
interface Props {
title: string;
description?: string;
}
const { title, description } = Astro.props;
---
<section class="feature-section">
<Container>
<h2 set:html={title} />
{description && <p set:html={description} />}
<slot />
</Container>
</section>
<style>
[...]
</style>
<script is:inline>
[...]
</script>
React
I haven't created many React components to define a good structure, so this is mostly a placeholder for now
- Imports
- Main component
- Constants
- Helper functions
- Helper components
import { FaPhone } from "react-icons/fa";
import { MdEmail } from "react-icons/md";
export function FeatureCard({ title, description }) {
const [example, setExample] = useState(false);
const changeBackground = () => {
setExample(window.scrollY >= 60);
};
return (
<article>
<h3 dangerouslySetInnerHTML={{ __html: title }} />
{description && <p dangerouslySetInnerHTML={{ __html: description }} />}
</article>
);
}
Recommended Folder Structure
src/
components/
blog/
BlogCard.astro
BlogFeed.astro
BlogRelatedPosts.astro
forms/
FormComp.astro
FormField.astro
FormContact.astro
FormDiscoveryFlight.astro
layout/
HeaderContact.astro
Hero.astro
Footer.astro
Navbar.jsx
sections/
ctas/
SectionImageAndDescription.astro
SectionIcons.astro
Bento.astro
ui/
cards/
Socials.astro
ImageComp.astro
FormattedDate.astro
widgets/
ChatWidget.astro
ElfSightGoogleReviews.astro
GoogleMaps.astro
Good Practices
- Use 2 space tab size
- Always use
set:html={text}(Astro) ordangerouslySetInnerHTML={{ __html: text }}(React) when inserting text into a tag
dangerouslySetInnerHTMLmight need some review since I’m not sure if it can cause Cross-Site Scripting (XSS) attacks or if that is only when used with user generated content.
Naming conventions
- Use Pascal case for component names (all words are capitalized, e.g. SectionTest, CardSomethingOrOther)
- Start component names with overall type: sections with Section (SectionImageAndDescription), cards with Card (CardWithIcon), bentos with Bento (BentoSplit)…
Common properties
Maintain the exact same structure for common properties throughout the project
interface ImageProp {
src: string;
alt: string;
class?: string;
}
interface ctaProp {
href: string;
content: string;
class?: string;
}
interface Props {
title: string;
subtitle: string;
paragraphs: string[];
img: ImageProp;
ctas: ctaProp[];
};
const { title, subtitle, paragraphs, img, ctas } = Astro.props;
Repeating class lists
Avoid repeating complex Tailwind class lists. When specific patterns are repeated many times add a custom class to the global.css file and apply the desired Tailwind classes there, then use the custom class where needed.
.title {
@apply font-title text-3xl md:text-4xl lg:text-5xl font-bold text-primary-900 mb-4 uppercase;
}
.subtitle {
@apply text-lg md:text-xl text-slate-700 max-w-2xl leading-8;
}
.section {
@apply relative isolate overflow-hidden bg-primary-50 text-slate-900 px-6 py-12 lg:py-24 lg:px-8;
}
Interface Props
Always use interface Props inside components
Component frontmatter example
interface Props {
eyebrow: string;
title: string;
subTitle: string;
ctas: CTAProp[];
img: ImageProp;
};
const { eyebrow, title, subTitle, ctas, img } = Astro.props;
Component usage example
<Example {...props} />
Default properties
Use specially on often repeated components with the same copy (Google Maps, bottom of the page CTA…).
interface Props {
upperHeader?: string;
title?: string;
description?: string;
mapsLink?: string;
mapsTitle?: string;
}
const {
upperHeader = "Where You Can Find Us",
title = `Come Meet ${COMPANY_NAME}`,
description = `Our training location is in ${ADDRESS_CITY}, ${ADDRESS_STATE_ABBR}, providing efficient access while maintaining a focused training environment.`,
mapsLink = "https://www.google.com/maps/embed?...",
mapsTitle = `${COMPANY_NAME} location map`,
} = Astro.props;
Icons
- Avoid using same concept icons with different stylings across the project (e.g. multiple different checkmark icons)
- Prefer icons from the same family, specially when in the same section / page
- Prefer ReactIcons over SVG tags for readability and ease of use
- If SVG tags are used, try to do one of the following:
- Create a data file with all of the used icons and export them as strings to be used where needed
- Create a component for each Icon. Useful when using animated icons
Anchor tags
Always check if the href includes http, and if so, set target as _blank and rel as noopener noreferrer.
<a
href={href}
target={href.includes("http") ? "_blank" : "_self"}
rel={href.includes("http") ? "noopener noreferrer" : ""}
class="btn-primary"
set:html={text}
/>
To facilitate this you can use a helper function. Export the following in a utils.ts file:
function getAnchorProps(href: string) {
if (!href) return { href };
return {
href,
target: href.includes("http") ? "_blank" : "_self",
rel: href.includes("http") ? "noreferrer noopener" : "",
}
}
export { getAnchorProps };
And use it like so:
<a {...getAnchorProps(href)} class="btn-primary" set:html={text} />
This function can also be used to guarantee a trailing slash is present in the used link. However, in some cases relative URLs (e.g. /contact-us/#contact-form) or parameters (e.g. /enroll/?program=private-pilot) are passed in these links, so those need to be accounted for. Since there might be more cases and I haven't tested that function yet, I didn't add it here yet.
Company data in the copy
Use the exported constant variables from /src/data/const whenever possible.
import { COMPANY_NAME } from "../data/consts";
const title = `Come Meet ${COMPANY_NAME}`;
Review Checklist
- Does the component have a props interface?
- Does the folder the component is in makes sense?
- Is repeated content stored in the data folder?
- Can the component be easily understood by another developer?
Final notes / Helpful tips
Tailwind style processing
By default Tailwind doesn’t process classes generated through code such as: delay-${number}. Keep this in mind to avoid headaches 😅
Semantic HTML syntax
In general it’s better to use semantic syntax but it’s possible to also make accessibility worse by using incorrectly structured semantic tags.
Examples:
- A
<dt>or<dd>must always be wrapped in a parent<dl>element to be valid and accessible - A
<ul>and<ol>tags should contain only<li>elements to be valid and accessible. More information.
Animate on Scroll (AOS) library
When using AOS, if you want to add separate animations on elements that have AOS applied to them, it can be useful to add a container to hold the AOS animations and apply the separate animations in the child element.
Especially when using data-aos-delay, if you apply a animation on hover, for example, on the same element, AOS and your animation will be competing for the same CSS properties, many times causing both animations to conflict. Your hover animation now has a delay you didn’t want and if you force some property by using important in CSS, now the AOS animation doesn’t behave as it is intended.
Solution example
Instead of:
{
cards.map((card, index) => (
<div
data-aos="fade-down"
data-aos-delay={index * 150}
class="relative size-full rounded-lg border border-accent-200 bg-accent-50 p-6 shadow-md transition-all duration-300 ease-in-out hover:-translate-y-1 hover:border-primary-600 hover:shadow-lg"
>
[...]
</div>
))
}
Do:
{
cards.map((card, index) => (
<div
data-aos="fade-down"
data-aos-delay={index * 150}
>
<div class="relative size-full rounded-lg border border-accent-200 bg-accent-50 p-6 shadow-md transition-all duration-300 ease-in-out hover:-translate-y-1 hover:border-primary-600 hover:shadow-lg">
[...]
</div>
</div>
))
}
Now the hover and AOS animations are applied to different elements but keep their intended effects.