Skip to main content
The Drakk3 Portfolio uses React 19 for components that require client-side interactivity. Currently, there is only one React component: the Contact form.

Why React?

React components are used when you need:
  • Client-side state management (form inputs, toggles, etc.)
  • User interactions that require JavaScript
  • Real-time updates or animations
  • Form validation and submission
Astro components are static by default, so React is brought in only where interactivity is needed. This is called partial hydration.
Astro allows you to mix multiple frameworks (React, Vue, Svelte, etc.) in the same project. This portfolio uses React exclusively for interactive components.

Contact Component

Location: src/components/Contact.tsx The Contact form is the only React component in the portfolio. It handles form state, submission, and success feedback.

Features

  • Form inputs: Name, Email, Message
  • Client-side state management with useState
  • Transition effect using useTransition
  • Success state with “Send another” button
  • Uses shadcn/ui primitives (Button, Input, Textarea, Label)

Full Code

src/components/Contact.tsx
import { useState, useTransition } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';

type FormState = 'idle' | 'success';

export default function Contact() {
  const [status, setStatus] = useState<FormState>('idle');
  const [isPending, startTransition] = useTransition();

  function handleSubmit(_formData: FormData) {
    startTransition(async () => {
      await new Promise<void>((resolve) => setTimeout(resolve, 900));
      setStatus('success');
    });
  }

  return (
    <section id="contact" className="py-24 border-t border-border">
      <div className="container max-w-275 mx-auto px-6">
        <div className="grid grid-cols-1 md:grid-cols-2 gap-20 items-start">

          {/* Left — info */}
          <div>
            <p className="text-xs font-semibold tracking-[3px] uppercase text-primary mb-4">
              Let's talk
            </p>
            <h2 className="text-[clamp(28px,4vw,42px)] font-bold leading-tight text-foreground mb-4">
              Get in touch
            </h2>
            <p className="text-base text-muted-foreground leading-relaxed max-w-100 mb-8">
              Have a project in mind, a question, or just want to say hi?
              My inbox is always open.
            </p>

            <div className="flex flex-col gap-4">
              <div className="flex items-center gap-3 text-sm text-muted-foreground">
                <span className="text-base">📍</span>
                <span>Las Vegas, US</span>
              </div>
            </div>
          </div>

          {/* Right — shadcn form card */}
          <div className="rounded-lg border border-border bg-card p-9">
            {status === 'success' ? (
              <div className="flex flex-col items-center text-center gap-4 py-4">
                <span className="text-5xl"></span>
                <h3 className="text-xl font-bold text-foreground">Message sent!</h3>
                <p className="text-sm text-muted-foreground">
                  Thank you for your message! I'll get back to you soon.
                </p>
                <Button
                  variant="outline"
                  size="sm"
                  className="mt-2"
                  onClick={() => setStatus('idle')}
                >
                  Send another
                </Button>
              </div>
            ) : (
              <form action={handleSubmit} className="flex flex-col gap-5" noValidate>
                <div className="flex flex-col gap-2">
                  <Label htmlFor="name">Name</Label>
                  <Input
                    id="name"
                    name="name"
                    placeholder="Your name"
                    autoComplete="name"
                    required
                  />
                </div>

                <div className="flex flex-col gap-2">
                  <Label htmlFor="email">Email</Label>
                  <Input
                    type="email"
                    id="email"
                    name="email"
                    placeholder="you@example.com"
                    autoComplete="email"
                    required
                  />
                </div>

                <div className="flex flex-col gap-2">
                  <Label htmlFor="message">Message</Label>
                  <Textarea
                    id="message"
                    name="message"
                    rows={5}
                    placeholder="Tell me about your project or idea..."
                    required
                  />
                </div>

                <Button type="submit" disabled={isPending} className="mt-1">
                  {isPending ? (
                    <>
                      <span
                        className="w-3.5 h-3.5 rounded-full border-2 border-white/30 border-t-white animate-spin"
                        aria-hidden="true"
                      />
                      Sending…
                    </>
                  ) : (
                    'Send Message →'
                  )}
                </Button>
              </form>
            )}
          </div>
        </div>
      </div>
    </section>
  );
}

How It’s Mounted in Astro

React components must be hydrated on the client using Astro’s client:* directives.
src/pages/index.astro
---
import Layout from '../layouts/Layout.astro';
import Navbar from '../components/Navbar.astro';
import Hero from '../components/Hero.astro';
import About from '../components/About.astro';
import Skills from '../components/Skills.astro';
import Projects from '../components/Projects.astro';
import Contact from '../components/Contact';
import Footer from '../components/Footer.astro';
---

