UI component and layout skill for the Banora Chiropractic Astro website. Use this skill whenever building, editing, or reviewing any Astro component, page layout, section, or interactive element. Trigger this for creating new pages, building reusable components, structuring layouts, adding navigation, building forms, or integrating third-party widgets. If it renders on screen, this skill applies.
<Image> from astro:assets or native <img> with lazy loading<a> tags (Astro doesn't need a special Link component)Extend tailwind.config.mjs with the brand system:
export default {
theme: {
extend: {
colors: {
brand: {
navy: '#1B3A5C',
blue: '#2C5F8A',
'light-blue': '#5B9EC9',
grey: '#F5F5F5',
}
}
}
}
}
Use text-brand-navy, bg-brand-blue, hover:bg-brand-light-blue etc. throughout all components.
HeroSection.astro, ServiceCard.astrocomponents/layout/, components/ui/, components/sections/Every component receives content via props — never hardcode text:
---
interface Props {
title: string;
subtitle?: string;
ctaText: string;
ctaLink: string;
}
const { title, subtitle, ctaText, ctaLink } = Astro.props;
---
<section>
<h1>{title}</h1>
{subtitle && <p>{subtitle}</p>}
<a href={ctaLink}>{ctaText}</a>
</section>
Use <slot /> for flexible content and named slots for multi-region layouts:
<!-- TwoColumn.astro -->
---
interface Props {
reversed?: boolean;
}
const { reversed = false } = Astro.props;
---
<div class={`flex flex-col md:flex-row gap-8 ${reversed ? 'md:flex-row-reverse' : ''}`}>
<div class="md:w-1/2">
<slot name="content" />
</div>
<div class="md:w-1/2">
<slot name="media" />
</div>
</div>
components/layout/)Header.astro — logo, navigation, mobile menu toggle, "Book Now" buttonFooter.astro — NAP details, quick links, social links, copyrightMobileMenu.astro — slide-out mobile navigation (uses <script> for toggle)Breadcrumbs.astro — accepts breadcrumb items as propsPageWrapper.astro — consistent max-width and padding via slotcomponents/ui/)Button.astro — primary (filled navy), secondary (outlined), CTA (filled mid-blue)Card.astro — rounded corners, subtle shadow, hover liftSectionHeading.astro — h2 with optional subtitle, consistent spacingBadge.astro — small labels for categoriesClickToCall.astro — phone link with proper tel: hrefcomponents/sections/)HeroSection.astro — full-width hero with heading, subtext, CTAServiceGrid.astro — 3-column grid of service cardsConditionList.astro — grid of conditions with linksTeamSection.astro — practitioner cards with photo, name, credentials, bioCTABanner.astro — full-width call-to-action stripFAQAccordion.astro — expandable Q&A pairs (uses <details>/<summary> — zero JS)MapSection.astro — Google Maps embed with clinic details alongsideContactSection.astro — form + clinic details side by sidecomponents/integrations/)BookingWidget.astro — IconPractice link/iframeChatbotWidget.astro — deferred script loaderGoogleMap.astro — embedded mapAnalytics.astro — GA4 script in headSchemaMarkup.astro — JSON-LD schema injectionFollow Tailwind defaults:
sm: 640px (large phones)md: 768px (tablets)lg: 1024px (small laptops)xl: 1280px (desktops)md: and lg: prefixeslg, full nav at lg and above---
import BaseLayout from '../layouts/BaseLayout.astro';
import Hero from '../components/sections/HeroSection.astro';
import CTABanner from '../components/sections/CTABanner.astro';
---
<BaseLayout title="Page Title | Banora Chiropractic" description="Meta description here">
<Hero title="Page Title" ctaText="Book a Visit" ctaLink="/contact" />
<!-- Content sections here -->
<CTABanner />
</BaseLayout>
---
// src/pages/services/[slug].astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import Hero from '../../components/sections/HeroSection.astro';
import CTABanner from '../../components/sections/CTABanner.astro';
import { services } from '../../data/services';
export function getStaticPaths() {
return services.map((service) => ({
params: { slug: service.slug },
props: { service },
}));
}
const { service } = Astro.props;
---
<BaseLayout title={service.metaTitle} description={service.metaDescription} schema={service.schema}>
<Hero title={service.title} subtitle={service.subtitle} />
<section class="py-16 px-4 max-w-4xl mx-auto">
<!-- service content -->
</section>
<CTABanner />
</BaseLayout>
All pages are statically generated at build time. No server-side rendering at request time.
Many "interactive" elements don't actually need JavaScript:
FAQ Accordion — use native HTML:
<details class="border-b border-gray-200 py-4">
<summary class="cursor-pointer font-semibold text-brand-navy">
{question}
</summary>
<p class="mt-3 text-gray-700">{answer}</p>
</details>
Hover effects — pure CSS via Tailwind:
<div class="hover:-translate-y-1 hover:shadow-lg transition-all duration-200">
For things like mobile menu toggle, use a <script> tag:
<button id="menu-toggle" aria-label="Toggle menu" aria-expanded="false">
<svg><!-- hamburger icon --></svg>
</button>
<nav id="mobile-menu" class="hidden" aria-hidden="true">
<!-- nav links -->
</nav>
<script>
const toggle = document.getElementById('menu-toggle');
const menu = document.getElementById('mobile-menu');
toggle?.addEventListener('click', () => {
const isOpen = menu?.classList.toggle('hidden');
toggle.setAttribute('aria-expanded', String(!isOpen));
menu?.setAttribute('aria-hidden', String(isOpen));
});
</script>
Almost never for this project. The site is content-driven with minimal interactivity. If you find yourself reaching for React or Preact, stop and ask: can this be done with a <script> tag or native HTML? The answer is almost always yes.
Formspree works with plain HTML forms:
<form action="https://formspree.io/f/mpqjeego" method="POST">
<input type="text" name="_gotcha" style="display:none" /> <!-- honeypot -->
<label for="name">Name</label>
<input type="text" id="name" name="name" required />
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
<label for="phone">Phone</label>
<input type="tel" id="phone" name="phone" />
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
<button type="submit">Send Message</button>
</form>
No JavaScript, no state management, no form libraries. It just works.
alt text<label> elements (use for attribute)aria-labelfocus:ring-2 focus:ring-brand-blue focus:outline-none<nav>, <main>, <article>, <section>, <footer>aria-expanded and aria-hidden<details>/<summary> for accordions (built-in accessibility)Keep animations subtle and purposeful:
hover:-translate-y-1 hover:shadow-lg transition-all duration-200)hover:scale-105 transition-transform)@keyframes with Intersection Observer in a <script> tag — not a libraryprefers-reduced-motion: wrap animations in @media (prefers-reduced-motion: no-preference)