Skip to main content

UX Standards — 07C: UDT Column Visibility and Default Hidden Fields

Governs: Which columns are visible by default, which are hidden, and how users manage column visibility. Parent rules: See 07-UNIVERSAL-DATA-TABLE.md and 00-OVERVIEW-AND-CSS-RULES.md first.


The Core Rule: Submit All, Display Default

Every field defined in the data model is included in the table definition. Only a curated default subset is visible to the user. Hidden fields remain accessible — users can show them at any time.

This means:

  • The backend query fetches ALL fields (or at minimum all non-blob fields)
  • The TanStack Table column definitions include ALL fields
  • defaultColumnVisibility hides the fields that are technical/audit-only by default
  • Users can reveal any hidden column via the Column Selector in the toolbar

Field Categories

CategoryDefault StateExamples
Primary display fieldsVisibleName, Status, Email, Type, Code, Created Date
Secondary fieldsVisiblePhone, Role, Tenant, Flags
System ID fieldsHiddenid, uuid, user_uuid, tenant_uuid
Audit timestamp fieldsHiddencreated_at, updated_at, deleted_at, modified_at
Audit user fieldsHiddencreated_by, updated_by, deleted_by
Internal flags/metadataHiddenis_deleted, is_system, version, etag
Foreign key referencesHiddenproduct_type_id, category_uuid (show the joined name instead)
Large text / JSON fieldsHiddennotes, metadata, raw_payload, config_json

Column Definition Pattern

// columns.tsx — all fields defined, defaultColumnVisibility controls what's shown

export const defaultColumnVisibility: Record<string, boolean> = {
// Visible by default (these keys are omitted — visibility defaults to true)
// Hidden by default:
id: false,
uuid: false,
user_uuid: false,
tenant_uuid: false,
created_at: false,
updated_at: false,
deleted_at: false,
created_by: false,
updated_by: false,
is_deleted: false,
is_system: false,
raw_payload: false,
};

export function getColumns(): ColumnDef<UserRecord>[] {
return [
// === CHECKBOX (always first, never hidden) ===
{
id: 'select',
enableHiding: false,
// ...
},

// === PRIMARY VISIBLE FIELDS ===
{
accessorKey: 'username',
header: ({ column }) => <DataTableColumnHeader column={column} title="Username" />,
enableHiding: true,
},
{
accessorKey: 'name',
header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
enableHiding: true,
},
{
accessorKey: 'email',
header: ({ column }) => <DataTableColumnHeader column={column} title="Email" />,
enableHiding: true,
},
{
accessorKey: 'status',
header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
enableHiding: true,
},

// === HIDDEN BY DEFAULT (but accessible) ===
{
accessorKey: 'id',
header: ({ column }) => <DataTableColumnHeader column={column} title="ID" />,
enableHiding: true, // User CAN show this
// Will be hidden via defaultColumnVisibility
},
{
accessorKey: 'uuid',
header: ({ column }) => <DataTableColumnHeader column={column} title="UUID" />,
enableHiding: true,
},
{
accessorKey: 'created_at',
header: ({ column }) => <DataTableColumnHeader column={column} title="Created At" />,
enableHiding: true,
cell: ({ row }) => (
<span className="text-muted-foreground text-xs">
{formatDateTime(row.getValue('created_at'))}
</span>
),
},
{
accessorKey: 'updated_at',
header: ({ column }) => <DataTableColumnHeader column={column} title="Updated At" />,
enableHiding: true,
},
{
accessorKey: 'created_by',
header: ({ column }) => <DataTableColumnHeader column={column} title="Created By" />,
enableHiding: true,
},
{
accessorKey: 'updated_by',
header: ({ column }) => <DataTableColumnHeader column={column} title="Updated By" />,
enableHiding: true,
},

// === NEVER HIDDEN (actions column) ===
{
id: 'actions',
enableHiding: false,
// ...
},
];
}

TanStack Table Initialization

const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
defaultColumnVisibility // Start with the curated default
);

const table = useReactTable({
data,
columns,
state: {
columnVisibility,
// ...other state
},
onColumnVisibilityChange: setColumnVisibility,
// ...
});

