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.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Beginner |
| Time required | 15-20 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 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
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.
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.
1// In a table column Value expression or a text component:2{{ moment(currentRow.created_at).format('MMM D, YYYY') }}34// Relative time (e.g. '3 days ago'):5{{ moment(currentRow.updated_at).fromNow() }}67// With timezone handling:8{{ moment(currentRow.created_at).utc().local().format('MMM D, YYYY h:mm A') }}910// ISO date to readable:11{{ moment('2024-11-15T15:45:00Z').format('MMMM D, YYYY') }}1213// 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.
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.
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: 26}) }}7// Output: $1,234.5689// Large number with commas:10{{ Number(currentRow.revenue).toLocaleString('en-US') }}11// Output: 1,234,5671213// Percentage:14{{ (currentRow.conversion_rate * 100).toFixed(1) + '%' }}15// Output: 12.5%1617// 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.
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.
1// Format US phone number (10 digits) in a column expression:2{{ currentRow.phone3 ? currentRow.phone.replace(/^(\d{3})(\d{3})(\d{4})$/, '($1) $2-$3')4 : 'N/A'5}}6// '5551234567' → '(555) 123-4567'78// Format ZIP code (ensure 5 digits with leading zero):9{{ String(currentRow.zip_code).padStart(5, '0') }}10// 1234 → '01234'1112// Truncate long text:13{{ currentRow.description?.length > 10014 ? currentRow.description.slice(0, 100) + '...'15 : currentRow.description ?? ''16}}Expected result: Structured fields like phone numbers display in standard human-readable formats.
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.
1// Tag column 'Tag color' expression:2{{3 currentRow.status === 'active' ? 'green' :4 currentRow.status === 'pending' ? 'yellow' :5 currentRow.status === 'rejected' ? 'red' : 'gray'6}}78// Available tag colors: red, orange, yellow, green,9// teal, blue, indigo, purple, pink, grayExpected result: Status columns display as colored tag badges instead of plain text.
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 }}.
1// Transformer: formattedData2// Provides display-ready fields alongside raw values34return (query1.data || []).map(row => ({5 // Keep raw values for writes/calculations6 ...row,78 // Add formatted versions for display9 price_display: Number(row.price).toLocaleString('en-US', {10 style: 'currency', currency: 'USD'11 }),12 created_display: row.created_at13 ? moment(row.created_at).format('MMM D, YYYY')14 : 'Unknown',15 updated_display: row.updated_at16 ? moment(row.updated_at).fromNow()17 : 'Never',18 phone_display: row.phone19 ? row.phone.replace(/^(\d{3})(\d{3})(\d{4})$/, '($1) $2-$3')20 : 'N/A',21 status_display: row.status22 ? row.status.charAt(0).toUpperCase() + row.status.slice(1)23 : 'Unknown',24 growth_display: row.growth_rate != null25 ? (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.
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.
1// Calculated column Value expression:2{{ currentRow.is_verified ? 'Verified' : 'Unverified' }}34// With emoji indicators:5{{ currentRow.is_active ? '✓ Active' : '✗ Inactive' }}67// Boolean column type in Inspector:8// Set column type to 'Boolean' for a checkbox display9// This renders true as a checked checkbox, false as unchecked1011// 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
1// Transformer: formattedUsers2// Pre-formats all display fields for consistent rendering3// across tables, text components, and charts.4// Bind table: {{ formattedUsers.value }}56const raw = query1.data || [];78return raw.map(row => ({9 // Preserve all raw fields for write operations10 ...row,1112 // Dates13 created_display: row.created_at14 ? moment(row.created_at).format('MMM D, YYYY')15 : '—',16 updated_display: row.updated_at17 ? moment(row.updated_at).fromNow()18 : 'Never updated',1920 // Numbers and currency21 revenue_display: Number(row.revenue || 0).toLocaleString('en-US', {22 style: 'currency', currency: 'USD', minimumFractionDigits: 023 }),24 growth_display: row.growth_rate != null25 ? (row.growth_rate >= 0 ? '+' : '') +26 (row.growth_rate * 100).toFixed(1) + '%'27 : '—',2829 // Status (capitalize + tag color logic handled in Inspector)30 status_display: row.status31 ? row.status.charAt(0).toUpperCase() + row.status.slice(1).toLowerCase()32 : 'Unknown',3334 // Phone35 phone_display: row.phone36 ? String(row.phone).replace(/^(\d{3})(\d{3})(\d{4})$/, '($1) $2-$3')37 : '—',3839 // Boolean40 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation