RRM logo Ops Team

Component Structure Guidelines

Category: Code
Created by: Guilherme Nunes
Created At: May 15, 2026
Updated: May 15, 2026

Component Structure Guidelines

Component Shape

Astro

  1. Imports
  2. Types and interfaces
  3. Props destructuring
  4. Constants and helpers
  5. Markup
  6. Scoped styles, if used
  7. 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

Note
I haven't created many React components to define a good structure, so this is mostly a placeholder for now
  1. Imports
  2. Main component
    1. Constants
    2. Helper functions
  3. 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>
  );
}
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) or dangerouslySetInnerHTML={{ __html: text }} (React) when inserting text into a tag

dangerouslySetInnerHTML might 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} />
Note
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.