Reset to Defaults

The toolbar's Reset & Refresh button restores column visibility to defaultColumnVisibility:

const handleResetAndRefresh = () => {
// Resets column visibility to the default set
if (defaultColumnVisibility && onColumnVisibilityChange) {
onColumnVisibilityChange(defaultColumnVisibility);
}
// Also resets: sorting, filters, selection, search, column order
onRefresh?.();
};
// Pass to DataTableToolbar:
<DataTableToolbar
table={table}
defaultColumnVisibility={defaultColumnVisibility}
defaultColumnOrder={defaultColumnOrder}
onColumnVisibilityChange={setColumnVisibility}
onColumnOrderChange={setColumnOrder}
onRefresh={refetch}
/>

Column Selector UI

Component: DataTableColumnSelector Trigger icon: Columns3 h-4 w-4 — tooltip: "Manage columns"

The column selector opens a draggable, resizable dialog (ResizableDialogContent) — NOT a popover.

Dialog Specification

PropertyValue
Default size400 × 450 px
Minimum size300 × 300 px
Maximum size600 × 700 px
DraggableYes — drag the title bar to reposition
ResizableYes — drag dialog edge/corner
Blocks tableNo — table remains interactive while open

Visual Anatomy

┌─────────────────────────────────────────┐
│ ⠿⠿ Manage Columns [✕] │ ← Title bar (draggable), ResizableDialogContent
│ Drag to reorder. Click eye to toggle.│
│ (9 of 12 visible) │
├─────────────────────────────────────────┤
│ [👁 Show All] [🚫 Hide All] [↺ Reset] │ ← Bulk action buttons (outline, size sm)
├─────────────────────────────────────────┤
│ ┌───────────────────────────────────┐ │
│ │ ⠿ select Required │ │ ← canHide=false: shows "Required" label
│ ├───────────────────────────────────┤ │
│ │ ⠿ username Required │ │
│ ├───────────────────────────────────┤ │
│ │ ⠿ fullName 👁 │ │ ← canHide=true: Eye icon = visible
│ ├───────────────────────────────────┤ │
│ │ ⠿ phone_number 👁 │ │
│ ├───────────────────────────────────┤ │
│ │ ⠿ email 👁 │ │
│ ├───────────────────────────────────┤ │
│ │ ⠿ status Required │ │
│ ├───────────────────────────────────┤ │
│ │ ⠿ created_at 🙈 │ │ ← EyeOff = currently hidden
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘

Column Item Anatomy

Each row in the column list contains (left to right):

ElementComponentDescription
Drag handleGripVertical h-4 w-4 text-muted-foregroundcursor-grab — drag to reorder
Column name<span className="flex-1 truncate text-sm">The column label from meta.label or column ID
Eye toggle / RequiredEye or EyeOff (h-4 w-4) or <span>Required</span>Toggle visibility, or locked indicator

Column item container:

className="flex items-center gap-2 rounded-md border bg-card p-2"
// When dragging: opacity-50 shadow-lg
// When hidden: opacity-60

Eye / Required Logic

// Column with enableHiding: true (default)
{column.isVisible
? <Eye className="h-4 w-4" /> // Visible — click to hide
: <EyeOff className="h-4 w-4" /> // Hidden — click to show
}

// Column with enableHiding: false
<span className="px-1 text-xs text-muted-foreground">Required</span>
// Tooltip: "This column cannot be hidden"

Bulk Action Buttons

Three buttons inside the dialog, above the column list:

ButtonIconAction
Show AllEye mr-1 h-3 w-3Makes all columns visible
Hide AllEyeOff mr-1 h-3 w-3Hides all canHide=true columns
ResetRotateCcw mr-1 h-3 w-3Restores default visibility + default order

All three are variant="outline" size="sm".

