Teul

An opinionated grid system for React and Tailwind

GitHub

Why 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.json

Then import it:

import { Grid, GridItem } from "@/components/ui/teul"

Examples

Responsive size

base: 12 · md: 8
base: 12 · md: 4
base: 12 · md: 6
base: 12 · md: 6
<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

A
B
C
<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

Top left
Top right
Bottom
Sidebar
<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.

1st in DOM · 3rd on md
2nd in DOM · 1st on md
3rd in DOM · 2nd on md
<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

sm offset: 3
md offset: 1
lg offset: 2
<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 | 12

Every prop accepts a single value or a per-breakpoint object (e.g. { md: 4, lg: 6 }). GapScale follows Tailwind’s spacing scale.

Grid

PropTypeDefaultNotes
rowGapGapScale12Vertical gap
colGapGapScale8Horizontal gap
gapGapScaleShorthand for both axes

GridItem

PropTypeDefaultNotes
sizeGridItemSize12Columns to span (1–12). Use 0 to hide at a breakpoint.
offsetGridItemSizeEmpty columns before the item