<Layout>
  <Navbar />
  <main>
    <Hero />
    <About />
    <Skills />
    <Projects />
    <Contact client:load />
  </main>
  <Footer />
</Layout>
Used in this project. Hydrates the component immediately on page load.
<Contact client:load />
When to use:
  • Component is visible on initial page load
  • Interactivity is needed immediately
  • Example: forms, navigation menus
Important: The Contact component is imported without the .tsx extension:
import Contact from '../components/Contact'; // ✅ Correct
import Contact from '../components/Contact.tsx'; // ❌ Wrong
Astro resolves the extension automatically.

State Management

The Contact component uses two React hooks:

useState for Form State

type FormState = 'idle' | 'success';
const [status, setStatus] = useState<FormState>('idle');
  • idle: Form is ready for input
  • success: Form submission succeeded, show success message

useTransition for Pending State

const [isPending, startTransition] = useTransition();

function handleSubmit(_formData: FormData) {
  startTransition(async () => {
    await new Promise<void>((resolve) => setTimeout(resolve, 900));
    setStatus('success');
  });
}
  • isPending: true while the form is submitting
  • startTransition: Wraps async operations without blocking UI
  • Button shows spinner while isPending is true
This is a demo implementation. In production, you’d replace the setTimeout with an actual API call (e.g., to Resend, SendGrid, or a serverless function).

Form Validation

The form uses HTML5 validation with required attributes:
<Input
  id="name"
  name="name"
  placeholder="Your name"
  autoComplete="name"
  required // Browser will validate this
/>

<Input
  type="email" // Browser validates email format
  id="email"
  name="email"
  required
/>
The noValidate attribute on the <form> prevents browser default validation UI, but you can still use checkValidity() or custom validation:
<form action={handleSubmit} noValidate>
  {/* ... */}
</form>
<Input type="email" required />
<Input minLength={3} maxLength={50} />
<Input pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}" />

Using shadcn/ui Components

The Contact form uses five shadcn/ui primitives:
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';

Example Usage

<Button type="submit" disabled={isPending}>
  {isPending ? 'Sending…' : 'Send Message →'}
</Button>

<Button variant="outline" size="sm" onClick={() => setStatus('idle')}>
  Send another
</Button>
All shadcn/ui components accept className for custom styling and are built on top of Radix UI primitives for accessibility.

Loading State

The submit button shows a spinner when isPending is true:
<Button type="submit" disabled={isPending}>
  {isPending ? (
    <>
      <span className="w-3.5 h-3.5 rounded-full border-2 border-white/30 border-t-white animate-spin" />
      Sending…
    </>
  ) : (
    'Send Message →'
  )}
</Button>
The spinner is a pure CSS animation:
<span className="
  w-3.5 h-3.5 rounded-full 
  border-2 border-white/30 border-t-white 
  animate-spin
" />
  • border-2 border-white/30: Creates a light gray circle
  • border-t-white: Makes the top border white
  • animate-spin: Tailwind’s built-in rotation animation

Success State

When the form is submitted, the component renders a success message:
{status === 'success' ? (
  <div className="flex flex-col items-center text-center gap-4 py-4">
    <span className="text-5xl"></span>
    <h3 className="text-xl font-bold text-foreground">Message sent!</h3>
    <p className="text-sm text-muted-foreground">
      Thank you for your message! I'll get back to you soon.
    </p>
    <Button variant="outline" size="sm" onClick={() => setStatus('idle')}>
      Send another
    </Button>
  </div>
) : (
  <form action={handleSubmit}>
    {/* form fields */}
  </form>
)}
Clicking “Send another” resets the state back to idle.

Adding More React Components

To add another React component:
1

Create the component

src/components/MyComponent.tsx
import { useState } from 'react';

export default function MyComponent() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}
2

Import in Astro

src/pages/index.astro
---
import MyComponent from '../components/MyComponent';
---

<Layout>
  <MyComponent client:load />
</Layout>
3

Choose hydration directive

  • client:load - Hydrate immediately
  • client:visible - Hydrate when visible
  • client:idle - Hydrate when browser is idle
  • client:only="react" - Client-only (no SSR)
Remember: Always add a client:* directive when using React components in Astro. Without it, the component will be rendered as static HTML with no interactivity.

shadcn/ui Primitives

Learn about Button, Input, Textarea, and other UI components

Astro Components

Explore static components like Navbar, Hero, and Projects