ComponentsThemes
Palette
Default
Stone
Rose
Blue
Green
Violet
Yellow
Orange
6

← Components

Table

Composable, semantic table primitives (shadcn parity). Static, no JS.

Installation

bin/rails g wabi:add table

Example

Source

app/components/ui/table.rb

# frozen_string_literal: true

module Components
  module UI
    class Table < Wabi::Base
      variants do
        base "w-full caption-bottom text-sm"
      end

      def initialize(**attrs) = @attrs = attrs

      def view_template(&)
        user_class = @attrs.delete(:class)
        div(class: "relative w-full overflow-auto") do
          table(**@attrs, class: merge_class(tokens, user_class), &)
        end
      end
    end
  end
end

app/components/ui/table_header.rb

# frozen_string_literal: true

module Components
  module UI
    class TableHeader < Wabi::Base
      variants do
        base "[&_tr]:border-b"
      end

      def initialize(**attrs) = @attrs = attrs

      def view_template(&)
        user_class = @attrs.delete(:class)
        thead(**@attrs, class: merge_class(tokens, user_class), &)
      end
    end
  end
end

app/components/ui/table_body.rb

# frozen_string_literal: true

module Components
  module UI
    class TableBody < Wabi::Base
      variants do
        base "[&_tr:last-child]:border-0"
      end

      def initialize(**attrs) = @attrs = attrs

      def view_template(&)
        user_class = @attrs.delete(:class)
        tbody(**@attrs, class: merge_class(tokens, user_class), &)
      end
    end
  end
end
# frozen_string_literal: true

module Components
  module UI
    class TableFooter < Wabi::Base
      variants do
        base "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0"
      end

      def initialize(**attrs) = @attrs = attrs

      def view_template(&)
        user_class = @attrs.delete(:class)
        tfoot(**@attrs, class: merge_class(tokens, user_class), &)
      end
    end
  end
end

app/components/ui/table_row.rb

# frozen_string_literal: true

module Components
  module UI
    class TableRow < Wabi::Base
      variants do
        base "border-b transition-colors motion-reduce:transition-none hover:bg-muted/50 data-[state=selected]:bg-muted"
      end

      def initialize(**attrs) = @attrs = attrs

      def view_template(&)
        user_class = @attrs.delete(:class)
        # Reflect selection state programmatically so AT users know when a row
        # is selected (data-state="selected" drives the visual via Tailwind;
        # aria-selected carries the same signal to screen readers).
        # Callers mark rows with data-state: "selected"; parent containers
        # should carry role="grid" or rows role="row" per ARIA grid pattern.
        aria_selected = @attrs[:'data-state'] == "selected" ? "true" : nil
        tr(aria_selected: aria_selected, **@attrs, class: merge_class(tokens, user_class), &)
      end
    end
  end
end

app/components/ui/table_head.rb

# frozen_string_literal: true

module Components
  module UI
    class TableHead < Wabi::Base
      variants do
        base "h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0"
      end

      # +scope+ defaults to "col" so screen readers can associate header cells
      # with data cells. Override with scope: "row" for row-header <th> cells.
      def initialize(scope: "col", **attrs)
        @attrs = attrs.merge(scope: scope)
      end

      def view_template(&)
        user_class = @attrs.delete(:class)
        th(**@attrs, class: merge_class(tokens, user_class), &)
      end
    end
  end
end

app/components/ui/table_cell.rb

# frozen_string_literal: true

module Components
  module UI
    class TableCell < Wabi::Base
      variants do
        base "p-4 align-middle [&:has([role=checkbox])]:pr-0"
      end

      def initialize(**attrs) = @attrs = attrs

      def view_template(&)
        user_class = @attrs.delete(:class)
        td(**@attrs, class: merge_class(tokens, user_class), &)
      end
    end
  end
end

app/components/ui/table_caption.rb

# frozen_string_literal: true

module Components
  module UI
    class TableCaption < Wabi::Base
      variants do
        base "mt-4 text-sm text-muted-foreground"
      end

      def initialize(**attrs) = @attrs = attrs

      def view_template(&)
        user_class = @attrs.delete(:class)
        caption(**@attrs, class: merge_class(tokens, user_class), &)
      end
    end
  end
end

Accessibility

  • Uses native table elements (table/thead/tbody/tfoot/tr/th/td/caption), so screen readers get the table semantics for free.
  • TableHead renders a th; add scope: "col" / scope: "row" via attrs when the header relationship isn't obvious.
  • TableCaption gives the table an accessible name — prefer it over a separate heading.