Skip to main content
RapidDev - Software Development Agency
retool-tutorial

How to Handle Data Formatting in Retool

Format data in Retool tables by setting the column type (Currency, Date, Percent, Tag) in the Table Inspector. For custom formatting, use moment(currentRow.date).format('MMM D, YYYY') for dates and Number(value).toLocaleString('en-US', {style:'currency', currency:'USD'}) for money. Apply consistent formatting across the app in a transformer before binding.

What you'll learn

  • How to use Retool's built-in column format types: Currency, Date, Percent, Tag
  • How to format dates with moment.js inside {{ }} expressions
  • How to format numbers and currencies with toLocaleString()
  • How to use template literals in Text components for composite display values
  • How to apply consistent formatting across multiple components using a transformer
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner8 min read15-20 minRetool Cloud and Self-hostedLast updated March 2026RapidDev Engineering Team
TL;DR

Format data in Retool tables by setting the column type (Currency, Date, Percent, Tag) in the Table Inspector. For custom formatting, use moment(currentRow.date).format('MMM D, YYYY') for dates and Number(value).toLocaleString('en-US', {style:'currency', currency:'USD'}) for money. Apply consistent formatting across the app in a transformer before binding.

Quick facts about this guide
FactValue
ToolRetool
DifficultyBeginner
Time required15-20 min
CompatibilityRetool Cloud and Self-hosted
Last updatedMarch 2026

Display Raw Data in Human-Readable Formats

Raw data from databases and APIs is rarely display-ready. Timestamps come as ISO strings, prices as raw numbers, booleans as 0/1, and statuses as lowercase codes. Retool provides several layers for formatting: built-in column type settings in the Table Inspector, {{ }} expressions using JavaScript formatting functions, and transformer-based pre-processing.

This tutorial covers the most common formatting scenarios: dates, currencies, percentages, phone numbers, and status labels. You'll learn which layer to use for each case and how to keep formatting consistent across your app.

Prerequisites

  • A Retool app with a Table component connected to a query
  • Query data that includes dates, numbers, or status fields needing formatting
  • Basic familiarity with JavaScript string methods and toLocaleString()
  • Optional: moment.js knowledge (bundled in Retool)

Step-by-step guide

1

Use Built-in Column Types for Common Formats

Select your Table component and open the Columns section in the Inspector. Click the gear icon on any column to expand its settings. The 'Column type' dropdown provides built-in formatters: String, Number, Currency, Date, Percent, Boolean, Tag, Button, Image, Link. For a price column, set type to 'Currency' and configure the currency code (USD, EUR, etc.) and number of decimal places. For a created_at column, set type to 'Date' and configure the display format.

Expected result: Columns display with appropriate formatting without any custom code.

2

Format Dates with moment.js

Retool bundles moment.js globally as moment. Use it in any {{ }} expression to parse and format date strings or timestamps. LDML format tokens are also supported via Retool's built-in date column type, but moment gives you more flexibility for custom formats. Common moment format strings: 'MMM D, YYYY' → Nov 15, 2024; 'MM/DD/YYYY' → 11/15/2024; 'YYYY-MM-DD' → 2024-11-15; 'h:mm A' → 3:45 PM; 'MMM D, YYYY h:mm A' → Nov 15, 2024 3:45 PM.

typescript
1// In a table column Value expression or a text component:
2{{ moment(currentRow.created_at).format('MMM D, YYYY') }}
3
4// Relative time (e.g. '3 days ago'):
5{{ moment(currentRow.updated_at).fromNow() }}
6
7// With timezone handling:
8{{ moment(currentRow.created_at).utc().local().format('MMM D, YYYY h:mm A') }}
9
10// ISO date to readable:
11{{ moment('2024-11-15T15:45:00Z').format('MMMM D, YYYY') }}
12
13// In a transformer:
14return data.map(row => ({
15 ...row,
16 createdFormatted: moment(row.created_at).format('MMM D, YYYY'),
17 updatedAgo: moment(row.updated_at).fromNow()
18}));

Expected result: Date columns display in readable formats like 'Nov 15, 2024' instead of raw ISO strings.

3

Format Currencies and Numbers with toLocaleString()

For currency and number formatting in text components or table column expressions, JavaScript's native toLocaleString() is clean and versatile. It respects locale-specific formatting (e.g. comma separators in the US, period separators in Europe). For a built-in currency column in a table, use the Currency column type instead — it is simpler and consistent. Use toLocaleString() for text components and transformer pre-processing.

typescript
1// Currency formatting in a text component or column expression:
2{{ Number(currentRow.price).toLocaleString('en-US', {
3 style: 'currency',
4 currency: 'USD',
5 minimumFractionDigits: 2
6}) }}
7// Output: $1,234.56
8
9// Large number with commas:
10{{ Number(currentRow.revenue).toLocaleString('en-US') }}
11// Output: 1,234,567
12
13// Percentage:
14{{ (currentRow.conversion_rate * 100).toFixed(1) + '%' }}
15// Output: 12.5%
16
17// In a transformer for consistent app-wide formatting:
18return data.map(row => ({
19 ...row,
20 priceFormatted: Number(row.price).toLocaleString('en-US', {
21 style: 'currency', currency: 'USD'
22 }),
23 growthFormatted: (row.growth_rate >= 0 ? '+' : '') +
24 (row.growth_rate * 100).toFixed(1) + '%'
25}));

