# Text field (/docs/schema/fields/text)

---
title: Text field
description: "`f.text()` is the single-line string. The call picks the varchar width, the width becomes the Zod cap, and the field filters with the full string operator set."
kind: reference
package: questpie
---

| Call                       | Column         | Derived schema        |
| -------------------------- | -------------- | --------------------- |
| `f.text()`                 | `varchar(255)` | `z.string().max(255)` |
| `f.text(n)`                | `varchar(n)`   | `z.string().max(n)`   |
| `f.text({ mode: "text" })` | `text`         | `z.string()`, no cap  |

Text mode is not a textarea. All three render the same single-line control, so
the argument moves the column and not the form. No method on the type reopens
the width. Only `.array()` and the `.drizzle()` escape hatch replace the column.

```ts title="src/questpie/server/collections/posts.ts"
import { collection } from "#questpie/factories";

export const posts = collection("posts").fields(({ f }) => ({
	title: f.text().required(), // varchar(255), NOT NULL
	slug: f.text(120).required(), // varchar(120)
	summary: f.text({ mode: "text" }), // text, no length cap
}));
```

Without `.required()` the field selects as `string | null`. With it, `string`.

## Methods

Six methods come with the type. `.required()`, `.default()`, `.label()`,
`.localized()` and the rest of the base chain work here as on any field.

| Method         | Effect                                      |
| -------------- | ------------------------------------------- |
| `.min(n)`      | Adds `z.string().min(n)`, a character count |
| `.max(n)`      | Adds `z.string().max(n)`, a character count |
| `.pattern(re)` | Adds `z.string().regex(re)`                 |
| `.trim()`      | Sets a `trim` flag                          |
| `.lowercase()` | Sets a `lowercase` flag                     |
| `.uppercase()` | Sets an `uppercase` flag                    |

```ts
username: f.text(32).required().min(3).pattern(/^[a-z0-9_]+$/),
```

<Callout type="warn" title="`.max()` tightens, it never widens">
	The constructor bakes its width into the schema and `.max(n)` adds a second
	check beside it. `f.text(255).max(500)` still rejects 300 characters. Widen by
	changing the constructor argument. Under `{ mode: "text" }` there is no first
	check, so `.max(n)` is the only cap.
</Callout>

<Callout
	type="warn"
	title="`.trim()`, `.lowercase()` and `.uppercase()` do not touch the value"
>
	Each sets a flag. No code transforms the stored string, so `.trim()` writes `"
	hi "` unchanged. Their one reader is the CRDT eligibility check, which counts
	them as refinements and rejects the field. Normalize with `.zod()` or a
	`beforeValidate` hook, which runs before the schema.
</Callout>

## Filtering

Text carries the `stringOps` set, fourteen operators.

| Operator               | Operand             | Matches                                     |
| ---------------------- | ------------------- | ------------------------------------------- |
| `eq` / `ne`            | `string`            | Equal, not equal                            |
| `not`                  | `string`, or `null` | Not equal, or `IS NOT NULL` when given null |
| `in` / `notIn`         | `string[]`          | In the list, not in the list                |
| `like` / `notLike`     | `string`            | `LIKE`, case-sensitive, you write the `%`   |
| `ilike` / `notIlike`   | `string`            | The same, case-insensitive                  |
| `contains`             | `string`            | Substring, the `%` are added for you        |
| `startsWith`           | `string`            | Prefix                                      |
| `endsWith`             | `string`            | Suffix                                      |
| `isNull` / `isNotNull` | `boolean`           | Pass `false` and each inverts               |

```ts
const { docs } = await app.collections.posts.find({
	where: { title: { contains: "guide" }, slug: { eq: "my-first-post" } },
});
```

`contains`, `startsWith` and `endsWith` build the pattern around a bound
parameter. The value is parameterized, but a `%` or `_` inside it still reads as
a wildcard.

## In the admin

The form control is `TextField`. The list cell is `TextCell`, which truncates
and puts the full value in a `title` attribute.

What crosses to the browser:

- `maxLength` becomes the input's `maxLength` attribute, so typing stops at the
  width. `{ mode: "text" }` sends no `maxLength` until you call `.max(n)`, so
  until then there is no attribute and no stop.
- The admin rebuilds a Zod schema from `minLength`, `maxLength` and `pattern`,
  so the form checks before it posts.
- `pattern` crosses as `RegExp.source`, which drops the flags. `/^a/i` is
  case-insensitive on the server and case-sensitive in the form.

A `.max(n)` above the constructor width is the number that crosses, so the
attribute and the browser schema both take it. The server schema keeps both
checks and the column keeps its width, so the form accepts what the write
rejects.

## Lists

`.array()` replaces the column with `jsonb` and swaps the string operators for
the multi-value set. The per-item length still applies, and `.minItems(n)` /
`.maxItems(n)` bound the list.

```ts
tags: f.text(40).array().maxItems(10),
```

## Related

[Textarea](/docs/schema/fields/textarea) is the multi-line sibling, an unbounded
`text` column with the same operators.

[Email](/docs/schema/fields/email) and [URL](/docs/schema/fields/url) wrap a
varchar in a format check and extend `stringOps` with their own operators.

[Arrays](/docs/schema/fields/array) covers what `.array()` does to reads, writes
and filters.

[Validation](/docs/schema/validation) covers the derived schema and `.zod()`,
the way to normalize a value.

[Eligible fields](/docs/schema/collaborative-documents/eligible-fields) covers
why `.crdt()` takes `f.text({ mode: "text" })` and never a varchar one.
