Ready to use UI components for your Astro projects. Just click "Copy Code" and paste it directly into your project.
Password must be at least 8 characters
This username is available!
import { cn } from '../../lib/utils';
import type { InputHTMLAttributes } from 'react';
/**
* Input component
* ────────────────
* Accessible form input with label, helper text, and validation states.
*
* @example
* <Input
* label="Email address"
* type="email"
* placeholder="you@example.com"
* helperText="We'll never share your email."
* />
*
* @example
* <Input
* label="Name"
* error="This field is required."
* />
*/
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
helperText?: string;
error?: string;
/** Show success state */
success?: boolean;
/** Wrapper class */
wrapperClass?: string;
}
export function Input({
label,
helperText,
error,
success,
id,
wrapperClass,
className,
...rest
}: InputProps) {
const inputId = id ?? `input-${Math.random().toString(36).slice(2, 7)}`;
return (
<div className={cn('flex flex-col gap-1.5', wrapperClass)}>
{label && (
<label
htmlFor={inputId}
className="text-sm font-medium text-[var(--color-text)]"
>
{label}
</label>
)}
<input
id={inputId}
className={cn(
'w-full rounded-[var(--radius-lg)] border px-4 py-2.5 text-sm',
'bg-[var(--color-bg)] text-[var(--color-text)]',
'placeholder:text-[var(--color-text-subtle)]',
'transition-colors duration-[var(--transition-fast)]',
'outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:ring-offset-1',
error
? 'border-red-500 focus:ring-red-500'
: success
? 'border-emerald-500 focus:ring-emerald-500'
: 'border-[var(--color-border)] hover:border-[var(--color-border-strong)]',
className,
)}
aria-describedby={helperText ? `${inputId}-helper` : undefined}
aria-invalid={error ? 'true' : undefined}
{...rest}
/>
{error && (
<p id={`${inputId}-helper`} className="text-xs text-red-600" role="alert">
{error}
</p>
)}
{!error && helperText && (
<p id={`${inputId}-helper`} className="text-xs text-[var(--color-text-muted)]">
{helperText}
</p>
)}
</div>
);
}---
/**
* Badge.astro
* ────────────
* Inline label for status, categories, and tags.
*
* Props:
* @prop variant - 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'outline'
* @prop size - 'sm' | 'md'
*
* @example
* <Badge variant="primary">New</Badge>
* <Badge variant="success">Live</Badge>
* <Badge variant="outline">Beta</Badge>
*/
interface Props {
variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'outline';
size?: 'sm' | 'md';
class?: string;
}
const { variant = 'default', size = 'md', class: className = '' } = Astro.props;
const variantStyles = {
default: 'bg-[var(--color-bg-muted)] text-[var(--color-text-muted)]',
primary: 'bg-[var(--color-primary-light)] text-[var(--color-primary-dark)]',
success: 'bg-emerald-100 text-emerald-700',
warning: 'bg-amber-100 text-amber-700',
danger: 'bg-red-100 text-red-700',
outline: 'border border-[var(--color-primary)] text-[var(--color-primary)] bg-transparent',
};
const sizeStyles = {
sm: 'px-2 py-0.5 text-xs',
md: 'px-2.5 py-1 text-xs',
};
---
<span
class={`inline-flex items-center gap-1 font-semibold rounded-full ${variantStyles[variant]} ${sizeStyles[size]} ${className}`}
>
<slot />
</span>Cards are versatile containers that can hold a variety of content like images, text, and actions.
---
/**
* Card.astro
* ───────────
* Surface container with border, shadow, and optional hover lift.
*
* Props:
* @prop padding - 'none' | 'sm' | 'md' | 'lg'
* @prop hover - Whether to show hover lift effect
* @prop bordered - Whether to show border
* @prop class - Additional CSS classes
*
* @example
* <Card padding="lg" hover>
* <h3>Feature Title</h3>
* <p>Description text here.</p>
* </Card>
*/
interface Props {
padding?: 'none' | 'sm' | 'md' | 'lg';
hover?: boolean;
bordered?: boolean;
class?: string;
}
const {
padding = 'md',
hover = false,
bordered = true,
class: className = '',
} = Astro.props;
const paddingStyles = {
none: '',
sm: 'p-4',
md: 'p-6',
lg: 'p-8',
};
---
<div
class={[
'rounded-[var(--radius-xl)] bg-[var(--color-bg-subtle)]',
bordered ? 'border border-[var(--color-border)]' : '',
hover ? 'transition-all duration-[var(--transition-slow)] hover:-translate-y-1 hover:shadow-[var(--shadow-xl)]' : 'shadow-[var(--shadow-md)]',
paddingStyles[padding],
className,
].join(' ')}
>
<slot />
</div>import { cn } from '../../lib/utils';
/**
* Alert component
* ───────────────
* Displays an important message in a way that attracts the user's attention.
*
* @example
* <Alert variant="warning" title="Warning">
* Your subscription is expiring soon.
* </Alert>
*/
type AlertVariant = 'info' | 'success' | 'warning' | 'error';
interface AlertProps {
variant?: AlertVariant;
title?: string;
children: React.ReactNode;
className?: string;
}
const variantClasses: Record<AlertVariant, string> = {
info: 'bg-blue-50 text-blue-800 border-blue-200',
success: 'bg-emerald-50 text-emerald-800 border-emerald-200',
warning: 'bg-amber-50 text-amber-800 border-amber-200',
error: 'bg-red-50 text-red-800 border-red-200',
};
const iconClasses: Record<AlertVariant, string> = {
info: 'text-blue-500',
success: 'text-emerald-500',
warning: 'text-amber-500',
error: 'text-red-500',
};
export function Alert({ variant = 'info', title, children, className }: AlertProps) {
return (
<div
role="alert"
className={cn(
'rounded-[var(--radius-lg)] border p-4 flex gap-3',
variantClasses[variant],
className
)}
>
<div className={cn('mt-0.5 flex-shrink-0', iconClasses[variant])}>
{/* Simple icon based on variant */}
{variant === 'info' && (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
)}
{variant === 'success' && (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/></svg>
)}
{variant === 'warning' && (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
)}
{variant === 'error' && (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>
)}
</div>
<div>
{title && <h5 className="font-semibold mb-1">{title}</h5>}
<div className="text-sm opacity-90">{children}</div>
</div>
</div>
);
}---
/**
* Avatar.astro
* ────────────
* Displays a user's profile picture or initials.
*
* @example
* <Avatar src="/images/user.jpg" alt="John Doe" size="md" />
* <Avatar fallback="JD" size="lg" />
*/
interface Props {
src?: string;
alt?: string;
fallback?: string;
size?: 'sm' | 'md' | 'lg' | 'xl';
class?: string;
}
const { src, alt = '', fallback = '?', size = 'md', class: className = '' } = Astro.props;
const sizeStyles = {
sm: 'w-8 h-8 text-xs',
md: 'w-10 h-10 text-sm',
lg: 'w-12 h-12 text-base',
xl: 'w-16 h-16 text-lg',
};
---
<div
class={`relative inline-flex items-center justify-center overflow-hidden bg-zinc-200 text-zinc-600 rounded-full shrink-0 ${sizeStyles[size]} ${className}`}
>
{src ? (
<img src={src} alt={alt} class="w-full h-full object-cover" loading="lazy" />
) : (
<span class="font-medium uppercase">{fallback}</span>
)}
</div>