Teul
An opinionated grid system for React and Tailwind
GitHubWhy Teul
Responsive layouts and Tailwind are everyday tools for building modern websites. Combining them isn’t — a few patterns keep getting in the way:
- Layout classes get buried in the utility string. Column widths, gaps, and alignment sit next to every other utility, breakpoints multiply them, and offsets feel off-by-one — shifting two columns in means writing
col-start-3. - Containers and items look identical. A grid has two roles — the container and its items — but in Tailwind they’re both just
<div>with a class string. - Tailwind’s breakpoints stop at the component boundary. Pair Tailwind with a responsive component from another library — MUI’s
Grid, for example — and you’ll redeclare breakpoints in its theme. Two configs to keep in sync, plus another provider wrapping your app.
Teul brings a 12-column grid system to Tailwind, built on flexbox: Grid for containers, GridItem for items. Type-safe responsive props, plain Tailwind under the hood, copy-paste install. No runtime, no dependencies, no config. (Why flexbox and not CSS grid?)
Installation
Add Teul to your project via the shadcn CLI.
pnpm dlx shadcn@latest add https://teul.joohyun.dev/registry/teul.jsonThen import it:
import { Grid, GridItem } from "@/components/ui/teul"Examples
Responsive size
<Grid>
<GridItem size={{ base: 12, md: 8 }}>...</GridItem>
<GridItem size={{ base: 12, md: 4 }}>...</GridItem>
<GridItem size={{ base: 12, md: 6 }}>...</GridItem>
<GridItem size={{ base: 12, md: 6 }}>...</GridItem>
</Grid>Responsive gap
<Grid gap={{ base: 2, sm: 4, md: 8 }}>
<GridItem size={4}>A</GridItem>
<GridItem size={4}>B</GridItem>
<GridItem size={4}>C</GridItem>
</Grid>Nested grids
<Grid>
<GridItem size={{ md: 8 }}>
<Grid gap={4}>
<GridItem size={6}>Top left</GridItem>
<GridItem size={6}>Top right</GridItem>
<GridItem size={12}>Bottom</GridItem>
</Grid>
</GridItem>
<GridItem size={{ md: 4 }}>Sidebar</GridItem>
</Grid>Reordering
Teul leaves visual order to Tailwind’s order-* utilities. Pass them via className — they take the same responsive prefixes (sm:, md:, …) as any other Tailwind class.
<Grid gap={4}>
<GridItem size={4} className="md:order-3">...</GridItem>
<GridItem size={4} className="md:order-1">...</GridItem>
<GridItem size={4} className="md:order-2">...</GridItem>
</Grid>Offsets
<Grid gap={4}>
<GridItem size={{ sm: 6 }} offset={{ sm: 3 }}>...</GridItem>
<GridItem size={{ md: 4 }} offset={{ md: 1 }}>...</GridItem>
<GridItem size={{ lg: 4 }} offset={{ lg: 2 }}>...</GridItem>
</Grid>API reference
type Breakpoint = "base" | "sm" | "md" | "lg" | "xl" | "2xl"
type GapScale = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12
type GridItemSize = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12Every prop accepts a single value or a per-breakpoint object (e.g. { md: 4, lg: 6 }). GapScale follows Tailwind’s spacing scale.
Grid
| Prop | Type | Default | Notes |
|---|---|---|---|
rowGap | GapScale | 12 | Vertical gap |
colGap | GapScale | 8 | Horizontal gap |
gap | GapScale | — | Shorthand for both axes |
GridItem
| Prop | Type | Default | Notes |
|---|---|---|---|
size | GridItemSize | 12 | Columns to span (1–12). Use 0 to hide at a breakpoint. |
offset | GridItemSize | — | Empty columns before the item |