//! # [Ratatui] Table example
//! The latest version of this example is available in the [examples] folder in the repository.
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use crossterm::event::KeyModifiers;
use itertools::Itertools;
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Layout, Margin, Rect},
style::{self, Color, Modifier, Style, Stylize},
Block, BorderType, Cell, HighlightSpacing, Paragraph, Row, Scrollbar, ScrollbarOrientation,
ScrollbarState, Table, TableState,
use style::palette::tailwind;
use unicode_width::UnicodeWidthStr;
const PALETTES: [tailwind::Palette; 4] = [
const INFO_TEXT: [&str; 2] = [
"(Esc) quit | (↑) move up | (↓) move down | (←) move left | (→) move right",
"(Shift + →) next color | (Shift + ←) previous color",
const ITEM_HEIGHT: usize = 4;
fn main() -> Result<()> {
let terminal = ratatui::init();
let app_result = App::new().run(terminal);
selected_row_style_fg: Color,
selected_column_style_fg: Color,
selected_cell_style_fg: Color,
footer_border_color: Color,
const fn new(color: &tailwind::Palette) -> Self {
buffer_bg: tailwind::SLATE.c950,
header_fg: tailwind::SLATE.c200,
row_fg: tailwind::SLATE.c200,
selected_row_style_fg: color.c400,
selected_column_style_fg: color.c400,
selected_cell_style_fg: color.c600,
normal_row_color: tailwind::SLATE.c950,
alt_row_color: tailwind::SLATE.c900,
footer_border_color: color.c400,
const fn ref_array(&self) -> [&String; 3] {
[&self.name, &self.address, &self.email]
fn address(&self) -> &str {
fn email(&self) -> &str {
longest_item_lens: (u16, u16, u16), // order is (name, address, email)
scroll_state: ScrollbarState,
let data_vec = generate_fake_names();
state: TableState::default().with_selected(0),
longest_item_lens: constraint_len_calculator(&data_vec),
scroll_state: ScrollbarState::new((data_vec.len() - 1) * ITEM_HEIGHT),
colors: TableColors::new(&PALETTES[0]),
pub fn next_row(&mut self) {
let i = match self.state.selected() {
if i >= self.items.len() - 1 {
self.state.select(Some(i));
self.scroll_state = self.scroll_state.position(i * ITEM_HEIGHT);
pub fn previous_row(&mut self) {
let i = match self.state.selected() {
self.state.select(Some(i));
self.scroll_state = self.scroll_state.position(i * ITEM_HEIGHT);
pub fn next_column(&mut self) {
self.state.select_next_column();
pub fn previous_column(&mut self) {
self.state.select_previous_column();
pub fn next_color(&mut self) {
self.color_index = (self.color_index + 1) % PALETTES.len();
pub fn previous_color(&mut self) {
let count = PALETTES.len();
self.color_index = (self.color_index + count - 1) % count;
pub fn set_colors(&mut self) {
self.colors = TableColors::new(&PALETTES[self.color_index]);
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
terminal.draw(|frame| self.draw(frame))?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
let shift_pressed = key.modifiers.contains(KeyModifiers::SHIFT);
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
KeyCode::Char('j') | KeyCode::Down => self.next_row(),
KeyCode::Char('k') | KeyCode::Up => self.previous_row(),
KeyCode::Char('l') | KeyCode::Right if shift_pressed => self.next_color(),
KeyCode::Char('h') | KeyCode::Left if shift_pressed => {
KeyCode::Char('l') | KeyCode::Right => self.next_column(),
KeyCode::Char('h') | KeyCode::Left => self.previous_column(),
fn draw(&mut self, frame: &mut Frame) {
let vertical = &Layout::vertical([Constraint::Min(5), Constraint::Length(4)]);
let rects = vertical.split(frame.area());
self.render_table(frame, rects[0]);
self.render_scrollbar(frame, rects[0]);
self.render_footer(frame, rects[1]);
fn render_table(&mut self, frame: &mut Frame, area: Rect) {
let header_style = Style::default()
.fg(self.colors.header_fg)
.bg(self.colors.header_bg);
let selected_row_style = Style::default()
.add_modifier(Modifier::REVERSED)
.fg(self.colors.selected_row_style_fg);
let selected_col_style = Style::default().fg(self.colors.selected_column_style_fg);
let selected_cell_style = Style::default()
.add_modifier(Modifier::REVERSED)
.fg(self.colors.selected_cell_style_fg);
let header = ["Name", "Address", "Email"]
let rows = self.items.iter().enumerate().map(|(i, data)| {
let color = match i % 2 {
0 => self.colors.normal_row_color,
_ => self.colors.alt_row_color,
let item = data.ref_array();
.map(|content| Cell::from(Text::from(format!("\n{content}\n"))))
.style(Style::new().fg(self.colors.row_fg).bg(color))
Constraint::Length(self.longest_item_lens.0 + 1),
Constraint::Min(self.longest_item_lens.1 + 1),
Constraint::Min(self.longest_item_lens.2),
.row_highlight_style(selected_row_style)
.column_highlight_style(selected_col_style)
.cell_highlight_style(selected_cell_style)
.highlight_symbol(Text::from(vec![
.bg(self.colors.buffer_bg)
.highlight_spacing(HighlightSpacing::Always);
frame.render_stateful_widget(t, area, &mut self.state);
fn render_scrollbar(&mut self, frame: &mut Frame, area: Rect) {
frame.render_stateful_widget(
.orientation(ScrollbarOrientation::VerticalRight)
fn render_footer(&self, frame: &mut Frame, area: Rect) {
let info_footer = Paragraph::new(Text::from_iter(INFO_TEXT))
.bg(self.colors.buffer_bg),
.border_type(BorderType::Double)
.border_style(Style::new().fg(self.colors.footer_border_color)),
frame.render_widget(info_footer, area);
fn generate_fake_names() -> Vec<Data> {
use fakeit::{address, contact, name};
let email = contact::email();
.sorted_by(|a, b| a.name.cmp(&b.name))
fn constraint_len_calculator(items: &[Data]) -> (u16, u16, u16) {
.map(UnicodeWidthStr::width)
.map(UnicodeWidthStr::width)
.map(UnicodeWidthStr::width)
#[allow(clippy::cast_possible_truncation)]
(name_len as u16, address_len as u16, email_len as u16)
fn constraint_len_calculator() {
name: "Emirhan Tala".to_string(),
address: "Cambridgelaan 6XX\n3584 XX Utrecht".to_string(),
name: "thistextis26characterslong".to_string(),
address: "this line is 31 characters long\nbottom line is 33 characters long"
let (longest_name_len, longest_address_len, longest_email_len) =
crate::constraint_len_calculator(&test_data);
assert_eq!(26, longest_name_len);
assert_eq!(33, longest_address_len);
assert_eq!(40, longest_email_len);