Skip to main content
Neubrutal

Menu

Astro + Tailwind CSS v4

Neubrutalism, fully assembled. Fully Astro.

Thick borders, hard shadows, and flat color — a complete component library, block set, and layout, ready to build on.

Why this theme

Everything a real product needs

Every piece below is a typed Astro component — copy it, compose it, or use it as-is.

Zero-JS by default

Every component ships as static HTML — interactivity is opt-in and minimal.

Tokenized aesthetic

Borders, shadows, and accents are Tailwind v4 theme tokens — change the vibe without rebuilding components.

Full component set

Buttons to drawers, tables to toasts — everything a real product needs is here.

Composable blocks

Hero, navbar, CTA, and feature grid blocks are built from the same primitives you get.

Accessible by construction

Focus rings, contrast, and semantics are part of the design system, not an afterthought.

Astro-native

Typed props, slots, and scoped styles — no framework runtime required.

Components

Buttons

One reusable Button with eight variants and three sizes — grouped here by how you'd actually reach for them.

Primary & Secondary

The default action, and its secondary counterpart.

Outline & Borderless

Lower-emphasis actions for dense layouts.

Sizes

Small, medium, and large — same variant, different scale.

Danger

Reserved for destructive, hard-to-undo actions.

Success

Confirms a completed, positive outcome.

Warning

Flags a risky action before it's taken.

Components

Badges

Three ways to attach a small, flat-color label to real content.

Tags

NewAstro 5.0 is here

A quick look at what shipped in the latest major release.

Dismissible filters

Active filters, removable inline.

ReactUnder $50

Status indicators

Live system state, at a glance.

APIOperational
Build queueDegraded
Components

Cards

One base Card, a couple of content-specific variants, and a few things you can build from just the base.

Design

Composable by default

Pass a title, description, and accent — or drop in your own markup via slots.

System Message

Deploy succeeded

neubrutal-theme was built and deployed in 42 seconds. No warnings, no errors.

Shipping a full component library in a weekend

Notes on building buttons, cards, and forms fast without reaching for a UI kit.

Pro

$29/mo

Everything in Free, plus unlimited projects and priority support.

42+

Components shipped

Lightning setup

Install once, theme everything from a single CSS file.

Components

Avatars & Lists

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

What's included

Everything ships in the box.

  • Typed props on every component
  • Zero client JS unless you opt in
  • Composable blocks and layouts
Components

Tabs & Accordion

Every component accepts typed props — this panel is just one of them, swapped in on click.

What are the basic features?

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.

How do I get started?

Copy any component into your project, or clone the whole theme and start editing the page in src/pages/index.astro.

What support options are available?

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.

Does it work with content collections?

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.

Can I change the color palette?

Every color is a single OKLCH token in global.css — swap the values there and the whole theme follows.

Is any of this locked behind JavaScript?

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.

variant="vertical"

Arrow Up/Down moves focus between triggers when the tablist is vertical.

Components

Forms

Every input, select, and control shares the same border, shadow, and focus language — composed here into a real registration flow.

Sign up

Create your account

Join the waitlist — no spam, just launch updates.

Account type

Already have an account? Log in

More field styles

Find anything

A search field paired with an inline submit button.

Get product updates

A single icon-input field, ready for a newsletter signup.

Send feedback

A textarea with an `actions` slot, buttons sharing its border.

Draft a comment

The same `actions` slot, positioned below instead of inside.

Notification preferences

A grouped set of checkboxes for multi-select settings.

Notification preferences

Validation states

An `error` prop sets `aria-invalid` and renders an announced message.

Components

Table, Progress & Alerts

Team members
NameRoleStatus
Ava StoneDesign LeadActive
Marcus LinEngineerActive
Priya AnandProductInvited
Theo BrandtEngineerSuspended

Installing Neubrutal CLI

npm install -g neubrutal

v1.0.0
Building theme80%
Under the hood

Anatomy of a component

Every component in this theme is a plain, typed Astro file — here's the Button you've been clicking this whole page.

Button.astro
---
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

  • Eight typed variantsprimary, secondary, outline, borderless, ghost, danger, success, warning.
  • Renders as <a> or <button>Picks the right tag automatically from whether href is set.
  • Three sizessm, md, and lg, applied as a single class-list swap.
  • Full native attribute passthroughExtends HTMLAttributes<'a'> — type, disabled, aria-*, all just work.
  • Zero client JSPure server-rendered HTML — nothing to hydrate.
Components

Modal, Drawer & Toast

Overlay components are dialog-based and controlled with plain data attributes — no JS to wire up yourself.

Build your next project loud.

Every component, block, and layout on this page is yours to copy.

Confirm action

This is a modal dialog. It traps focus, closes on backdrop click or Escape, and needs zero JS to wire up beyond a data-dialog-open attribute.

Your cart

Drawers slide in from either edge and share the same open/close wiring as modals.