Expected result: Numbers display with locale-appropriate separators and currency symbols.

4

Format Phone Numbers and Other Structured Strings

Raw phone numbers (e.g. '5551234567') need formatting for display. Use string manipulation in a calculated column or transformer to add formatting. The same approach works for credit card numbers, ZIP codes, or any structured identifier. Note: only apply display formatting for presentation — always store the raw unformatted value in your database.

typescript
1// Format US phone number (10 digits) in a column expression:
2{{ currentRow.phone
3 ? currentRow.phone.replace(/^(\d{3})(\d{3})(\d{4})$/, '($1) $2-$3')
4 : 'N/A'
5}}
6// '5551234567' → '(555) 123-4567'
7
8// Format ZIP code (ensure 5 digits with leading zero):
9{{ String(currentRow.zip_code).padStart(5, '0') }}
10// 1234 → '01234'
11
12// Truncate long text:
13{{ currentRow.description?.length > 100
14 ? currentRow.description.slice(0, 100) + '...'
15 : currentRow.description ?? ''
16}}

Expected result: Structured fields like phone numbers display in standard human-readable formats.

5

Use Tag Column Type for Status Labels

For status fields with a small set of values (active/inactive, pending/approved/rejected), use the Tag column type in the Table Inspector. Configure the tag color mapping: select the column, set type to 'Tag', then in the 'Tag color' field use a conditional expression to map values to colors. This creates colored badge-style indicators without any custom CSS.

typescript
1// Tag column 'Tag color' expression:
2{{
3 currentRow.status === 'active' ? 'green' :
4 currentRow.status === 'pending' ? 'yellow' :
5 currentRow.status === 'rejected' ? 'red' : 'gray'
6}}
7
8// Available tag colors: red, orange, yellow, green,
9// teal, blue, indigo, purple, pink, gray

Expected result: Status columns display as colored tag badges instead of plain text.

6

Apply Consistent Formatting in a Transformer

For consistent formatting across your entire app (not just one table), pre-format values in a transformer and bind all components to the transformer's output. This avoids duplicating format expressions across multiple tables, text components, and charts. Create a transformer that formats all display fields and returns a new array. All components reference {{ formattedData.value }} instead of {{ query1.data }}.

typescript
1// Transformer: formattedData
2// Provides display-ready fields alongside raw values
3
4return (query1.data || []).map(row => ({
5 // Keep raw values for writes/calculations
6 ...row,
7
8 // Add formatted versions for display
9 price_display: Number(row.price).toLocaleString('en-US', {
10 style: 'currency', currency: 'USD'
11 }),
12 created_display: row.created_at
13 ? moment(row.created_at).format('MMM D, YYYY')
14 : 'Unknown',
15 updated_display: row.updated_at
16 ? moment(row.updated_at).fromNow()
17 : 'Never',
18 phone_display: row.phone
19 ? row.phone.replace(/^(\d{3})(\d{3})(\d{4})$/, '($1) $2-$3')
20 : 'N/A',
21 status_display: row.status
22 ? row.status.charAt(0).toUpperCase() + row.status.slice(1)
23 : 'Unknown',
24 growth_display: row.growth_rate != null
25 ? (row.growth_rate >= 0 ? '+' : '') +
26 (row.growth_rate * 100).toFixed(1) + '%'
27 : 'N/A'
28}));

Expected result: All components bound to formattedData.value display consistently formatted values without duplicating format logic.

7

Format Booleans as User-Friendly Labels

Boolean fields (true/false or 0/1) often need human-readable labels. In a table column expression, use a ternary to convert booleans to meaningful text. Use the Boolean column type for simple checkmark/cross display, or a Tag column type for colored labels.

typescript
1// Calculated column Value expression:
2{{ currentRow.is_verified ? 'Verified' : 'Unverified' }}
3
4// With emoji indicators:
5{{ currentRow.is_active ? '✓ Active' : '✗ Inactive' }}
6
7// Boolean column type in Inspector:
8// Set column type to 'Boolean' for a checkbox display
9// This renders true as a checked checkbox, false as unchecked
10
11// In a transformer for multiple booleans:
12return data.map(row => ({
13 ...row,
14 verifiedLabel: row.is_verified ? 'Verified' : 'Pending',
15 activeLabel: row.is_active ? 'Active' : 'Inactive',
16 emailConfirmedLabel: row.email_confirmed ? 'Confirmed' : 'Unconfirmed'
17}));

Expected result: Boolean fields display as clear, readable labels rather than 'true'/'false' or 0/1.

Complete working example

