Tables in WordPress are a quietly painful problem. The default editor block produces something that works but looks like a print spreadsheet pasted into a webpage. The page builders mostly skip tables entirely (Elementor’s "Table" widget exists but its sorting is busted and its mobile rendering is rough). Half of "table plugins" on the wordpress.org repo are ports of jQuery DataTables wrapped in a half-baked admin UI.
I started looking for a real solution when a client asked for a price comparison page: 30 products, 8 columns, sortable, searchable, filterable by category, responsive on phones. The Google Sheets they gave me as the source of truth was updated weekly by their ops team. The default WP table block was a no-go. WPDataTables I tried first was capable but the UI felt like a vintage Joomla extension. After a couple of dead ends I landed on Ninja Tables. Then on the Pro version because the Free was so good I wanted to see what the paywall opened up. Three projects later it’s the table plugin I install by default.
Together they handle 95% of what people want from a "table plugin" without making you write JavaScript.
Table of Contents
- What Ninja Tables actually is
- The seven data sources
- Building a table from scratch
- Google Sheets as a live source
- WP Posts and WooCommerce products as tables
- Custom SQL queries (for the brave)
- Installation and setup
- Where this fits in real projects
- Developer reference
- Performance, mobile, and rough edges
- Pricing and licensing
- FAQ
- Should you install it?
What Ninja Tables actually is
Ninja Tables is a two-part product from WPManageNinja, the same team behind Fluent Forms and FluentCRM. Free version lives on wordpress.org with a million-plus active installs and handles basic tables: manually entered rows, CSV import, sortable headers, mobile-responsive layouts, searchable. Pro adds the things that turn a "table plugin" into a "data presentation layer": pulling rows from WP posts, WooCommerce products, Google Sheets, custom SQL queries, or Fluent Forms submissions; conditional formatting; advanced column types (images, buttons, computed values); per-table custom CSS; form-to-table capture; and a few more.
You install both. Free first, then Pro layers on top via the standard WordPress add-on pattern. The Pro plugin doesn’t replace Free; it extends it with new tabs, new data sources, and new column types in the existing UI.
What you don’t get with Ninja Tables: a true spreadsheet editor with formulas across cells, pivot-table-style data analysis, or charting (Pro has basic chart rendering via a separate "Charts" submenu, but it’s not the core focus). For those you’d want wpDataTables or a real spreadsheet plugin. Ninja Tables is opinionated about being a table renderer more than a data tool.
The seven data sources
The "Create a Table" modal is where the value of Pro becomes obvious. When you click Create Your First Table you get a modal with seven source choices:
- Default (manual): type rows by hand, or paste from Excel/Sheets. Same flow as the free plugin.
- Drag & Drop Table: manual rows with a different reorder-by-drag UX. Useful for short, presentation-focused tables.
- Connect Fluent Forms: every submission to a Fluent Form becomes a row. Pair with Fluent Forms (free or Pro) and you’ve got a live "form responses" table on a public page.
- WP Posts: query any post type with full WP_Query argument support (taxonomies, meta, dates, ordering), pick which fields become columns, table renders the result. Auto-updates when posts change.
- Connect Google Sheets: paste a Sheets URL, set a refresh interval, the table syncs from the spreadsheet. The ops team edits Sheets, the website updates.
- Connect External CSV: point at a remote CSV URL with a refresh interval. Use case: vendor CSV uploaded to S3 daily, table reads from it.
- Custom SQL Query: write your own SELECT statement against the WP database, the columns are the SELECT’d fields. Gated by capability so non-admins can’t break things.
After picking the source, you choose the rendering engine: FooTable (lightweight, responsive, ~80KB JS) or DataTables (jQuery DataTables for advanced features like server-side processing, multi-column filter UI, fixed headers). FooTable is the default and it’s what you’ll want 80% of the time. DataTables only matters when you’re past ~1000 rows or need DataTables-specific UI like Excel-export buttons.
Building a table from scratch
Picking Default drops you into the standard table builder. You define columns (key, label, type) then add rows. Column types include text, number, date, image, link, button, HTML (rich), select, multi-select, formula, rating (stars). For each column you can set: sortable yes/no, searchable yes/no, hidden on mobile yes/no, hidden in admin yes/no, custom CSS classes, conditional formatting rules.
The conditional formatting deserves a callout. For any column you can stack rules like:
- "If value < 10 → cell color red, text white"
- "If value matches
wholesale-*→ cell font-weight bold, prefix the text with 🏭" - "If column ‘stock’ is 0 → entire row strikethrough"
The rule editor isn’t fancy but it covers the common cases. The actual cell styling is real CSS, so once you save the table it inherits whatever your theme defines for .ninja_table td.cell-class-name.
Rows are added inline or imported as CSV/JSON. The CSV importer matches columns by header row by default; you can manually map if headers don’t match. Re-importing the same CSV (with a unique-ID column) updates existing rows by ID rather than creating duplicates. Useful for syncing from a Sheets export → CSV → import flow when you don’t want a live Sheets connection.
Google Sheets as a live source
This is the feature that closes most deals for the Pro license. The pattern:
- Create a Google Sheet with your data, share it as "Anyone with the link can view".
- In Ninja Tables, Create New Table → Connect Google Sheets.
- Paste the Sheets URL. The plugin fetches the sheet, shows a preview, asks you to confirm columns.
- Set a refresh interval: 30 minutes, hourly, every 4 hours, daily. Or "manual only".
- Save. The table is live. Embed it on a page via the shortcode
[ninja_tables id=N].
What happens behind the scenes: Ninja Tables stores a snapshot of the sheet rows in its own table (wp_ninja_table_items). The refresh job runs via WP-Cron and pulls fresh data from Sheets. Page visitors hit the cached snapshot, not Sheets directly. So performance stays fast even if your Sheet has 10,000 rows.
The ops team edits in Sheets. Marketing reviews the embedded table on the staging site. When something looks wrong, they fix it in Sheets and within the refresh interval the website reflects the change. No copy-paste, no manual import, no "wait for the developer to update the table".
A few gotchas worth knowing:
- Sheets URLs need to be public ("Anyone with the link") for the simple integration. There’s a separate Google API key flow if you want to keep the sheet private; it adds a layer of setup but works.
- If the column headers change in Sheets, the column mapping breaks and the table needs a manual reconfigure. Don’t rename columns lightly.
- Cell formulas in Sheets are evaluated by Sheets and the computed value is what Ninja Tables fetches. So you can use Sheets formulas freely; only the output appears on the website.
WP Posts and WooCommerce products as tables
The WP Posts source is the developer-favorite feature. You’re effectively building a WP_Query through a UI and rendering the result as a table.
The configuration screen lets you set:
- Post type (any registered type:
post,page,product, your custom CPTs) - Status (published, draft, any custom status)
- Taxonomy filter (in category X, has tag Y, etc.)
- Meta filter (post_meta key/value with comparison)
- Date range
- Order by (title, date, author, menu_order, meta_value_num, etc.) and direction
- Posts per page (or all)
Then you pick columns. The available columns include every standard post field (ID, title, content, excerpt, date, author), every registered taxonomy, every post meta key (ACF / Meta Box / Pods / JetEngine fields are detected automatically if those plugins are installed), the featured image, custom fields, and so on. You drag the ones you want into the table, name each column, set its type and rendering rules.
A real example from a recent project: a recipes site with 200 recipe CPTs, each with custom meta for cook time, calories, cuisine, and a hero image. The "Browse all recipes" page used to be a clunky archive template. We replaced it with a Ninja Tables WP Posts table showing image / title / cuisine / cook time / calories, sortable by any column, filterable by cuisine via a dropdown, with the title cell linking to the recipe single page. Took two hours from "I need this" to live. The archive template still exists for SEO and direct category browsing, but the table is what most visitors actually use.
For WooCommerce, swap the source to "WooCommerce Products" and you get product-aware columns: price, regular price, sale price, stock, attributes, variations expanded, ratings, image, add-to-cart button. The "Add to Cart" column type is especially nice, drop it in and you’ve got a functional product table where every row has a one-click cart button, no extra plugin needed.
Custom SQL queries (for the brave)
For the cases where the UI’s WP_Query builder isn’t enough, there’s the Custom SQL source. You type a SELECT statement, Ninja Tables parses it (using a PHP SQL parser, not just raw exec), maps each SELECT’d column to a table column, runs the query at render time (or on a cron, cached), renders the result.
A real query I’ve shipped in production:
SELECT
p.ID AS id,
p.post_title AS title,
pm_price.meta_value AS price,
COUNT(o.ID) AS order_count,
SUM(oi_total.meta_value) AS revenue
FROM wp_posts p
LEFT JOIN wp_postmeta pm_price ON p.ID = pm_price.post_id AND pm_price.meta_key = '_price'
LEFT JOIN wp_woocommerce_order_itemmeta oim_pid ON oim_pid.meta_key = '_product_id' AND oim_pid.meta_value = p.ID
LEFT JOIN wp_woocommerce_order_items oi ON oi.order_item_id = oim_pid.order_item_id
LEFT JOIN wp_woocommerce_order_itemmeta oi_total ON oi_total.order_item_id = oi.order_item_id AND oi_total.meta_key = '_line_total'
LEFT JOIN wp_posts o ON o.ID = oi.order_id AND o.post_type = 'shop_order' AND o.post_status IN ('wc-completed','wc-processing')
WHERE p.post_type = 'product' AND p.post_status = 'publish'
GROUP BY p.ID
ORDER BY revenue DESC
LIMIT 50
That’s a "top 50 products by lifetime revenue" report. The result is a sortable table on a private admin page. No custom plugin, no wpdb->get_results() template overrides, no maintenance burden. The SQL is in plain view; if it ever breaks (because of a WC schema change in WooCommerce 12, for example) the fix is editing one query string.
A safety note: the SQL feature is admin-only by default. The plugin sanitizes the query to disallow DROP, DELETE, UPDATE, INSERT keywords. You can override via the ninja_table_sql_permission filter if you have a specific reason, but the default safety is reasonable for the audience.
Installation and setup
Three plugins (well, two), one workflow.
- Install Ninja Tables (free) from Plugins → Add New → search "Ninja Tables". Install and activate.
- Upload Ninja Tables Pro at Plugins → Add New → Upload Plugin. Install and activate.
- Visit Ninja Tables → Tables to land on the welcome panel. Click Create Your First Table to test the flow.
That’s it from the install side. The Pro plugin adds tabs to the existing Ninja Tables admin pages; no separate menu, no settings migration needed.
If you’ll be using Google Sheets as a source, also do this once:
- Make the Sheet public (Share → "Anyone with the link can view") OR generate a Google API key with Sheets read scope in Google Cloud Console and paste it into Ninja Tables → Settings → Google Sheets. Public sheets are easier; API key is needed for private sheets.
If you’ll be using Fluent Forms integration:
- Install Fluent Forms (free is enough for basic capture). The integration adds an "Insert to Ninja Table" action option in any Fluent Form’s settings.
Where this fits in real projects
I’ll list specific scenarios where Ninja Tables Pro has been the right call, with the alternatives I considered.
Product comparison page on a single-product e-commerce site. 12 product variations, 8 spec columns, mobile users need to see specs without scrolling horizontally. Ninja Tables with the Stack view + Hide on Mobile column setting solved it cleanly. Alternative considered: Elementor’s Table widget + custom CSS. Rejected because of weak mobile rendering.
Live race results during a triathlon event. Race timing system exports a CSV to an S3 bucket every 5 minutes. Ninja Tables External CSV source points at the bucket URL. Visitors get near-real-time updates. Alternative: custom plugin pulling from the timing API. Rejected because Ninja Tables solved it without code.
Affiliate product table for a product review site. 30 products from an affiliate program, displayed as a table with image / name / star rating / price / "Buy Now" button. Ninja Tables WP Posts source + custom CPT for affiliate products. The Buy Now button column is configured to link to each row’s affiliate URL meta field. Pair with ThirstyAffiliates-style link cloaking and you’ve got a clean affiliate revenue table.
"Our staff" directory for a 40-employee company. Custom employee CPT, table source = WP Posts, columns = photo / name / role / department / email. Conditional formatting highlights any row with _is_on_leave = 1. Updates when HR edits the employee CPT, no separate maintenance.
Course catalog for an LMS site running LearnDash. Custom SQL query joining LearnDash’s course post type with enrollment counts. Result is a "Most popular courses" table on the homepage that updates daily. Alternative: hand-curated featured course list. Rejected because curation is a treadmill.
If two of those map to your work, Pro will pay for itself.
Developer reference
The hook surface is smaller than some plugins (94 filters in the Pro release) but the ones that exist are well-placed.
Modify a Posts-source query
add_filter( 'ninja_table_post_table_args', function ( $args, $table_id ) {
if ( (int) $table_id === 42 ) {
$args['meta_query'][] = [
'key' => '_is_featured',
'value' => '1',
'compare' => '=',
];
}
return $args;
}, 10, 2 );
Useful when the UI’s WP_Query builder doesn’t expose a specific arg you need (custom meta comparison operators, nested OR/AND groups, etc.).
Modify a WooCommerce-source query
add_filter( 'ninja_table_woo_table_query_terms', function ( $args, $table_id ) {
if ( (int) $table_id === 7 ) {
$args['tax_query'][] = [
'taxonomy' => 'product_visibility',
'field' => 'name',
'terms' => [ 'exclude-from-catalog', 'exclude-from-search' ],
'operator' => 'NOT IN',
];
}
return $args;
}, 10, 2 );
Restrict SQL queries per user role
add_filter( 'ninja_table_sql_permission', function ( $allowed, $user, $query ) {
if ( ! current_user_can( 'manage_options' ) ) {
return false; // only admins can run any SQL source
}
// Forbid joins on the users table even for admins
if ( stripos( $query, 'wp_users' ) !== false ) {
return false;
}
return $allowed;
}, 10, 3 );
Add HTML attributes to a row’s <tr>
add_filter( 'ninja_tables_item_attributes', function ( $attributes, $row, $table_id ) {
if ( ! empty( $row['stock'] ) && (int) $row['stock'] === 0 ) {
$attributes['class'] = ( $attributes['class'] ?? '' ) . ' row-out-of-stock';
$attributes['data-stock'] = '0';
}
return $attributes;
}, 10, 3 );
Combine with custom CSS targeting tr.row-out-of-stock for visual styling.
Hook into row CRUD
add_action( 'ninja_table_after_add_item', function ( $row_data, $table_id, $row_id ) {
// Push the new row to an external CRM
if ( (int) $table_id === 12 ) {
wp_remote_post( 'https://crm.example.com/api/leads', [
'body' => $row_data,
'headers' => [ 'Authorization' => 'Bearer ' . MY_CRM_TOKEN ],
] );
}
}, 10, 3 );
Customize an ACF value before display
add_filter( 'ninja_table_format_acf_value', function ( $value, $field, $post_id ) {
if ( $field['type'] === 'image' && is_array( $value ) ) {
return '<img src="' . esc_url( $value['sizes']['thumbnail'] ) . '" alt="' . esc_attr( $value['alt'] ) . '">';
}
return $value;
}, 10, 3 );
Bump the remote CSV timeout
add_filter( 'ninja_tables_remote_csv_timeout', function ( $timeout ) {
return 30; // bump from default 10s
} );
Useful when fetching large CSVs from slow remote hosts.
REST endpoints
Ninja Tables exposes a REST API at wp-json/ninja-tables/v1/ for listing tables, fetching rows, adding rows, and updating rows. Requires the standard WP REST auth (cookie + nonce for logged-in admin, or application passwords). Not heavily documented but reading the endpoint controllers in the plugin source is sufficient to figure out the payload shape.
Performance, mobile, and rough edges
Performance. The FooTable rendering engine is light (about 80KB of JS plus 20KB of CSS for the default theme). Tables up to ~500 rows render fast on initial page load. Past 1000 rows you’ll want to switch the rendering engine to DataTables which does server-side processing, fetching pages of rows via AJAX as the user scrolls.
Page caching plays nicely as long as you’re not using AJAX-only features (live search, server-side pagination). For Google Sheets sources, the cached snapshot in wp_ninja_table_items is what the page renders, so caching at the page level works fine; new visitors get the cached page, the snapshot updates on the cron schedule, the next cache invalidation picks up the new data. Pair with WP-Optimize Premium for production hosts.
Mobile. Two modes: "Responsive" (the default, columns collapse into a foldable rows on small screens) and "Stack" (each row becomes a card, header→value pairs shown vertically). Both look better than 95% of WordPress tables. The card view is the right choice when your columns are very different widths (image + long description + a couple of short metadata fields).
You can mark individual columns as "Hidden on mobile" so the responsive layout simplifies to just the columns that matter on small screens.
Rough edges I’ve hit.
- The Vue 2 admin UI is showing its age. Loading the table list takes 2-3 seconds even on a fast host, the modals are slightly janky on slower laptops. Newer plugins moved to Vue 3 or React; Ninja Tables is still on the old stack. Functional but not snappy.
- Bulk operations are limited. You can’t bulk-delete 50 rows; you have to delete them one at a time or via SQL. Not a dealbreaker, but worth knowing.
- Image columns don’t optimize images. A 4MB JPEG in an image column ships at 4MB. Pre-optimize uploads via Smush Pro or similar; Ninja Tables won’t do it.
- The Charts module is bare-bones. If you need real charting, look at wpDataTables or a dedicated chart plugin. Ninja Tables charts are okay for a quick bar/line/pie but lack the polish for primary-content visualizations.
- The Permissions tab is a single role-list. You can grant "edit tables" to Editor/Author/Contributor but not configure per-table permissions or per-column field-level access. For multi-author setups with complex permissions, this can feel restrictive.
- Google Sheets sync uses WP-Cron which is unreliable on low-traffic sites. Replace WP-Cron with a server cron if your refresh schedule is critical.
Pricing and licensing
WPManageNinja sells Ninja Tables Pro as an annual subscription with single-site / 5-site / 50-site / unlimited tiers, and a lifetime option. Pricing changes occasionally; check ninjatables.com for the current numbers.
The free Ninja Tables plugin is on wordpress.org and stays free. It handles manual tables, CSV import, sortable headers, basic responsive layouts. For most personal sites and many small-business sites, free is enough. Pro becomes worth it when you need a dynamic data source (WP Posts / WooCommerce / Sheets / SQL) or conditional formatting.
That’s what I run on staging environments and side projects. For production sites where you want vendor support, buy direct.
FAQ
Do I need the free plugin if I have Pro?
Yes. Pro extends the free plugin; you install both. Free first, then Pro on top.
Can I import an existing Google Sheet?
Yes, three ways: one-time CSV export from Sheets and import as a manual table; live connection via the Connect Google Sheets source (refreshes on a cron); or via a Google API key for private sheets. The live connection is the typical choice.
Does it support nested tables / cells with multiple values?
Nested tables, no. Cells with multiple values, yes, since there’s an "HTML" column type where the cell content can be arbitrary HTML including lists, nested elements, links, etc.
Will sorting and filtering work after a CDN cache?
Yes. Sorting and filtering happen client-side in the browser (FooTable) or via AJAX requests (DataTables with server-side processing). Page caching doesn’t affect either flow.
Can I export a table back to CSV?
Yes, from Tools and Settings → Export. CSV and JSON exports of the current table data. Useful for round-tripping data through Excel.
Is the search/filter UI accessible?
Reasonably. The default theme uses semantic table markup, proper <th scope> attributes, ARIA labels on the search input and pagination controls. Keyboard navigation works. Not exhaustively WCAG-tested but baseline-accessible.
How does it compare to wpDataTables?
Different audiences. wpDataTables is more developer-focused (heavy on DataTables-style features, server-side processing, SQL editor). Ninja Tables is more designer-focused (better mobile rendering, cleaner default themes, simpler UI). I’ve shipped both. Ninja Tables for client-facing tables; wpDataTables when the client themselves is a data analyst who wants Excel-like features.
Does the Fluent Forms integration work with non-Fluent forms?
Only directly with Fluent Forms. For other form plugins (WPForms, Gravity Forms, Contact Form 7), you’d write a custom action hook from the form’s submission event into the Ninja Tables REST API or use the ninja_table_after_add_item hook in reverse.
Will tables break if I switch themes?
The CSS for the tables is shipped by Ninja Tables itself, not the theme. So tables render the same regardless of theme. Theme-defined font and color variables may cascade in (which is usually fine), but the table structure is independent.
Can I make a table editable by visitors?
Yes (capability-gated). Pro has an "Inline editing from the frontend" option that lets visitors with specific user capabilities edit cells directly on the public table. Useful for collaborative tables (a team availability schedule, a shared todo list, etc.). Default-off and never enabled for anonymous users.
Should you install it?
Map this against your work:
- You have at least one page where you want a table sourced from WP posts, WooCommerce, or a Google Sheet.
- The default WP table block (or your page builder’s table widget) has frustrated you on a mobile device in the last six months.
- You’ve ever copy-pasted a Google Sheet into the WP block editor for a page and immediately regretted it.
- You’re shipping a product comparison, a directory, a price list, or a public data table this year.
Two yes-es and Pro is worth it. The free plugin alone is excellent for simple tables; Pro is what you reach for when the data source needs to live somewhere other than the WordPress post you’re editing.
Install both plugins, build a Google Sheets-backed table in 10 minutes, and you’ll know whether it fits your stack. For me it has, on five projects in a row, which is the strongest endorsement I can give a plugin.