Important Implementation Notes

  • Changes apply immediately (autoApply=true by default) — no Apply/Cancel needed
  • If autoApply=false, a footer with Cancel + "Apply Order" buttons appears
  • Drag-to-reorder uses @dnd-kit/core + @dnd-kit/sortable with verticalListSortingStrategy
  • All columns appear in the list — including enableHiding: false ones (they just show "Required" instead of eye icon)
  • The list is rendered in columnOrder sequence, so visible and hidden columns are intermixed (no separator)
  • Changes to column order also propagate via onColumnOrderChange prop

enableHiding: false — Columns That Cannot Be Hidden

Some columns must never be hidden:

ColumnWhy
select (checkbox)Required for row selection to work
actionsRequired for per-row actions
Primary identifier (e.g., username, code)User must always be able to identify the row

Set enableHiding: false in the column definition for these:

{
id: 'select',
enableHiding: false, // Will NOT appear in column selector
}

Columns with enableHiding: false do not appear in the Column Selector popover at all.


Persistence of User Preferences (Optional)

For applications where users expect their column visibility to persist across sessions:

// Save to localStorage
const VISIBILITY_STORAGE_KEY = `table-visibility-${doctype}`;

const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(() => {
const saved = localStorage.getItem(VISIBILITY_STORAGE_KEY);
return saved ? JSON.parse(saved) : defaultColumnVisibility;
});

// Persist on change
const handleVisibilityChange = (visibility: VisibilityState) => {
setColumnVisibility(visibility);
localStorage.setItem(VISIBILITY_STORAGE_KEY, JSON.stringify(visibility));
};

Do not persist by default — the Reset & Refresh button must always restore the curated default. Only add persistence when explicitly required.


Audit Field Display Standards

When audit/system fields ARE shown (user has unhidden them), format them appropriately:

// Timestamp columns — relative time + absolute on hover
{
accessorKey: 'created_at',
cell: ({ row }) => {
const date = new Date(row.getValue('created_at'));
return (
<Tooltip>
<TooltipTrigger>
<span className="text-xs text-muted-foreground">
{formatRelativeTime(date)} {/* e.g., "3 days ago" */}
</span>
</TooltipTrigger>
<TooltipContent>
{formatDateTime(date)} {/* e.g., "Jan 15, 2025 at 2:34 PM" */}
</TooltipContent>
</Tooltip>
);
}
}

// UUID columns — truncated with full value on hover
{
accessorKey: 'uuid',
cell: ({ row }) => {
const uuid = row.getValue('uuid') as string;
return (
<Tooltip>
<TooltipTrigger>
<code className="text-xs text-muted-foreground font-mono">
{uuid?.slice(0, 8)}
</code>
</TooltipTrigger>
<TooltipContent>
<code className="text-xs font-mono">{uuid}</code>
</TooltipContent>
</Tooltip>
);
}
}

Standard Default Column Sets by Page Type

User Management Lists

export const defaultColumnVisibility = {
// Visible: username, name, email, status, tenant, roles, flags, actions
// Hidden:
id: false, uuid: false, user_uuid: false,
created_at: false, updated_at: false, deleted_at: false,
created_by: false, updated_by: false,
is_deleted: false, is_system: false, password_hash: false,
};

Product / Config Lists

export const defaultColumnVisibility = {
// Visible: code, name, type, category, status, actions
// Hidden:
id: false, uuid: false, tenant_uuid: false,
created_at: false, updated_at: false,
created_by: false, updated_by: false,
sort_order: false, internal_notes: false,
};

Transaction / Log Lists

export const defaultColumnVisibility = {
// Visible: date, type, status, source, destination, actions
// Hidden — extremely detailed fields:
id: false, uuid: false, tenant_uuid: false,
raw_payload: false, error_details: false,
created_by: false, updated_by: false,
processing_time_ms: false, retry_count: false,
};

Violation Checklist

  • defaultColumnVisibility defined as a constant, not inline in useState
  • All system/audit/UUID fields are false in defaultColumnVisibility
  • select and actions columns have enableHiding: false
  • Primary identifier column has enableHiding: false
  • defaultColumnVisibility passed to DataTableToolbar for Reset & Refresh
  • UUID values truncated to 8 chars with full value in tooltip
  • Timestamps in audit columns shown as relative time with absolute in tooltip
  • Column selector shows visible columns first, hidden second