Deploy succeeded
neubrutal-theme was built and deployed in 42 seconds. No warnings, no errors.
Thick borders, hard shadows, and flat color — a complete component library, block set, and layout, ready to build on.
Every piece below is a typed Astro component — copy it, compose it, or use it as-is.
Every component ships as static HTML — interactivity is opt-in and minimal.
Borders, shadows, and accents are Tailwind v4 theme tokens — change the vibe without rebuilding components.
Buttons to drawers, tables to toasts — everything a real product needs is here.
Hero, navbar, CTA, and feature grid blocks are built from the same primitives you get.
Focus rings, contrast, and semantics are part of the design system, not an afterthought.
Typed props, slots, and scoped styles — no framework runtime required.
Three ways to attach a small, flat-color label to real content.
A quick look at what shipped in the latest major release.
Active filters, removable inline.
Live system state, at a glance.
One base Card, a couple of content-specific variants, and a few things you can build from just the base.
Pass a title, description, and accent — or drop in your own markup via slots.
neubrutal-theme was built and deployed in 42 seconds. No warnings, no errors.
Notes on building buttons, cards, and forms fast without reaching for a UI kit.
$29/mo
Everything in Free, plus unlimited projects and priority support.
42+
Components shipped
Install once, theme everything from a single CSS file.
Sizes & shapes
Ava Stone
Design Lead
Marcus Lin
Engineer
Priya Anand
Product
Group — mock chat room
# general 3 members
Ava 9:41 AM
Just shipped the new Table component 🎉
Marcus 9:42 AM
The status pills look great
Lists
Everything ships in the box.
Every component accepts typed props — this panel is just one of them, swapped in on click.
No client framework required: a few lines of vanilla JS handle the switching and ARIA state.
Panels are hidden with the native hidden attribute, so there's nothing to hydrate and nothing to flash.
A full component library — buttons, cards, forms, tables, modals, and more — plus blocks and layouts, all built with Tailwind CSS v4 and typed Astro props.
Copy any component into your project, or clone the whole theme and start editing the page in src/pages/index.astro.
The theme ships as plain, readable Astro files — check the "Anatomy of a component" section further down this page to see exactly how one is built.
Yes — components are plain Astro files with typed props, so they compose with content collections, server islands, or any framework component the same way any other Astro component would.
Every color is a single OKLCH token in global.css — swap the values there and the whole theme follows.
No — this accordion included. It's a native <details> element; the only components with a script tag are the ones that genuinely need one, like the Carousel or Modal.
variant="pill"
Same controller, same keyboard support — just a different trigger and panel treatment.
Pass a variant prop instead of copying the component.
One Tabs.astro, four looks: underline, contained, pill, vertical.
variant="vertical"
Arrow Up/Down moves focus between triggers when the tablist is vertical.
Home and End jump to the first and last tab.
Panels swap with the native hidden attribute — no hydration involved.
Every input, select, and control shares the same border, shadow, and focus language — composed here into a real registration flow.
More field styles
| Name | Role | Status |
|---|---|---|
| Ava Stone | Design Lead | Active |
| Marcus Lin | Engineer | Active |
| Priya Anand | Product | Invited |
| Theo Brandt | Engineer | Suspended |
npm install -g neubrutal
Every component in this theme is a plain, typed Astro file — here's the Button you've been clicking this whole page.
---
import type { HTMLAttributes } from "astro/types";
type Variant = "primary" | "secondary" | "outline" | "ghost" | "borderless" | "danger" | "success" | "warning";
type Size = "sm" | "md" | "lg";
interface Props extends Omit<HTMLAttributes<"a">, "class"> {
/** Renders an <a> when `href` is set, otherwise a <button>. Force one explicitly with `as`. */
as?: "a" | "button";
type?: "button" | "submit" | "reset";
variant?: Variant;
size?: Size;
class?: string;
}
const { as, type = "button", variant = "primary", size = "md", class: className, href, ...rest } = Astro.props;
const Tag = as ?? (href ? "a" : "button");
const base =
"inline-flex items-center justify-center gap-2 border-2 font-heading font-bold uppercase tracking-wide transition-all duration-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring disabled:pointer-events-none disabled:opacity-50";
// Variants filled with a categorical accent use border-ink-on-accent and
// text-ink-on-accent, not border-ink/text-ink: the accent fills stay the
// same bright color in dark mode (see global.css), so a border or text
// color that *inverts* would lose contrast against a fill that didn't — a
// near-white border on bright yellow is barely visible (checked: ~1.4:1).
// Variants filled with bg-paper (or transparent) use the inverting
// border-ink/text-ink instead, since paper inverts right along with them.
const variants: Record<Variant, string> = {
primary:
"border-ink-on-accent bg-yellow text-ink-on-accent shadow-brutal hover:-translate-x-0.5 hover:-translate-y-0.5 hover:shadow-brutal-lg active:translate-x-0.5 active:translate-y-0.5 active:shadow-none",
secondary:
"border-ink-on-accent bg-blue text-ink-on-accent shadow-brutal hover:-translate-x-0.5 hover:-translate-y-0.5 hover:shadow-brutal-lg active:translate-x-0.5 active:translate-y-0.5 active:shadow-none",
outline:
"border-ink bg-paper text-ink shadow-brutal hover:-translate-x-0.5 hover:-translate-y-0.5 hover:shadow-brutal-lg active:translate-x-0.5 active:translate-y-0.5 active:shadow-none",
ghost: "border-transparent bg-transparent text-ink hover:bg-ink hover:text-paper",
borderless:
"border-transparent bg-paper text-ink shadow-brutal hover:-translate-x-0.5 hover:-translate-y-0.5 hover:shadow-brutal-lg active:translate-x-0.5 active:translate-y-0.5 active:shadow-none",
danger:
"border-ink-on-accent bg-pink text-ink-on-accent shadow-brutal hover:-translate-x-0.5 hover:-translate-y-0.5 hover:shadow-brutal-lg active:translate-x-0.5 active:translate-y-0.5 active:shadow-none",
success:
"border-ink-on-accent bg-green text-ink-on-accent shadow-brutal hover:-translate-x-0.5 hover:-translate-y-0.5 hover:shadow-brutal-lg active:translate-x-0.5 active:translate-y-0.5 active:shadow-none",
warning:
"border-ink-on-accent bg-orange text-ink-on-accent shadow-brutal hover:-translate-x-0.5 hover:-translate-y-0.5 hover:shadow-brutal-lg active:translate-x-0.5 active:translate-y-0.5 active:shadow-none",
};
const sizes: Record<Size, string> = {
sm: "px-4 py-2 text-sm",
md: "px-6 py-3 text-base",
lg: "px-8 py-4 text-lg",
};
---
<Tag
class:list={[base, variants[variant], sizes[size], className]}
href={Tag === "a" ? href : undefined}
type={Tag === "button" ? type : undefined}
{...rest}
>
<slot />
</Tag>Component features
Overlay components are dialog-based and controlled with plain data attributes — no JS to wire up yourself.
Every component, block, and layout on this page is yours to copy.