Transformer: formattedUsers
1// Transformer: formattedUsers
2// Pre-formats all display fields for consistent rendering
3// across tables, text components, and charts.
4// Bind table: {{ formattedUsers.value }}
5
6const raw = query1.data || [];
7
8return raw.map(row => ({
9 // Preserve all raw fields for write operations
10 ...row,
11
12 // Dates
13 created_display: row.created_at
14 ? moment(row.created_at).format('MMM D, YYYY')
15 : '—',
16 updated_display: row.updated_at
17 ? moment(row.updated_at).fromNow()
18 : 'Never updated',
19
20 // Numbers and currency
21 revenue_display: Number(row.revenue || 0).toLocaleString('en-US', {
22 style: 'currency', currency: 'USD', minimumFractionDigits: 0
23 }),
24 growth_display: row.growth_rate != null
25 ? (row.growth_rate >= 0 ? '+' : '') +
26 (row.growth_rate * 100).toFixed(1) + '%'
27 : '—',
28
29 // Status (capitalize + tag color logic handled in Inspector)
30 status_display: row.status
31 ? row.status.charAt(0).toUpperCase() + row.status.slice(1).toLowerCase()
32 : 'Unknown',
33
34 // Phone
35 phone_display: row.phone
36 ? String(row.phone).replace(/^(\d{3})(\d{3})(\d{4})$/, '($1) $2-$3')
37 : '—',
38
39 // Boolean
40 verified_display: row.is_verified ? 'Verified' : 'Unverified',
41 active_display: row.is_active ? 'Active' : 'Inactive'
42}));

Common mistakes when handling Data Formatting in Retool

Why it's a problem: Formatting values in the database query instead of the transformer, making them hard to change later

How to avoid: Keep SQL focused on data retrieval and filtering. Do formatting in Retool's transformer layer so you can update display formats without changing database queries.

Why it's a problem: Using moment() directly in a calculated column causing performance issues on large tables

How to avoid: Pre-compute formatted date strings in a transformer. Column expressions run for every rendered row — moment() parsing in a 1000-row table means 1000 moment() calls per render.

Why it's a problem: Displaying NaN when a number field is null or undefined

How to avoid: Always use Number(value || 0) or parseFloat(value ?? 0) before calling toLocaleString() or toFixed(). Null values passed to Number() return 0, preventing NaN display.

Why it's a problem: Overwriting raw field values with formatted strings in the transformer

How to avoid: Add formatted fields alongside raw fields using spread: { ...row, price_display: formatted }. Never replace raw numeric fields with string-formatted versions — you'll lose the ability to use them in calculations or write queries.

Best practices

  • Use built-in column types (Currency, Date, Percent, Tag) for standard formatting — they handle edge cases and are faster to configure than custom expressions.
  • Pre-format data in a transformer for app-wide consistency instead of duplicating format expressions across individual table columns and text components.
  • Keep raw values in your data alongside formatted display versions — you need the raw value for write operations, comparisons, and calculations.
  • Use moment().format() for date formatting but pre-compute it in a transformer rather than calling it per-row in column expressions to avoid repeated parsing overhead.
  • Always null-guard format expressions: Number(value || 0), row.field ?? '—', and similar patterns prevent displaying 'NaN' or blank cells.
  • For status labels with colors, use the Tag column type and a conditional color expression rather than encoding colors in text.
  • Store monetary values as integers (cents) in your database and divide by 100 when displaying to avoid floating-point precision issues.

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I'm building a Retool app with a table showing user records. The SQL query returns these fields: id, first_name, last_name, email, created_at (ISO timestamp), revenue (float), growth_rate (decimal like 0.125), phone (10-digit string), status (lowercase string), is_verified (boolean). Write a Retool transformer that adds display-formatted versions of each field: formatted date, formatted currency, growth as +12.5%, formatted phone number, capitalized status, and Yes/No for boolean. Preserve the raw fields alongside the formatted ones.

Retool Prompt

Add data formatting to my Retool table. I need: (1) created_at column showing 'Nov 15, 2024' format using moment.js, (2) revenue column as USD currency, (3) status column as colored Tag badges (active=green, pending=yellow, inactive=red), (4) growth_rate as +12.5% percentage, and (5) is_verified as 'Verified'/'Unverified'. Use a transformer named 'formattedData' that preserves raw field values alongside formatted display fields.

Frequently asked questions

How do I format a date column in a Retool table without writing code?

Click the gear icon on the column in the Table Inspector's Columns section, then set the Column type to 'Date'. A date format field appears where you can enter LDML format strings like 'MMM D, YYYY' or choose from presets. This requires no JavaScript and applies automatically.

Is moment.js available in Retool without importing it?

Yes. Retool bundles moment.js and it is available globally in all {{ }} expressions and JavaScript code (transformers, JS queries). You do not need an import statement. Use it as moment(value).format('MMM D, YYYY') directly.

How do I display a number as currency in a Retool text component?

Use JavaScript's toLocaleString() method: {{ Number(query1.data[0].price).toLocaleString('en-US', { style: 'currency', currency: 'USD' }) }}. For tables, use the built-in Currency column type which handles the formatting automatically without code.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Learning is great. Shipping is faster with help.

Our engineers have built 600+ apps on Retool and the tools around it. If your project needs to be live sooner than your learning curve allows — book a free consultation.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.