The vote was greeted with relief in Zagreb, but anger and dismay in Bucharest and Sofia.
Germany's foreign minister said it was a bad day for Europe.
The European Commission had backed all three countries as meeting the criteria necessary for joining the zone, which includes 420 million people.
"I'm also disappointed," said EU home affairs commissioner Ylva Johansson.
Croatia joined the EU in 2013; Romania and Bulgaria became member states six years earlier.
Croatian Prime Minister Andrej Plenkovic praised the decision, which means that Croatia will decommission its border crossings with EU neighbours Slovenia and Hungary on 1 January, the same day as it joins the euro.
Created in 1985, the Schengen area allows people and goods to travel freely, usually without showing travel or customs documents.
It includes 22 of the EU's 27 states as well as Norway, Switzerland, Iceland and Liechtenstein.
Austria and the Netherlands voted against admitting Romania and Bulgaria, citing concerns that both were soft on illegal migration. The most vehement opposition came from Austria.
"It is not right for a system that is not working in many places to be expanded at this time," said Austrian Interior Minister Gerhard Karner, explaining his country's veto.
As its main reason for keeping Romania and Bulgaria out of Schengen, the government in Vienna cited a rapid increase in the number of migrants entering Austria illegally through the West Balkan route.
However, political leaders in Romania and Bulgaria argue that relatively few cross their territory, and that their bid to join Schengen has been derailed for political reasons. They say the Austrian and Dutch governments are looking to attract the anti-immigrant vote.
EU Border Agency Frontex has reported 128,000 "irregular entries" into the EU so far this year from the West Balkans, a 77% increase on 2021, with 22,300 in October alone. These figures include multiple attempts by the same people.
The Austrian government says 75,000 irregular migrants are currently in the country. Some 40% are from India and Tunisia, after flying into non-EU country Serbia legally under a no-visa scheme; a further 40% are from countries led by Afghanistan, Pakistan, and Syria.
Romania said it did not understand Austria's "inflexible decision".
The head of the ruling Social Democrats, Marcel Ciolacu, went further: "European unity and stability received a tough blow today from a state that chose, in difficult times, to abandon its European comrades and serve instead the interests of Russia."
But Bulgaria's acting interior minister, Ivan Demerdzhiev, was more diplomatic: "Austria has made it clear it is ready for compromise and wants a complete reform of the Schengen area in those places where the mechanisms do not work," he said, holding out hope that Thursday's decision would be reversed next year.
There was criticism inside Austria as well, with migration researcher Judith Kohlenberger pointing out that the majority of people who had come to Austria for protection were already in the EU.
"To blame Romania and Bulgaria but not Hungary or even Croatia, which continue systematic push-backs, is both irrational and hypocritical," she said.